diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 000000000..93a45e6b8 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: c6bdf4c1e4f787f482940201325cee80 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..ee565f1c1 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,44 @@ +name: Build docs and publish to gh-pages branch and GitHub Pages +on: + push: + branches: + - development + paths-ignore: + - '.github/**' + + workflow_dispatch: + +jobs: + gh-pages: + name: Publish Github Pages + runs-on: ubuntu-latest + steps: + - name: Setup locale + run: | + sudo locale-gen en_US.UTF-8 + sudo update-locale + - name: Install dependencies + run: | + sudo apt update -y + sudo apt install -y python3-appdirs python3-humanfriendly python3-humanize python3-jsonpickle python3-paramiko python3-psutil python3-watchdog + sudo apt install -y pandoc dia python3-paramiko + sudo apt install -y python3-sphinx python3-nbsphinx python3-sphinx-rtd-theme + sudo apt install -y python3-amqp + - name: Checkout + uses: actions/checkout@master + with: + fetch-depth: 0 + - name: install Sarracenia + run: | + pip3 install . + - name: Build and Commit + uses: sphinx-notes/pages@v2 + with: + documentation_path: ./docs/source + requirements_path: ./requirements-dev.txt + - name: Push changes + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: gh-pages + diff --git a/.github/workflows/flow.yml b/.github/workflows/flow.yml new file mode 100644 index 000000000..3e64db865 --- /dev/null +++ b/.github/workflows/flow.yml @@ -0,0 +1,88 @@ +name: Flow Tests / AMQP + +on: + pull_request: + types: [opened, edited, reopened] + push: + branches: + - development + - v2_dev + - stable + - v2_stable + + paths-ignore: + - '.github/**' + - 'debian/changelog' + - 'TODO.txt' + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + +jobs: + + run_sr_insects_tests: + + strategy: + # Don't cancel the entire matrix when one job fails + fail-fast: false + matrix: + which_test: [ static_flow, no_mirror, flakey_broker, dynamic_flow, restart_server ] + osver: [ "ubuntu-20.04", "ubuntu-22.04" ] + + runs-on: ${{ matrix.osver }} + + name: ${{ matrix.which_test }} on ${{ matrix.osver }} + timeout-minutes: 40 + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + travis/flow_autoconfig.sh + travis/ssh_localhost.sh + + - name: Setup ${{ matrix.which_test }} test. + run: | + cd ${HOME}; pwd; ls ; + echo hoho + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_setup.sh + + # Enable tmate debugging of manually-triggered workflows if the input option was provided + # https://github.com/marketplace/actions/debugging-with-tmate + # 2023/07/22 PAS, removed because when manually invoking test it always enables in spite + # of the github box not being checked, so cannot invoke the tests manually. + # + #- name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # if: ${{ github.event.inputs.debug_enabled }} + # #if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + + - name: Limit ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_limit.sh + + - name: Check results of ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_check.sh + + - name: Compress log files for artifacts + if: always() + continue-on-error: true + run: | + sr3 stop --dangerWillRobinson=1 + cd ${HOME}/.cache/sr3/ + tar -czf ${HOME}/cache_sr3.tar.gz * + + - name: Save run artifacts + if: always() + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: sr3_${{ matrix.which_test }}_${{ matrix.osver }}_state_${{ github.sha }} + path: ~/cache_sr3.tar.gz diff --git a/.github/workflows/flow_amqp_consumer.yml b/.github/workflows/flow_amqp_consumer.yml new file mode 100644 index 000000000..a5d861bd5 --- /dev/null +++ b/.github/workflows/flow_amqp_consumer.yml @@ -0,0 +1,81 @@ +name: Flow Tests / AMQP Consumer + +on: + pull_request: + types: [opened, edited, reopened] + push: + branches: + - development + + paths-ignore: + - '.github/**' + - 'debian/changelog' + - 'TODO.txt' + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + +jobs: + + run_sr_insects_tests: + + strategy: + # Don't cancel the entire matrix when one job fails + fail-fast: false + matrix: + which_test: [ static_flow, no_mirror, flakey_broker, dynamic_flow, restart_server ] + osver: [ "ubuntu-20.04", "ubuntu-22.04" ] + + runs-on: ${{ matrix.osver }} + + name: ${{ matrix.which_test }} on ${{ matrix.osver }} + timeout-minutes: 40 + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + # 2023-11-13 RS Added SSH config changes to see if it makes the tests more reliable + run: | + travis/flow_autoconfig.sh + travis/ssh_localhost.sh + sudo sh -c 'echo "MaxSessions 750" >> /etc/ssh/sshd_config' + sudo sh -c 'echo "MaxStartups 750" >> /etc/ssh/sshd_config' + sudo systemctl restart ssh + echo "amqp_consumer True" >> ${HOME}/.config/sr3/default.conf + #echo "set sarracenia.moth.amqpconsumer.AMQPConsumer.logLevel debug" >> ${HOME}/.config/sr3/default.conf + + - name: Setup ${{ matrix.which_test }} test. + run: | + cd ${HOME}; pwd; ls ; + echo hoho + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_setup.sh + + - name: Limit ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_limit.sh + + - name: Check results of ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_check.sh + + - name: Compress log files for artifacts + if: always() + continue-on-error: true + run: | + sr3 stop --dangerWillRobinson=1 + cd ${HOME}/.cache/sr3/ + tar -czf ${HOME}/cache_sr3.tar.gz * + + - name: Save run artifacts + if: always() + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: sr3_${{ matrix.which_test }}_${{ matrix.osver }}_state_${{ github.sha }} + path: ~/cache_sr3.tar.gz diff --git a/.github/workflows/flow_basic.yml b/.github/workflows/flow_basic.yml new file mode 100644 index 000000000..e943f16e8 --- /dev/null +++ b/.github/workflows/flow_basic.yml @@ -0,0 +1,53 @@ +name: sr_insects test basic declare/cleanup, and python API + +on: + pull_request: + types: [opened, edited, reopened] + push: + paths-ignore: + - '.github/**' + - 'debian/changelog' + - 'TODO.txt' + + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + +jobs: + + run_sr_insects_tests: + + strategy: + # Don't cancel the entire matrix when one job fails + fail-fast: false + matrix: + osver: [ "ubuntu-20.04", "ubuntu-22.04" ] + + runs-on: ${{ matrix.osver }} + + name: Maintenance test on ${{ matrix.osver }} + timeout-minutes: 20 + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + travis/flow_autoconfig.sh + travis/ssh_localhost.sh + + # Enable tmate debugging of manually-triggered workflows if the input option was provided + - name: Setup tmate session + uses: mxschmitt/action-tmate@v3 + if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }} + + - name: Add and Remove configs, + run: | + pwd + ls + cd ${HOME}/sr_insects/static_flow; ./flow_maint_test.sh diff --git a/.github/workflows/flow_mqtt.yml b/.github/workflows/flow_mqtt.yml new file mode 100644 index 000000000..e5f8d5921 --- /dev/null +++ b/.github/workflows/flow_mqtt.yml @@ -0,0 +1,86 @@ +name: Flow Tests / MQTT + +on: + pull_request: + types: [opened, edited, reopened] + push: + branches: + - never + paths-ignore: + - '.github/**' + - 'debian/changelog' + - 'TODO.txt' + + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + +jobs: + + run_sr_insects_tests: + + strategy: + # Don't cancel the entire matrix when one job fails + fail-fast: false + matrix: + which_test: [ static_flow, no_mirror, flakey_broker, dynamic_flow ] + osver: [ "ubuntu-22.04" ] + + runs-on: ${{ matrix.osver }} + + name: ${{ matrix.which_test }} on ${{ matrix.osver }} + timeout-minutes: 45 + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + travis/flow_autoconfig.sh + travis/flow_autoconfig_add_mosquitto.sh + travis/ssh_localhost.sh + + - name: Setup ${{ matrix.which_test }} test. + run: | + cd ${HOME}; pwd; ls ; + echo hoho + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_setup.sh + + # Enable tmate debugging of manually-triggered workflows if the input option was provided + # https://github.com/marketplace/actions/debugging-with-tmate + # + # 2023/06/06 - pas - this ALWAYS enables... some weird bug. commenting out to be able + # to run at all. + #- name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # if: ${{ github.event.inputs.debug_enabled }} + + - name: Limit ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_limit.sh + sleep 300 + + - name: Check results of ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_check.sh + + - name: Compress log files for artifacts + if: always() + continue-on-error: true + run: | + sr3 stop --dangerWillRobinson=1 + cd ${HOME}/.cache/sr3/ + tar -czf ${HOME}/cache_sr3.tar.gz * + + - name: Save run artifacts + if: always() + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: sr3_${{ matrix.which_test }}_${{ matrix.osver }}_state_${{ github.sha }} + path: ~/cache_sr3.tar.gz diff --git a/.github/workflows/flow_redis.yml b/.github/workflows/flow_redis.yml new file mode 100644 index 000000000..5502e0cda --- /dev/null +++ b/.github/workflows/flow_redis.yml @@ -0,0 +1,86 @@ +name: Flow Tests / Redis + +on: + pull_request: + types: [opened, edited, reopened] + push: + branches: + - v03_disabled + + paths-ignore: + - '.github/**' + - 'debian/changelog' + - 'TODO.txt' + + workflow_dispatch: + inputs: + debug_enabled: + type: boolean + description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)' + required: false + default: false + +jobs: + + run_sr_insects_tests: + + strategy: + # Don't cancel the entire matrix when one job fails + fail-fast: false + matrix: + which_test: [ static_flow, no_mirror, flakey_broker, dynamic_flow, restart_server ] + osver: [ "ubuntu-22.04" ] + + runs-on: ${{ matrix.osver }} + + name: ${{ matrix.which_test }} test on ${{ matrix.osver }} + timeout-minutes: 40 + + steps: + - uses: actions/checkout@v3 + + # Enable tmate debugging of manually-triggered workflows if the input option was provided + # https://github.com/marketplace/actions/debugging-with-tmate + # + # - name: Setup tmate session + # uses: mxschmitt/action-tmate@v3 + # if: ${{ github.event.inputs.debug_enabled }} + # #if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled }} + + - name: Install dependencies + run: | + travis/flow_autoconfig.sh + travis/ssh_localhost.sh + travis/add_redis.sh + + - name: Setup ${{ matrix.which_test }} test. + run: | + cd ${HOME}; pwd; ls ; + echo hoho + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_setup.sh + + + + - name: Limit ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_limit.sh + + - name: Check results of ${{ matrix.which_test }} test. + run: | + cd ${HOME}/sr_insects/${{ matrix.which_test }}; ./flow_check.sh + + - name: Compress log files for artifacts + if: always() + continue-on-error: true + run: | + sr3 stop --dangerWillRobinson=1 + cd ${HOME}/.cache/sr3/ + tar -czf ${HOME}/cache_sr3.tar.gz * + + - name: Save run artifacts + if: always() + uses: actions/upload-artifact@v3 + continue-on-error: true + with: + name: sr3_${{ matrix.which_test }}_${{ matrix.osver }}_state_${{ github.sha }} + path: ~/cache_sr3.tar.gz diff --git a/.github/workflows/ghcr-base.yml b/.github/workflows/ghcr-base.yml new file mode 100644 index 000000000..9ec62eb63 --- /dev/null +++ b/.github/workflows/ghcr-base.yml @@ -0,0 +1,62 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: build base container image to GitHub Container Registry + +on: + schedule: + - cron: '17 2 1 * *' + + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: sarracenia_base + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + push: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Checkout branch + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Rename image for publication + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + echo IMAGE_ID=$IMAGE_ID >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v2.7.0 + with: + context: ./ + file: ./Dockerfile_base + platforms: linux/arm64, linux/amd64 + push: true + tags: | + ${{ env.IMAGE_ID }}:latest diff --git a/.github/workflows/ghcr-dev.yml b/.github/workflows/ghcr-dev.yml new file mode 100644 index 000000000..5484b0775 --- /dev/null +++ b/.github/workflows/ghcr-dev.yml @@ -0,0 +1,63 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: build development container image to GitHub Container Registry + +on: + push: + branches: + - development + + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: sarracenia + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + push: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Checkout branch + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Rename image for publication + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + echo IMAGE_ID=$IMAGE_ID >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v2.7.0 + with: + context: ./ + file: ./Dockerfile + platforms: linux/arm64, linux/amd64 + push: true + tags: | + ${{ env.IMAGE_ID }}:development diff --git a/.github/workflows/ghcr.yml b/.github/workflows/ghcr.yml new file mode 100644 index 000000000..631de5f61 --- /dev/null +++ b/.github/workflows/ghcr.yml @@ -0,0 +1,72 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: Build stable container image to GitHub Container Registry + +on: + push: + branches: + - stable + +env: + REGISTRY: ghcr.io + IMAGE_NAME: sarracenia + +jobs: + # Push image to GitHub Packages. + # See also https://docs.docker.com/docker-hub/builds/ + push: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Checkout branch + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Rename image for publication + run: | + IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME + + # Get actual tagged version + TAG_VERSION=$(git describe --exact-match) + # Change all uppercase to lowercase + IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') + # Strip git ref prefix from version + VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') + # Strip "v" prefix from tag name + [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') + # Use Docker `latest` tag convention when building from stable branch, otherwise branch/tag name is used + #[ "$VERSION" == "stable" ] && VERSION=latest + echo IMAGE_ID=$IMAGE_ID >> $GITHUB_ENV + echo VERSION=$VERSION >> $GITHUB_ENV + echo TAG_VERSION=$TAG_VERSION >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v2.7.0 + with: + context: ./ + file: ./Dockerfile + platforms: linux/arm64, linux/amd64 + push: true + tags: | + ${{ env.IMAGE_ID }}:latest + ${{ env.IMAGE_ID }}:${{ env.VERSION }} + ${{ env.IMAGE_ID }}:${{ env.TAG_VERSION }} diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml new file mode 100644 index 000000000..185acf7cd --- /dev/null +++ b/.github/workflows/publish-to-pypi.yml @@ -0,0 +1,59 @@ +name: Publish ${package_name} to PyPI / GitHub + +on: + push: + branches: + - none + +jobs: + build-n-publish: + name: Build and publish to PyPI + runs-on: ubuntu-latest + + steps: + - name: Checkout source + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: "3.x" + + - name: Build source and wheel distributions + run: | + python -m pip install --upgrade build twine + python -m build + twine check --strict dist/* + - name: Publish distribution to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false + + - name: Get Asset name + run: | + export PKG=$(ls dist/ | grep tar) + set -- $PKG + echo "name=$1" >> $GITHUB_ENV + - name: Upload Release Asset (sdist) to GitHub + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: dist/${{ env.name }} + asset_name: ${{ env.name }} + asset_content_type: application/zip + diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 000000000..29057e35e --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,112 @@ +name: Unit Testing + +on: + pull_request: + types: [opened, edited, reopened, closed, synchronize] + paths: + - '**.py' + branches: + - 'development' + # push: + # paths-ignore: + # - '.github/**' + workflow_dispatch: + +permissions: + contents: write + checks: write + pull-requests: write + +jobs: + build: + runs-on: ubuntu-22.04 + + name: Unit test + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + + pip install -r requirements.txt + pip install -e . + pip install -r tests/requirements.txt + + - name: Test with pytest + run: | + pytest tests --junitxml=tests/junit/test-results.xml \ + --cov-config=tests/.coveragerc --cov=sarracenia --cov-report=html --cov-report=lcov --cov-report=xml \ + --html=tests/report.html --self-contained-html + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/linux@v2 + if: ${{ always() }} + with: + files: | + tests/junit/test-results.xml + + # - name: Publish Coverage Results + # uses: orgoro/coverage@v3.1 + # with: + # coverageFile: tests/coverage/coverage.xml + + # - name: Pytest coverage comment + # uses: MishaKav/pytest-coverage-comment@main + # if: ${{ always() }} + # with: + # #pytest-coverage-path: tests/coverage/coverage.lcov + # pytest-xml-coverage-path: tests/coverage/coverage.xml + # #title: My Coverage Report Title + # #badge-title: My Badge Coverage Title + # #hide-badge: false + # #hide-report: false + # create-new-comment: false + # hide-comment: false + # report-only-changed-files: true + # remove-link-from-badge: false + # #unique-id-for-comment: python3.8 + # junitxml-path: tests/junit/test-results.xml + # junitxml-title: Test Results + + - name: Upload pytest junit results + uses: actions/upload-artifact@v4 + with: + name: results-junit + path: tests/junit/test-results.xml + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} + + - name: Upload pytest HTML report + uses: actions/upload-artifact@v4 + with: + name: results-report + path: tests/report.html + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} + + - name: Upload code coverage report (HTML) + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: tests/coverage/html_report + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} + + - name: Upload code coverage report (LCOV) + uses: actions/upload-artifact@v4 + with: + name: coverage-lcov + path: tests/coverage/coverage.lcov + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} + + - name: Upload code coverage report (XML) + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: tests/coverage/coverage.xml + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1b85615ad --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +win_installer.cfg +dist +*.egg +*.egg-info +.vagrant +.pybuild +*.debhelper +*.debhelper.log +libsarra-c* +sarra-c* +build +pynsist_pkgs +sarra/*.pyc +.idea +.vscode +.pyproject-builddir/ +metpx-sr3-*-pyproject-buildrequires +metpx-sr3-*-pyproject-files +metpx-sr3-*-pyproject-ghost-distinfo +metpx-sr3-*-pyproject-modules +metpx-sr3-*-pyproject-record +pyproject-wheeldir/ + diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/.style.yapf3 b/.style.yapf3 new file mode 100644 index 000000000..8b4e954a8 --- /dev/null +++ b/.style.yapf3 @@ -0,0 +1,3 @@ + +[yapf] +column_limit=119 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..0ee4f7645 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,42 @@ +dist: bionic +services: rabbitmq +language: python +arch: arm64-graviton2 + +matrix: + include: + - name: "3.6-tests" + python: "3.6" + env: LIMIT=500 TEST=tests + - name: "3.6-static_flow" + python: "3.6" + env: LIMIT=500 TEST=static_flow + - name: "3.7-flakey_broker" + python: "3.7" + env: LIMIT=500 TEST=flakey_broker + - name: "3.5-1000limit" + python: "3.5" + env: LIMIT=1000 TEST=dynamic_flow + - name: "3.6-2000limit" + python: "3.6" + env: LIMIT=2000 TEST=dynamic_flow + - name: "3.7-3000limit" + python: "3.7" + env: LIMIT=3000 TEST=dynamic_flow + + +branches: + only: + - main + +install: +- travis/install.sh +- ${HOME}/sr_insects/flow_autoconfig.sh + +script: +- cd ${HOME}/sr_insects/$TEST +- ./flow_setup.sh +- timeout 60m ./flow_limit.sh $LIMIT +- ./flow_check.sh +- ./flow_cleanup.sh + diff --git a/Contribution/AMQPprimer.html b/Contribution/AMQPprimer.html new file mode 100644 index 000000000..1f0ac49ed --- /dev/null +++ b/Contribution/AMQPprimer.html @@ -0,0 +1,510 @@ + + + + + + + AMQP - Primer for Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

AMQP - Primer for Sarracenia

+

This is a short but rather dense briefing to explain +the motivation for the use of AMQP by the MetPX-Sarracenia +data pump. Sarracenia is essentially an AMQP application, +so some understanding AMQP is very helpful. +AMQP is a vast and interesting topic in it’s own right. No attempt is made to explain +all of it here. This brief just provides a little context, and introduces only +background concepts needed to understand and/or use Sarracenia. For more information +on AMQP itself, a set of links is maintained at +the Metpx web site but a search engine +will also reveal a wealth of material.

+ +
+

AMQP Feature Selection

+

AMQP is a universal message passing protocol with many different +options to support many different messaging patterns. MetPX-sarracenia specifies and uses a +small subset of AMQP patterns. An important element of Sarracenia development was to +select from the many possibilities a small subset of methods are general and +easily understood, in order to maximize potential for interoperability.

+
+

Analogy FTP

+

Specifying the use of a protocol alone may be insufficient to provide enough information for +data exchange and interoperability. For example when exchanging data via FTP, a number of choices +need to be made above and beyond the protocol.

+
+
    +
  • authenticated or anonymous use?

  • +
  • how to signal that a file transfer has completed (permission bits? suffix? prefix?)

  • +
  • naming convention

  • +
  • text or binary transfer

  • +
+
+

Agreed conventions above and beyond simply FTP (IETF RFC 959) are needed. Similar to the use +of FTP alone as a transfer protocol is insufficient to specify a complete data transfer +procedure, use of AMQP, without more information, is incomplete. The intent of the conventions +layered on top of AMQP is to be a minimum amount to achieve meaningful data exchange.

+
+
+

AMQP: not 1.0, but 0.8 or 0.9

+

AMQP 1.0 standardizes the on-the-wire protocol, but removed all broker standardization. +As the use of brokers is key to Sarracenia´s use of, was a fundamental element of earlier standards, +and as the 1.0 standard is relatively controversial, this protocol assumes a pre 1.0 standard broker, +as is provided by many free brokers, such as rabbitmq and Apache QPid, often referred to as 0.8, +but 0.9 and post 0.9 brokers could inter-operate well.

+
+
+

Named Exchanges and Queues

+

In AMQP prior to 1.0, many different actors can define communication parameters, such as exchanges +to publish to, queues where notification messages accumulate, and bindings between the two. Applications +and users declare and user their exchanges, queues, and bindings. All of this was dropped +in the move to 1.0 making topic based exchanges, an important underpinning of pub/sub patterns +much more difficult.

+

in AMQP 0.9, one subscriber can declare a queue, and then multiple processes (given the right +permissions and the queue name) can consume from the same queue. That requires being able +to name the queue. In another protocol, such as MQTT, one cannot name the queue, and so +this processing pattern is not supported.

+

The mapping convention described in Topic, allows +MQTT to establish separate hierarchies which provides a fixed distribution among +the workers, but not exactly the self-balancing shared queue that AMQP provides.

+
+

Note

+

In RabbitMQ (the initial broker used), permissions are assigned using regular expressions. So +a permission model where AMQP users can define and use their exchanges and queues +is enforced by a naming convention easily mapped to regular expressions (all such +resources include the username near the beginning). Exchanges begin with: xs_<user>_. +Queue names begin with: q_<user>_.

+
+
+
+

Topic-based Exchanges

+

Topic-based exchanges are used exclusively. AMQP supports many other types of exchanges, +but sr3_post have the topic sent in order to support server side filtering by using topic +based filtering. At AMQP 1.0, topic-based exchanges (indeed all exchanges, are no +longer defined.) Server-side filtering allows for much fewer topic hierarchies to be used, +and for much more efficient subsciptions.

+

In Sarracenia, topics are chosen to mirror the path of the files being announced, allowing +straight-forward server-side filtering, to be augmented by client-side filtering on +message reception.

+

The root of the topic tree is the version of the message payload. This allows single brokers +to easily support multiple versions of the protocol at the same time during transitions. v02, +created in 2015, is the third iteration of the protocol and existing servers routinely support previous +versions simultaneously in this way. The second sub-topic defines the type of message. +At the time of writing: v02.post is the topic prefix for current notification messages.

+
+
+

Little Data

+

The AMQP messages contain notification messages, no actual file data. AMQP is optimized for and assumes +small messages. Keeping the messages small allows for maximum message throughtput and permits +clients to use priority mechanisms based on transfer of data, rather than the notification messages. +Accomodating large messages would create many practical complications, and inevitably require +the definition of a maximum file size to be included in the message itself, resulting in +complexity to cover multiple cases.

+

Sr3_post is intended for use with arbitrarily large files, via segmentation and multi-streaming. +Blocks of large files are announced independently and blocks can follow different paths +between initial pump and final delivery. The protocol is unidirectional, in that there +is no dialogue between publisher and subscriber. Each post is a stand-alone item that +is one message in a stream, which on receipt may be spread over a number of nodes.

+

However, it is likely that, for small files over high latency links, it is +more efficient to include the body of the files in the notification messages themselves, +rather than forcing a separate retrieval phase. The relative advantage depends on:

+
    +
  • relative coarseness of server side filtering means some filtering is done on +the client side. Any data embedded for notification messages discarded on the client-side +are waste.

  • +
  • Sarracenia establishes long-lived connections for some protocols, such as SFTP, +so the relative overhead for a retrieval may not be long.

  • +
  • One will achieve a higher messaging rate without data being embedded, and if the +notification messages are distributed to a number of workers, it is possible that the resulting +message rate is higher without embedded data (because of faster distribution for +parallel download) than the savings from embedding.

  • +
  • the lower the latency of the connection, the lesser the performance advantage +of embedding, and the more it becomes a limiting factor on high performance +transfers.

  • +
+

Further work is needed to better clarify when it makes sense to embed content +in notification messages. For now, the content header is included to allow such experiments +to occur.

+
+
+

Other Parameters

+

AMQP has many other settings, and reliability for a particular use case +is assured by making the right choices.

+
    +
  • persistence (have queues survive broker restarts, default to true),

  • +
  • expiry (how long a queue should exist when no-one is consuming from it. Default: a few +minutes for development, but can set much longer for production)

  • +
  • message_ttl (the life-span of queued notification messages. Messages that are too old will not +be delivered: default is forever.)

  • +
  • Pre-fetch is an AMQP tunable to determine how many notification messages a client will +retrieve from a broker at once, optimizing streaming. (default: 25)

  • +
+

These are used in declarations of queues and exchanges to provide appropriate +message processing. This is not an exhaustive list.

+
+
+
+

Mapping AMQP Concepts to Sarracenia

+../_images/AMQP4Sarra.svg +

An AMQP Server is called a Broker. Broker is sometimes used to refer to the software, +other times server running the broker software (same confusion as web server.) In the above diagram, AMQP vocabulary is in Orange, and Sarracenia terms are in blue.

+

There are many different broker software implementations. We use rabbitmq. +Not trying to be rabbitmq specific, but management functions differ between implementations. +So admin tasks require ‘porting’ while the main application elements do not.

+
+
Queues are usually taken care of transparently, but you need to know
    +
  • A Consumer/subscriber creates a queue to receive notification messages.

  • +
  • Consumer queues are bound to exchanges (AMQP-speak)

  • +
+
+
An exchange is a matchmaker between publisher and consumer queues.
    +
  • A message arrives from a publisher.

  • +
  • message goes to the exchange, is anyone interested in this message?

  • +
  • in a topic based exchange, the message topic provides the exchange key.

  • +
  • interested: compare message key to the bindings of consumer queues.

  • +
  • message is routed to interested consumer queues, or dropped if there aren’t any.

  • +
+
+
+
    +
  • +
    Multiple processes can share a queue, they just take turns removing notification messages from it.
      +
    • This is used heavily for sr_sarra and sr_subcribe multiple instances.

    • +
    +
    +
    +
  • +
  • Queues can be durable, so even if your subscription process dies, +if you come back in a reasonable time and you use the same queue, +you will not have missed any notification messages.

  • +
  • +
    How to Decide if Someone is Interested.
      +
    • For Sarracenia, we use (AMQP standard) topic based exchanges.

    • +
    • Subscribers indicate what topics they are interested in, and the filtering occurs server/broker side.

    • +
    • Topics are just keywords separated by a dot. wildcards: # matches anything, * matches one word.

    • +
    • We create the topic hierarchy from the path name (mapping to AMQP syntax)

    • +
    • Resolution & syntax of server filtering is set by AMQP. (. separator, # and * wildcards)

    • +
    • Server side filtering is coarse, notification messages can be further filtered after download using regexp on the actual paths (the reject/accept directives.)

    • +
    +
    +
    +
  • +
  • +
    topic prefix? We start the topic tree with fixed fields
      +
    • v02 the version/format of sarracenia notification messages.

    • +
    • post … the message type, this is an notification message +of a file (or part of a file) being available.

    • +
    +
    +
    +
  • +
+
+
+

Sarracenia is an MQP Application

+

in Version 2, MetPX-Sarracenia is only a light wrapper/coating around AMQP. +in Version 3, this was reworked and an MQTT driver was added to make it +less AMQP specific.

+
    +
  • A MetPX-Sarracenia pump is a python AMQP application that uses an (rabbitmq) +broker to co-ordinate SFTP and HTTP client data transfers, and accompanies a +web server (apache) and sftp server (openssh) on the same user-facing address.

  • +
  • Wherever reasonable, we use their terminology and syntax. +If someone knows AMQP, they understand. If not, they can research.

    +
      +
    • Users configure a broker, instead of a pump.

    • +
    • users explicitly can pick their queue names.

    • +
    • users set subtopic,

    • +
    • topics with dot separator are minimally transformed, rather than encoded.

    • +
    • queue durable.

    • +
    • we use message headers (AMQP-speak for key-value pairs) rather than encoding in JSON or some other payload format.

    • +
    +
  • +
  • +
    reduce complexity through conventions.
      +
    • use only one type of exchanges (Topic), take care of bindings.

    • +
    • +
      naming conventions for exchanges and queues.
        +
      • exchanges start with x. +- xs_Weather - the exchange for the source (amqp user) named Weather to post notification messages +- xpublic – exchange used for most subscribers.

      • +
      • queues start with q

      • +
      +
      +
      +
    • +
    +
    +
    +
  • +
  • Internet resources are more useful and reduce our documentation burden.

  • +
  • We write less code (exposing raw AMQP means less glue.)

  • +
  • Less potential for bugs/ higher reliability.

  • +
  • we make minimum number of choices/restrictions

  • +
  • set sensible defaults.

  • +
+
+
+

Review

+

If you understood the rest of the document, this should make sense to you:

+

An AMQP broker is a server process that houses exchanges and queues used to route notification messages +with very low latency. A publisher sends notification messages to an exchange, while a consumer reads +notification messages from their queue. Queues are bound to exchanges. Sarracenia links a broker +to a web server to provide fast notifications, and uses topic exchanges to enable +consumers’ server side filtering. The topic tree is based on the file tree you can +browse if you visit the corresponding web server.

+
+
+

Appendix A: Background

+
+

Why Use AMQP?

+
    +
  • open standard, multiple free implementations.

  • +
  • low latency message passing.

  • +
  • encourages asynchronous patterns/methods.

  • +
  • language, protocol & vendor neutral.

  • +
  • very reliable.

  • +
  • robust adoption (next two sections as examples)

  • +
+
+
+

Where does AMQP Come From?

+
    +
  • Open International standard from financial world.

  • +
  • Many proprietary similar systems exist, AMQP built to get away from lock-in. Standard is built with long experience of vendor messaging systems, and so quite mature.

  • +
  • invariably used behind the scenes as a component in server-side processing, not user visible.

  • +
  • many web companies (soundcloud)

  • +
  • seeing good adoption in monitoring and integration for HPC

  • +
+
+
+

Intel/Cray HPC Stack

+

Intel/Cray HPC stack

+../_images/IntelHPCStack.png + +
+
+

OpenStack

+

AMQP is the messaging technology chosen by the OpenStack cloud.

+../_images/OpenStackArch.png + +
+
+

How to Adopt AMQP

+

Adopting AMQP is more like adopting XML than it is like adopting FTP. FTP interoperability +is easy as choices are limited. With XML, however you get more palette than painting. Many +different dialects, schema methods, etc… XML will be valid and parse, but without +additional standardization, data exchange remains uncertain. For real interoperabiltiy, +one must standardize specific dialects. Examples:

+
+
    +
  • RSS/Atom,

  • +
  • Common Alerting Protocol (CAP)

  • +
+
+

AMQP brokers and the client software can connect and send notification messages, but without +additional standardization, applications will not communicate. AMQP calls +those additional layers applications. AMQP enables every conceivable message +pattern, so a well formed application is built by eliminating features from +consideration, choosing the colours to use. +Sarracenia is an applicaton of AMQP message passing to file transfer.

+

As CAP narrows XML, Sarracenia narrows the scope of AMQP. This narrowing is necessary to obtain a useful result: Interoperability. Sarracenia conventions and formats are defined in:

+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/BasicIdea.html b/Contribution/BasicIdea.html new file mode 100644 index 000000000..6377b43bd --- /dev/null +++ b/Contribution/BasicIdea.html @@ -0,0 +1,318 @@ + + + + + + + Basic Idea — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Approved-Draft1-20150608

+
+

Basic Idea

+

MetPX-Sarracenia is a data duplication or distribution engine that leverages existing +standard technologies (sftp and web servers and AMQP brokers) to achieve real-time message +delivery and end to end transparency in file transfers. Whereas in Sundew, each +pump is a standalone configuration which transforms data in complex ways, in +sarracenia, the data sources establish a structure which is carried through any +number of intervening pumps until they arrive at a client. The consumer can +provide explicit acknowledgement that propagates back through the network to the +source.

+

Whereas traditional file pumping is a point-to-point affair where knowledge is only +between each segment, in Sarracenia, information flows from end to end in both directions. +At it’s heart, sarracenia exposes a tree of web accessible folders (WAF), using +any standard HTTP server (tested with apache). Weather applications are soft real-time, +where data should be delivered as quickly as possible to the next hop, and +minutes, perhaps seconds, count. The standard web push technologies, ATOM, RSS, etc… +are actually polling technologies that when used in low latency applications consume a great +deal of bandwidth an overhead. For exactly these reasons, those standards +stipulate a minimum polling interval of five minutes. Advanced Message Queueing +Protocol (AMQP) messaging brings true push to notifications, and makes real-time +sending far more efficient.

+../_images/e-ddsr-components.jpg +

Sources of data announce their products, pumping systems pull the data onto their +WAF trees, and then announce their trees for downstream clients. When clients +download data, they may write a log message back to the server. Servers are configured +to forward those client log messages back through the intervening servers back to +the source. The Source can see the entire path that the data took to get to each +client. With traditional pumping applications, sources only see that they delivered +to the first hop in a chain. Beyond that first hop, routing is opaque, and tracing +the path of data required assistance from administrators of each intervening system. +With Sarracenia’s log forwarding, the pumping network is completely transparent +to the sources, in that they can see where it went. With end to end logs, diagnostics +are vastly simplified for everyone.

+

For large files / high performance, files are segmented on ingest if they are sufficiently +large to make this worthwhile. Each file can traverse the pump network independently, +and reassembly is only needed at end points. A file of sufficient size will announce +the availability of several segments for transfer, multiple threads or transfer nodes +will pick up segments and transfer them. The more segments available, the higher +the parallelism of the transfer. Sarracenia manages parallelism and network usage +without explicit user intervention. As intervening pumps do not store and +forward entire files, the maximum file size which can traverse the network is +maximized.

+

These concepts below are not in order (yet?) maybe we will do that later. +not sure about priorities, just number to be able to refer to them. +They are meant to help guide (reflect?) design/implementation decisions:

+

For each objective/consideration/advice below, see if they make sense, +and seem helpful. We should get rid of any that are not helpful.

+
    +
  1. The pump is, or any number of pumps are, transparent. +Put another way: +The source is in charge of the data they provide.

    +

    The source determines the distribution (scope, and permissions) +The source can obtain any information about themselves:

    +
    - when status changed:  start,stop,drop.
    +- when notification messages are accepted.
    +- when data is pulled by a consumer (a scope layer, or a end point.)
    +
    +
    +
  2. +
  3. AMQP brokers do not transfer any user data, just metadata.

    +

    reasoning: +need to keep the notification messages small so that the forwarding rate is high. +large notification messages will gum up the works. also permissions become interesting. +end up with a ‘maximum size’ threshold, and implementing two methods for everything.

    +
  4. +
  5. Config changes should propagate, not be unique to a host +you should not have to do dsh, or px-push. +That sort of management is built in. the message bus is there for that. +might use ‘scope’ to have commands propagate through multiple clusters.

  6. +
  7. Log is data.

    +

    It is not enough for justice to be done. Justice must be seen to be done.

    +

    It is not enough for data to be delivered. That delivery must be logged, +and that log must be returned to the source. While we want to supply +enough information to data sources, we do not want to drown the network +in meta data. The local component logs will have much more information, +The log messages traverse the network to the source are ´final dispositions´ +whenever an operation is either completed or finally abandoned.

    +
  8. +
  9. This is a data distribution tool, not a file tree replicator.

    +
      +
    • we do not need to know what linux uid/gid owned it originally.

    • +
    • we do not care when it was modified.

    • +
    • we do not care about it’s original permission bits.

    • +
    • we do not care what ACL’s it has (they aren’t relevant on the destination.)

    • +
    • we do not care about extended attributes. (portability, win,mac,lin,netapp?)

    • +
    +

    again doubtful about this one. Does it help?

    +
  10. +
  11. Not worried about performance in phase 1 +- performance is enabled by the scalability of the design:

    +
    -- segmentation/re-assembly provides multi-threading.
    +-- segmentation means bigger files transfer with greater parallelism.
    +   adds multiple streams when that is worthwhile, uses a single stream
    +   when that makes sense.
    +-- validation provides source bandwidth limiting.
    +
    +
    +
      +
    • need to prove all the moving parts work together first.

    • +
    • much later, may return to see how to make each transfer engine +go faster.

    • +
    +
  12. +
  13. This is not a web application, this is not an FTP server.

    +

    This application uses HTTP as one of the transport protocols, that’s all. +It is not trying to be a web site, any more than it is trying to be an sftp server.

    +
  14. +
  15. Common management not needed, just pass logs around.

    +

    Different groups can manage different pumps. +when we interconnect pumps, they become a source for us. +log messages are routed to the data sources, so they get our logs on their +data. (security can have something to say about that.)

    +
  16. +
  17. It needs to run anywhere. +ubuntu,centos – primary. +but windows also.

    +

    We are trying to make a pump that others can easily adopt. +That means they can install and go.

    +
    +
    It needs to be easy to set up, both client and server.

    (this aspect dealt with in packaging)

    +
    +
    +
  18. +
  19. the application does not need to pursue absolute reliability.

  20. +
+
+

Node failure is rare in a Data Centre environment. +Working well in the normal case is the priority. +if it breaks, information is never lost. +Worst case, just re-post, and the system will resend the missing parts +through the nodes that are left.

+

There might be some diagnostics to figure out which files are ‘in flight’ +when a given node goes down (deadman timers). But not sure that multiple +acks with guarantees in the face of node failure is needed. +going faster and being simpler is likely more reliable in practice.

+

this is not a database, but a transfer engine.

+
+
    +
  1. Bulletins getting less common, Files are larger… No file too large.

  2. +
+
+

old apps are used to tiny files (millions of them) in EC/MSC. +but even in EC, files are getting bigger, and will likely grow a lot. +Satellite sensor data is now very critical, and that is substantially larger. +A traditional WMO format weather warning was limited to 15Kbytes (limited by internals +systems to 32 Kbytes now) and those sizes were rarely reached. It was more like 7-12K. +an average modern XML weather warning (CAP) is 60K so, so a five to eight fold increase. +WMO since raised the limit to 500,000 bytes for WMO-GTS messages. and other mechanisms, +such as FTP, have no fixed limit.

+

Other scientific domains use very large files (measured in terabytes.) aim to be able +to flow those through the pumps. Worth thinking about transporting huge files.

+
+
    +
  1. Normal operation should not require programming knowledge.

  2. +
+
+

Configuratin and coding are distinct activities. One should not have to modify scripts +to configure standard elements of the application. Software can be much simpler if it +just leaves all features implemented as plug-in scripts. leaving the local details +for the scripts. But most people will not be able to use it.

+

Need to provide all core functionality through CLI at the very least. +config files are consiered part of the CLI, which is why we try to choose carefully +there as well. For programmers, difference between script and config is subtle, +not so for most other people.

+

Scripting should only be required to extend features beyond what is standard. +to provide added flexibility. If the flexibility proves generally useful over time, +then it should be brought out of scripts and into the configuration realm.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Design.html b/Contribution/Design.html new file mode 100644 index 000000000..921c23bb2 --- /dev/null +++ b/Contribution/Design.html @@ -0,0 +1,871 @@ + + + + + + + 1 Strawman Design — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Draft

+
+

1 Strawman Design

+

This document reflects the current design resulting from discussions and thinking +at a more detailed level that the outline document. See Outline +for an overview of the design requirements. See use-cases for +an exploration of functionality of how this design works in different situations. +The way to make progress towards a working implementation is described in plan.

+ +
+

1.1 Assumptions/Constraints

+
+
    +
  • Are there cluster file systems available everywhere? No.

  • +
  • an operational team might want to monitor/alert when certain transfers experience difficulty.

  • +
  • security may want to run different scanning on different traffic (each block?) +security might want us to refuse certain file types, so they go through heavier scanning. +or perform heavier scanning on those file types.

  • +
  • extranet zones cannot initiate connections to internal zones. +extranet zones receive inbound connections from anywhere.

  • +
  • Government operations zones can initiate connections anywhere. +however, Science is considered a sort of extranet to all the partners.

  • +
  • No-one can initiate connections into partner networks, but all partner departments can initiate +connections into science.gc.ca zone. Within the science zones, there is the shared file system +area, where servers access a common cluster oriented file system, as well as some small restricted +zones, where very limited access is afforded to ensure availability.

  • +
  • Within NRC, there are labs with equipment which cannot be maintained, software-wise, +to address disclosed vulnerabilities because of excessive testing dependencies (ie. certifying +that a train shaker still works after applying a patch.) These systems are not given access +to the internet, only to a few other systems on the site.

  • +
  • collaborators are academic, other-governmental, or commercial entities which which government +scientists exchange data.

  • +
  • collaborators connect to extranet resources from their own networks. Similarly to partners, +(subject to exceptions) no connections can be initiated into any collaborator network.

  • +
  • There are no proxies, no systems in the extranet are given exceptional permissions to +initiate inbound connections. File storage protocols etc… are completely isolated between +them. There are no file systems that cross network zone boundaries.

  • +
  • One method of improving service reliability is to use internal services for internal use +and reserve public facing services for external users. Isolated services on the inside +are completely impervious to internet ´weather´ (DDOS of various forms, load, etc…) +internal and external loads can be scaled independently.

  • +
+
+
+
+

1.2 Number of Switches

+

The application is supposed to support any number of topologies, that is any number of pumps S=0,1,2,3 +may exist between origin and final delivery, and do the right thing.

+

Why isn´t everything point to point, or when do you insert a pumps?

+
+
    +
  • network topology/firewall rules sometimes require being at rest in a transfer area between two +organizations. Exception to these rules create vulnerabilities, so prefer to avoid. +whenever traffic prevents initiating a connection, that indicates a store & forward pumps +may be needed.

  • +
  • physical topology. While connectivity may be present, optimal bandwidth use may involve +not taking the default path from A to B, but perhaps passing through C.

  • +
  • when the transfer is not 1:1, but 1:<source does not know how> many. The pumping takes +care of sending it to multiple points.

  • +
  • when the source data needs to be reliably available. This translates to making many copies, +rather than just one, so it is easier for the source to post once, and have the network +take care of replication.

  • +
  • for management reasons, you want to centrally observe data large transfers.

  • +
  • for management reasons, to have transfers routed a certain way.

  • +
  • for management reasons, to ensure that transfer failures are detected and escalated +when appropriate. They can be fixed rather than waiting for ad-hoc monitoring to detect +the issue.

  • +
  • For asynchronous transfers. If the source has many other activities, it may want +to give responsibility to another service to do potentially lengthy file transfers. +the pump is inserted very near to the source, and is full store & forward. sr_post +completes (nearly instant), and from then on the pumping network manages transfers.

  • +
+
+
+
+

1.3 AMQP Feature Selection

+

AMQP is a universal message passing protocol with many different options to support many +different messaging patterns. MetPX-sarracenia specifies and uses a small subset of AMQP +patterns. Indeed an important element of sarracenia development was to select from the +many possibilities a small subset of methods are general and easily understood, in order +to maximize potential for interoperability.

+

Specifying the use of a protocol alone may be insufficient to provide enough information for +data exchange and interoperability. For example when exchanging data via FTP, a number of choices +need to be made above and beyond the basic protocol.

+
+
    +
  • authenticated or anonymous use?

  • +
  • how to signal that a file transfer has completed (permission bits? suffix? prefix?)

  • +
  • naming convention.

  • +
  • text or binary transfer.

  • +
+
+

Agreed conventions above and beyond simply FTP (IETF RFC 959) are needed.

+

Similar to the use of FTP alone as a transfer protocol is insufficient to specify a complete data +transfer procedure, use of AMQP, without more information, is incomplete.

+

AMQP 1.0 standardizes the on the wire protocol, but leaves out many features of broker interaction. +As the use of brokers is key to sarracenia´s use of, was a fundamental element of earlier standards, +and as the 1.0 standard is relatively controversial, this protocol assumes a pre 1.0 standard broker, +as is provided by many free brokers, such as rabbitmq, often referred to as 0.8, but 0.9 and post +0.9 brokers are also likely to inter-operate well.

+

In AMQP, many different actors can define communication parameters. To create a clearer +security model, sarracenia constrains that model: sr3_post clients are not expected to declare +Exchanges. All clients are expected to use existing exchanges which have been declared by +broker administrators. Client permissions are limited to creating queues for their own use, +using agreed upon naming schemes. Queue for client: qc_<user>.????

+

Topic-based exchanges are used exclusively. AMQP supports many other types of exchanges, +but sr3_post have the topic sent in order to support server side filtering by using topic +based filtering. The topics mirror the path of the files being announced, allowing +straight-forward server-side filtering, to be augmented by client-side filtering on +message reception.

+

The root of the topic tree is the version of the message payload. This allows single brokers +to easily support multiple versions of the protocol at the same time during transitions. v02 +is the third iteration of the protocol and existing servers routinely support previous versions +simultaneously in this way. The second topic in the topic tree defines the type of message. +at the time of writing: v02.post is the topic prefix for current notification messages.

+

The AMQP messages contain notification messages, no actual file data. AMQP is optimized for and assumes +small messages. Keeping the messages small allows for maximum message throughtput and permits +clients to use priority mechanisms based on transfer of data, rather than the notification messages. +Accomodating large messages would create many practical complications, and inevitably require +the definition of a maximum file size to be included in the message itself, resulting in +complexity to cover multiple cases.

+

sr_post is intended for use with arbitrarily large files, via segmentation and multi-streaming. +blocks of large files are announced independently. and blocks can follow different paths +between initial pump and final delivery.

+

AMQP vhosts are not used. Never saw any need for them. The commands support their optional +use, but there was no visible purpose to using them is apparent.

+

Aspects of AMQP use can be either constraints or features:

+
+
    +
  • interaction with a broker are always authenticated.

  • +
  • We define the anonymous for use in many configurations.

  • +
  • users authenticate to local cluster only. We don´t impose any sort of credential or identity propagation +or federation, or distributed trust.

  • +
  • pumps represent users by forwarding files on their behalf, there is no need to include +information about the source users later on in the network.

  • +
  • This means that if user A from S0 is defined, and a user is given the same name on S1, then they may +collide. sad. Accepted as a limitation.

  • +
+
+
+
+

1.4 Application

+

Description of application logic relevant to discussion. There is a ´control plane´ where notification messages about new +data available are made, and log messages reporting status of transfers of the same data are routed among +control plane users and pumps. A pump is an AMQP broker, and users authenticate to the broker. Data +may (most of the time does) have a different other authentication method.

+

There are very different security use cases for file transfer:

+
+
    +
  1. Public Dissemination data is being produced, whose confidentiality is not an issue, the purpose is to +disseminate to all who are interested as quickly and reliably as possible, potentially involving many +copies. The data authentication is typically null for this case. Users just issue HTTP GET requests with +no authentication. For AMQP authentication, it can be done as anonymous, with no ability for providers to +monitor. If there is to be support from the data source, then the source would assign a non-anonymous user +for the AMQP traffic, and the client would ensure logging was working, enabling the provider to monitor and +alert when problems arise.

  2. +
  3. Private Transfer proprietary data is being generated, and needs to be moved to somewhere where it can be +archived and/or processed effectively, or shared with specific collaborators. AMQP and HTTP traffic must +be encrypted with SSL/TLS. Authentication is typically common between AMQP and HTTPS. For Apache httpd +servers, the htpasswd/htaccess method will need to be continuously configured by the delivery system. +These transfers can have requirements for be high availability.

  4. +
  5. Third Party Transfer the control plane is explicitly used only to control the transfer, authentication +at both ends is done separately. Users authenticate to the data-less, or SEP pump with AMQP, but the +authentication at both ends is outside sarracenia control. Third-party transfer is limited to S=0. +If the data does not cross the pump, it cannot be forwarded. So no routing is relevant to this case. +Also dependent on the availability of the two end points throughout, so more difficult to assure in practice.

  6. +
+
+

Both public and private transfers are intended to support arbitrary chains of pumps between source and consumer. +The cases depend on routing of notification messages and log messages.

+
+

Note

+

forward routing… Private and Public transfers… not yet clear, still considering. +what is written here on that subject is tentative. wondering if split, and do public +first, then private later?

+
+

To simplify discussions, names will be selected with a prefix things according to the type +of entity:

+
+
    +
  • exchanges start with x.

  • +
  • queues start with q.

  • +
  • users start with u. users are also referred to as sources

  • +
  • servers start with svr

  • +
  • clusters start with c

  • +
  • ´pumps´ is used as a synonym for cluster, and they start with S (capital S.): S0, S1, S2…

  • +
+
+
+
on pumps:
    +
  • users that pumps used to authenticate to each other are interpump accounts. Another word: feeder , concierge ?

  • +
  • users that inject data into the network are called sources.

  • +
  • users that subscribe to data are called consumers.

  • +
+
+
+
+
+

1.5 Routing

+

There are two distinct flows to route: notification messages, and logs. +The following header in messages relate to routing, which are set in all messages.

+
+
    +
  • source - the user that injected the original notification messages.

  • +
  • source_cluster - the cluster where the source injected the notification messages.

  • +
  • to_clust - the comma separated list of destination clusters.

  • +
  • private - the flag to indicate whether the data is private or public.

  • +
+
+

An important goal of notification messages routing is that the source decides where notification messages go, so +pumping of individual products must be done only on the contents of the notification messages, not +some administrator configuration.

+

Administrators configure the inter pump connections (via SARRA and other components) +to align with network topologies, but once set up, all data should flow properly with +only source initiated routing commands. Some configuration may be needed on all pumps +whenever a new pump is added to the network.

+
+

1.5.1 Routing Posts

+

Post routing is the routing of the notification messages announced by data sources. +The data corresponding to the source follows the same sequence of pumps as the notification messages +themselves. When a notification message is processed on a pump, it is downloaded, and then the +notification message is modified to reflect that´s availability from the next-hop pump.

+

Post messages are defined in the sr_post(7) man page. They are initially emitted by sources, +published to xs_source. After Pre-Validation, they go (with modifications described in Security) to +either xPrivate or xPublic.

+
+

Note

+

FIXME: Tentative!? +if not separate exchange, then anyone can see any notification message (not the data, but yes the notification message) +I think that´s not good.

+
+

For Public data, feeders for downstream pumps connect to xPublic. +They look at the to_clust Header in each message, and consult a post2cluster.conf file. +post2cluster.conf is just a list of cluster names configured by the administrator:

+
ddi.cmc.ec.gc.ca
+dd.weather.gc.ca
+ddi.science.gc.ca
+
+
+

This list of clusters is supposed to be the clusters that are reachable by traversing +this pump. If any cluster in post2cluster.conf is listed in the to_clust of the +message field, then the data needs to tr

+

Separate Downstream feeders connect to xPrivate for private data. Only feeders are +allowed to connect to xprivate.

+
+

Note

+

FIXME: perhaps feed specific private exchanges for each feeder? x2ddiedm, x2ddidor, x2ddisci ? +using one xPrivate means pumps can see messages they may not be allowed to download +(lesser issue than with xPublic, but depends how trusted downstream pump is.)

+
+
+
+

1.5.2 Routing Logs

+

Log messages are defined in the sr_log(7) man page. They are emitted by consumers at the end, +as well as feeders as the messages traverse pumps. log messages are posted to +the xl_<user> exchange, and after log validation queued for the xlog exchange.

+

Messages in xlog destined for other clusters are routed to destinations by +log2cluster component using log2cluster.conf configuration file. log2cluster.conf +uses space separated fields: First field is the cluster name (set as per soclust in +notification messages, the second is the destination to send the log messages for posting +originating from that cluster to) Sample, log2cluster.conf:

+
clustername amqp://user@broker/vhost exchange=xlog
+
+
+

Where message destination is the local cluster, log2user (log2source?) will copy +the messages where source=<user> to sx_<user>.

+

When a user wants to view their messages, they connect to sx_<user>. and subscribe. +this can be done using sr_subscribe -n –topic_prefix=v02.log or the equivalent sr_log.

+
+
+
+

1.6 Security Model

+
+

1.6.1 Users, Queues & Exchanges

+
+
Each user Alice, on a broker to which she has access:
    +
  • has an exchange xs_Alice, where she writes her notification messages, and reads her logs from.

  • +
  • has an exchange xl_Alice, where she writes her log messages.

  • +
  • can create queues qs_Alice_.* to bind to exchanges.

  • +
+
+
Switches connect with one another using inter-exchange accounts.
    +
  • Alice can create and destroy her own queues, but no-one else’s.

  • +
  • Alice can only write to her xs_exchange,

  • +
  • Exchanges are managed by the administrator, and not any user.

  • +
  • Alice can only post data that she is publishing (it will refer back to her)

  • +
+
+
..NOTE::

tester ^q_tester.* ^q_tester.*|xs_tester ^q_tester.*|^xl_tester$ +leaving all permissions for queues for an amqp users also gives the permission +do create/configure/write any amqp objects with a name starting with q_tester +in this example.

+
+
+
+
+

1.6.2 Pre-Validation

+

Pre-Validation refers to security and correctness checks performed on +the information provided by the notification message before the data itself is downloaded. +Some tools may refer to this as message validation

+
+
    +
  • input sanitizing (looking for errors/malicious input.)

  • +
  • an undefined number of checks that need to be configurable (script?)

  • +
  • vary per configuration, and installation (sizes)

  • +
+
+
+
When reading from a source:
    +
  • a notification message arrives on xs_Alice, from a user logged in as Alice.

  • +
  • overwrites the source to be Alice: source=Alice … or reject?

  • +
  • sets some headers that we do not trust users to do: cluster=

  • +
  • set cluster header to local one.

  • +
+
+
Reading from a feeder:
    +
  • source doesn´t matter. (feeders can represent other users)

  • +
  • do not overwrite source.

  • +
  • ensure cluster is not local cluster (as that would be a lie.) ?

  • +
+
+
Regardless:
    +
  • check the partitioning size, if it exceeds pump maximum, Reject.

  • +
  • check the bandwidth limitations in place. If exceeded, Hold.

  • +
  • check the disk usage limit in place. If exceeded, Hold.

  • +
  • If the private flag is set, then accept by copying to xPrivate

  • +
  • If the private flag is not set, then accept by copy to xPublic

  • +
+
+
Results:
    +
  • Accept means: queue the message to another exchange (xinput) for downloading.

  • +
  • Reject means: do not copy message (still accept & ack so it leaves queue) product log message.

  • +
  • Hold means: do not consume… but sleep for a while.

  • +
+
+
+

Hold is for temporary failure type reasons, such as bandwidth of disk space reasons. +as these reasons are independent of the particular message, hold applies for +the entire queue, not just the message.

+

After Pre-Processing, a component like sr_sarra assumes the notification message is good, +and just processes it. That means it will fetch the data from the posting source. +Once the data is downloaded, it goes through Post-Validation.

+
+
+

1.6.3 Post-Validation

+

When a file is downloaded, before re-announcing it for later hops it goes +through some analysis. The tools may call this file validation:

+
+
    +
  • when a file is downloaded, it goes through post-validation,

  • +
  • invoke one or more virus scanners chosen by security

  • +
  • the scanners will not be the same everywhere, even different locations within +same org, may have different scanning standards (function on security zone.)

  • +
  • Accept means: it is OK to send this data to further hops in the network.

  • +
  • Reject menas: do not forward this data (potentially delete local copy.) Essentially quarantine

  • +
+
+
+
+

1.6.4 Log Validation

+

When a client like sarra or subscribe completes an operation, it creates a log message +corresponding to the result of the operation. (This is much lower granularity than a +local log files.) It is important for one client not to be able to impersonate another +in creating log messages.

+
+
    +
  • Messages in exchanges have no reliable means of determining who inserted them.

  • +
  • so users publish their log messages to sl_<user> exchange.

  • +
  • For each user, log reader reads the message, and overwrites the consuminguser to force match. (if reading a message from sl_Alice, it forces the consuminguser field to be Alice) see sr_log(7) for user field

  • +
  • sl_* are write-only for all users, they cannot read their own notification messages for that.

  • +
  • is there some check about consuminghost?

  • +
  • Accepting a log message means publishing on the xlog exchange.

  • +
  • Only admin functions can read from xlog.

  • +
  • downstream processing is from xlog exchange which is assumed clean.

  • +
  • Rejecting a log message means not copying it anywhere.

  • +
  • sourcce check does not make sense when channels are used for inter-pump log routing. +Essentially, all downstream pumps can do is forward to the source cluster. +The pumps receiving the log messages must not convert the consuminguser on those links. +evidence of need of some sort of setting: user vs. inter-pump setting.

  • +
+
+
+
… NOTE::

FIXME: if you reject a log message, does it generate a log message? +Denial of service potential by just generating infinite bogs log messages. +It is sad that if a connection is mis-configured as a user one, when it is inter-pump, +that will cause messages to be dropped. how to detect configuration error?

+
+
+
+
+

1.6.5 Private vs. Public Data Transfer

+

Transfers in the past have been public, just a matter of sharing public information. +A crucial requirement of the package is to support private data copies, where the +ends of the transfer are not sharing with arbitrary others.

+
+

Note

+

FIXME: This section is a half-baked idea! not sure how things will turn out. +basic problem: Alice connecting to S1 wants to share with Bob, who has an +account on S3. To get from S1 to S3, one needs to traverse S2. the normal +way such routing is done is via a sr_sarra subscription to xpublic on S1, and +S2. So Eve, a user on S1 or S2, can see the data, and presumably download it. +unless the http permissions are set to deny on S1 and S2. Eve should not have +access. Implement via http/auth permitting inter-pump accounts on S2 +to access S1/<private> and S3 account to S2/<private>. then permit bob on +S3.

+
+

There are two modes of sending products through a network, private vs. public. +With public sending, the information transmitted is assumed to be public and available +to all comers, If someone sees the data on an intervening pump, then they are likely +to be able to download it at will without further arrangements. public data is posted +for inter-pump copies using the xPublic exchange, which all users may access as well.

+

Private data is only made available to those who are explicitly permitted access. +private data is made available only on the xPrivate exchange. Only Interpump channel +users are given access to these messages.

+
+

Note

+
    +
  • Is two exchanges needed, or is setting permissions enough?

  • +
  • if nobody on B is permitted, then only C is able to download from B, which just works.

  • +
  • This only works with http because setting sftp permissions is going to be hell.

  • +
  • If only using http, then Even can still see all postings, just not get data, unless xprivate happens.

  • +
+
+

For SEP topologies (see Topologies) things are much simpler as end users can just use mode bits.

+
+
+

1.6.6 HTTPS Private Access

+
+

Note

+

FIXME: Not designed yet. +Really not baked yet. For https, need to create/manage .htaccess (canned but generated every day) +and .htpasswd (generated every day) files.

+
+

Need some kind of adm message that sources can send N pumps later to alter the contents of .htpasswd +CRUD? or just overwrite every time? query?

+

Sarra likely needs to look at this and add the ht* files every day. Need to talk with the webmailteam guys.

+

How to change passwords

+
+
+
+

1.7 Topologies

+

Questions… There are many choices for cluster layout. One can do simple H/A on a pair of nodes, +simple active/passive? One can go to scalable designs on an array of nodes, which requires a load +balancer ahead of the processing nodes. The disks of a cluster can be shared or individual to +the processing nodes, as can broker state. Exploring whether to support any/all configurations, +or to determine if there is a particular design pattern that can be applied generally.

+

To make these determinations, considerable exploration is needed.

+

We start with naming the topologies so they can be referred to easily in further discussions. +None of the topologies assume that disks are pumped among servers in the traditional HA style.

+

Based on experience, disk pumping is considered unreliable in practice, as it involves complex +interaction with many layers, including the application. Disks are either dedicated to nodes, +or a cluster file system is to be used. The application is expected to deal with those two +cases.

+

most of the cluster management is taken care of by the sr3_tools project:

+
+
+

A review of that project to manage deployments regardless of topology, would be helpful.

+

Some document short-hand:

+
+
Bunny

A shared/clustered broker instance, where multiple nodes use a common broker to co-ordinate.

+
+
Capybara Effect

capybara through a snake where a large rodent distorts the body of a snake +as it is being digested. Symbolic of poor load balancing, where one node +experiences a spike in load and slows down inordinately.

+
+
Fingerprint Winnowing

Each product has a checksum and size intended to identify it uniquely, referred to as +as fingerprint. If two products have the same fingerprint, they are considered +equivalent, and only one may be forwarded. In cases where multiple sources of equivalent +data are available but downstream consumers would prefer to receive single notification messages +of products, processes may elect to publish notifications of the first product +with a given fingerprint, and ignore subsequent ones.

+

This is the basis for the most robust strategy for high availability, but setting up +multiple sources for the same data, accepting notification messages for all of them, but only +forwarding one downstream. In normal operation, one source may be faster than the +other, and so the second source’s products are usually ‘winnowed’. When one source +disappears, the other source’s data is automatically selected, as the fingerprints +are now fresh and used, until a faster source becomes available.

+

The advantage of this method is that now A/B decision is required, so the time +to pumpover is zero. Other strategies are subject to considerable delays +in making the decision to pumpover, and pathologies one could summarize as flapping, +and/or deadlocks.

+
+
+
+

1.7.1 Standalone

+

In a standalone configuration, there is only one node in the configuration. I runs all components +and shares none with any other nodes. That means the Broker and data services such as sftp and +apache are on the one node.

+

One appropriate usage would be a small non-24x7 data acquisition setup, to take responsibility of data +queueing and transmission away from the instrument.

+
+
+

1.7.2 DDSR: Switching/Routing Configuration

+

This is a more scalable configuration involving several data mover nodes, and potentially several brokers. +These clusters are not destinations of data transfers, but intermediaries. Data flows through them, but +querying them is more complicated because no one node has all data available. The downstream clients +of DDSR’s are essentially other sarracenia instances.

+

There are still multiple options available within this configuration pattern. +ddsr one broker per node? (or just one broker ( clustered,logical ) broker?)

+

On a pumping/router, once delivery has occurred to all contexts, can you delete the file? +Just watch the log files and tick off as each scope confirms receipt. +when last one confirmed, delete. (makes re-xmit difficult ;-)

+

Based on a file size threshold? if the file is too big, don´t keep it around?

+

The intended purpose has a number of implementation options, which must be further sub-divided for analysis.

+
+
+

1.7.3 Independent DDSR

+

In Independent DDSR, there is a load balancer which distributes each incoming connection to +an individual broker running on a single node.

+

ddsr - broker

+

pre-fetch validation would happen on the broker. then re-post for the sarra’s on the movers.

+
+
    +
  • each node broker and transfer engines act independently. Highest robustness to failure.

  • +
  • load balancer removes mover nodes from operation on detection of a failure.

  • +
  • individual files land, mostly entirely on single nodes.

  • +
  • no single data mover sees all of the files of all of the users in a cluster.

  • +
+
+

CONFIRM: Processes running on the individual nodes, are subscribed to the local broker. +Highly susceptible to the Capybara Effect where all of the blocks of +the large file are channelled though a single processing node. Large file transfers +with trigger it.

+

CONFIRM: Maximum performance for a single transfer is limited to a single node.

+
+
+

1.7.4 Shared Broker DDSR

+

While the data nodes disk space remain independent, the brokers are clustered together to +form a single logical entity.

+

on all nodes, the mover processes use common exchanges and queues.

+
+
    +
  • each node transfers independently, but dependent on the broker cluster.

  • +
  • load balancer removes nodes (broker or mover) from operation.

  • +
  • external users connect to shared queues, not node specific ones.

  • +
  • transfer engines connect to cluster queues, obtaining blocks.

  • +
  • no single data mover sees all of the files of all of the users in a cluster.

  • +
  • requires broker to be clustered, adding complexity there.

  • +
+
+

In Shared Broker DDSR, Capybara Effect is minimized as individual blocks of a transfer +are distributed across all the mover nodes. When a large file arrives, all of the movers +on all of the nodes may pick up individual blocks, so the work automatically is +distributed across them.

+

This assumes that large files are segmented. As different transfer nodes will have +different blocks of a file, and the data view is not shared, no re-assembly of files +is done.

+

Broker clustering is considered mature technology, and therefore relatively trustworthy.

+
+
+

1.7.5 DD: Data Dissemination Configuration (AKA: Data Mart)

+

The sr3 deployment configuration is more of an end-point configuration. Each node is expected to +have a complete copy of all the data downloaded by all the nodes. Giving a unified view makes +it much more compatible with a variety of access methods, such as a file browser (over http, +or sftp) rather than being limited to AMQP notification messages. This is the type of view presented by +dd.weather.gc.ca.

+

Given this view, all files must be fully reassembled on receipt, prior to announcing downstream +availability. Files may have been fragmented for transfer across intervening pumps.

+

There are multiple options for achieving this end user visible effect, each with tradeoffs. +In all cases, there is a load balancer in front of the nodes which distributes incoming +connection requests to a node for processing.

+
+
    +
  • multiple server nodes. Each standalone.

  • +
  • sr3 - load balancer, just re-directs to a sr3 node? +dd1,dd2,

    +

    broker on sr3 node has connection thereafter.

    +
  • +
+
+
+
+

1.7.6 Independent DD

+
+
    +
  • The load balancer hands the incoming requests to multiple Standalone configurations.

  • +
  • Each node downloads all data. Disk space requirements for nodes in this configuration +are far larger than for DDSR nodes, where each node only has 1/n of the data.

  • +
  • Each node announces each product that it has downloaded, using it’s own node name, because +it does not know if other nodes have that product.

  • +
  • Once a connection is established, the client will communicate exclusively with that node. +ultimate performance is limited by the individual node performance.

  • +
  • The data movers can (for maximum reliability) be configured independently, but if inputs +are across the WAN, one can reduce bandwidth usage N times by havng N nodes +share queues for distant sources and then have local transfers between the nodes.

    +

    CONFIRM: is Fingerprint Winnowing required for intra-cluster copies?

    +

    When a single node fails, it ceases to download, and the other n-1 nodes continue transferring.

    +
  • +
+
+
+

Note

+

FIXME: shared broker and shared file system… hmm… Could use second broker +instance to do cooperating download via fingerpring winnowing.

+
+
+
+

1.7.7 Shared-Broker DD

+
+
    +
  • a single clustered broker is shared by all nodes.

  • +
  • Each node downloads all data. Disk space requirements for nodes in this configuration +are far larger than for DDSR nodes, where each node only has 1/n of the data.

  • +
  • clients connect to a cluster-wide broker instance, so the download links can be from any +node in the cluster.

  • +
  • if the clustered broker fails, the service is down. (should be reliable)

  • +
  • A node cannot announce each product that it has downloaded, using it’s own node name, because +it does not know if other nodes have that product. (announce as dd1 vs. dd)

  • +
  • Either:

    +
    +

    – Can only announce a product once it is clear that every active node has the product. +– 1st come, 1st serve: apply fingerprint winnowing. Announce only node that got the data first.

    +
    +
  • +
  • as in the independent configuration, nodes share queues and download a fraction upstream data. +They therefore need to exchange data amongst each other, but that means using a non-clustered +broker. So likely there will be two brokers access by the nodes, one node local, and one shared.

  • +
  • this is more complicated, but avoids the need for a clustered file system. hmm… pick your poison. +demo both?

  • +
+
+
+
+

1.7.8 Shared-Data DD

+
+
    +
  • The load balancer hands the incoming request to multiple nodes.

  • +
  • Each node has read/write access to a shared/cluster file system.

  • +
  • clustered broker configuration, all nodes see the same broker.

  • +
  • downloaded once means available everywhere (written to a shared disk)

  • +
  • so can advertise immediately with shared host spec (dd vs. dd1)

  • +
  • if the clustered broker fails, the service is down. (should be reliable)

  • +
  • if the clustered file system fails, the service is down. (??)

  • +
+
+
+
+

1.7.9 SEP: Shared End-Point Configuration

+

The SEP configuration, all of the mover nodes are directly accessible to users. +The broker does not provide data service, just a pure message broker. Can be called +data-less pump, or a bunny.

+

The broker is run clustered, and nothing can be said about the mover nodes. +Consumers and watchers can be started up by anyone on any collection of nodes, +and all data visible from any node where cluster file systems provide that benefit.

+

Disk space administration is entirely a user configuration setting, not in +control of the application (users set ordinary quotas for their file systems directly)

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Development.html b/Contribution/Development.html new file mode 100644 index 000000000..711f32498 --- /dev/null +++ b/Contribution/Development.html @@ -0,0 +1,1460 @@ + + + + + + + MetPX-Sarracenia Developer’s Guide — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

MetPX-Sarracenia Developer’s Guide

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+

Tools you Need

+

To hack on the Sarracenia source, you need:

+
    +
  • A linux development environment, either a workstation, or a VM of some kind. +setup using ubuntu is automated, but adjustment for other distros is possible. +command-line comfort a must.

  • +
  • python3. The application is developed in and depends on python versions >= 3.5.

  • +
  • style: PEP8 except max line length is 119, enforced via pycodestyle for VSCode, yapf3 or other similar tool.

  • +
  • an account on github.com will help in submitting patches for consideration.

  • +
+

Things that will be installed by automated setup:

+
    +
  • a bunch of other python modules indicated in the dependencies (setup.py or debian/control)

  • +
  • python3 pyftpdlib module, used to run an ftpserver on a high port during the flow test.

  • +
  • git. in order to download the source from the github repository, and to prepare and submit +changes.

  • +
  • a dedicated rabbitmq broker, with administrative access, to run the sr_insects tests. +this is installed by automated tools for setting up the linux environment. +The flow test creates and destroys exchanges and will disrupt any active flows on the broker.

  • +
+

after you have cloned the source code:

+
git clone -b development https://github.com/MetPX/sarracenia sr3
+git clone -b development https://github.com/MetPX/sarrac sr3c
+git clone https://github.com/MetPX/sr_insects insects
+cd sr3
+
+
+

The rest of the Guide takes the above for granted.

+
+
+

Documentation

+

Documentation Standards exist in /docs/Contribution/Documentation.rst +process for locally building the docs are there, as well as live web-site maintenance +methods.

+
+

Where to Put Options

+

Options are documented in sr3_options(7) dictionary style in alphabetic order. +Should it be worthwhile, examples of use could be added to other guides.

+
+
+
+

Development

+

In general, the development workflow is to get a laptop or a VM where one can run +the flow_tests (available from http://github.com/MetPX/sr_insects ) The first step +in configuring a development environment is ensuring that the sr_insects flow tests +work, as they function as a gate for commits to important branches.

+

Development is most commonly done on Ubuntu >=18.04 platform.

+
+

v2 Workflow

+

Finished development work for version 2 is committed to on the v2_dev branch, which is used +to produce daily snapshots. One should not normally commit changes to the v2_dev branch, +but rather merge them from a working branch.

+

Development branches are named after the issue they are meant to address “v2_issue365”, for +example. If there are multiple attempts to address a given issue, then use the issue +as a name prefix. For example, there could be issue365, but if we decide that isn’t +a good way to address the issue, there could be an issue365_methodB branch.

+

Before submitting a pull-request (PR), please ensure that the flow tests from +sr_insects have been run successfully: at least static_flow, flakey_broker, and dynamic_flow

+

When a PR is generated, the second developer can look it over for concerns. +Once satisfied with the nature of the patch, the second developer should pull the branch +and run the flow tests again (the same three) to confirm. Only after the flow tests +have been run on multiple machines should a change be merged to stable.

+

issues unique to v2 should be tagged v2only. +on Launchpad.net:

+
+
    +
  • daily repository packages of v2 will be build from v2_dev

  • +
  • pre-release repository packages of v2 will be build from v2_dev

  • +
  • release repository packages are generated from v2_stable.

  • +
+
+
+
+

v3 Workflow

+

The upcoming version of Sarracenia is developed in the development (work in progress) branch. +As the major refactor is substantially complete, the remaining work is now entirely constructive +and all development is co-ordinated through issues exactly as v2 is. Issues unique to v3, be +they regressions or enhancements that don’t make sense to add to v2, have the tag v3only. +Issues that are common between the releases are tagged v3.

+

The workflow with v3 is similar to v2 but with different branches. branches are assumed +to be branched from the development branch, so v3 is assumed unless v2_ is present. +Having all the flow tests complete fairly successfully +is one criterion for acceptance into development.

+

To run the sr_insects tests, the repository must be cloned with the development branch. +A gate for merging to development is for a second developer to run the flow_tests. +For v03, these tests must run: static_flow, flakey_broker, dynamic_flow, transform_flow

+
+
    +
  • launchpad has recipes to produce metpx-sr3 packages from various branches.

  • +
  • The MetPX Daily repository is a snapshot of the development branch.

  • +
  • The MetPX Pre-Release repository should receive versions ending in rcX (release candidate) +The packages here from from pre-release branch which comes from snapshots of the development branch. +There is also a pre-release-py36 branch for building pre-release packages for older operating systems.

  • +
  • stable comes from on snapshots of (version 3) pre-release branch.

  • +
  • The MetPX repository should only contain stable releases that have graduated from the rcX series. +there is a stable_py36 branch to build packages for older operating systems that have +python 3.6 (redhat 8, ubuntu 18, ubuntu 20) or are too old to use hatchling installer.

  • +
+
+
+
+

sr_insects

+

The sr_insects repository has it’s own issues DB, and work on sr_insects is encouraged. +Both v2 and v3 are supported on the stable branch of sr_insects. That branch should be +used to support all development in both versions….

+
+
+
+

Local Installation

+

There are many different ways to install python packages on a computer. Different developers +will prefer different methods, and all the methods need to be tested prior to each release. +Sarracenia can work with either mqtt or amqp (most mature and stable) message passing libraries. +Install one of those first. in these examples, we use amqp.

+
    +
  • Wheel when people are running different operating systems (non-ubuntu, non-debian) people will be installing wheels, typically that have been uploaded to pypi.python.org. On the other hand, it is a bit of a pain/noise to upload every development version, so we only upload releases, so testing of wheels is done by building local wheels. Need to build a new wheel every time a change is made.

  • +
  • pip install metpx-sr3[amqp] would pull a wheel down from pypi.python.org. Generally not used during development of Sarracenia itself. +one could also pull in all possible dependencies with pip install metpx-sr3[all]

  • +
  • pip install -e .[amqp] … lets you edit the source code of the installed package, ideal for debugging problems, because it allows live changes to the application without having to go through building and installing a new package.

  • +
  • apt install metpx-sr3 install debian package from repositories, similarly to pip install (not -e), normally dev snapshots are not uploaded to repositories, so while this would be the normal way for users of ubuntu servers, it is not available during development of the package itself. Also need apt install python3-amqp

  • +
  • dpkg -i builds a debian package for local installation. This is how packages are tested prior to upload to repositories. It can also be used to support development (have to run dpkg -i for each package change.) also need apt install python3-amqp

  • +
+

The sr_insects tests invokes the version of metpx-sarracenia that is installed on the system, +and not what is in the development tree. It is necessary to install the package on +the system in order to have it run the sr_insects tests.

+
+

Prepare a Vanilla VM

+

This section describes creating a test environment for use in a virtual machine. One way to build +a virtual machine is to use multipass (https://multipass.run) Assuming it is installed, one can +create a vm with:

+
multipass launch -m 8G -d 30G --name flow
+
+
+

need to have ssh localhost work in the multipass container. Can do that by copying multipass +private key into the container:

+
fractal% multipass list
+Name                    State             IPv4             Image
+primary                 Stopped           --               Ubuntu 20.04 LTS
+flow                    Running           10.23.119.56     Ubuntu 20.04 LTS
+keen-crow               Running           10.23.119.5      Ubuntu 20.04 LTS
+fractal%
+
+
+

Weird issues with ssh keys not being interpreted properly by paramiko, work around +( https://stackoverflow.com/questions/54612609/paramiko-not-a-valid-rsa-private-key-file )

+
fractal% sudo cat /var/snap/multipass/common/data/multipassd/ssh-keys/id_rsa | sed 's/BEGIN .*PRIVATE/BEGIN RSA PRIVATE/;s/END .*PRIVATE/END RSA PRIVATE/' >id_rsa_container
+chmod 600 id_rsa_container
+scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.175:/home/ubuntu/.ssh/id_rsa
+                                                                  100% 1704     2.7MB/s   00:00
+
+fractal% scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.106:/home/ubuntu/.ssh/id_rsa
+The authenticity of host '10.23.119.106 (10.23.119.106)' can't be established.
+ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8.
+Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
+Warning: Permanently added '10.23.119.106' (ECDSA) to the list of known hosts.
+id_rsa_container                                                                                                                         100% 1712     9.4MB/s   00:00
+fractal% multipass shell flow
+Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)
+
+ * Documentation:  https://help.ubuntu.com
+ * Management:     https://landscape.canonical.com
+ * Support:        https://ubuntu.com/advantage
+
+  System information as of Fri Aug 27 21:12:16 EDT 2021
+
+  System load:  0.42              Processes:             112
+  Usage of /:   4.4% of 28.90GB   Users logged in:       0
+  Memory usage: 5%                IPv4 address for ens4: 10.23.119.106
+  Swap usage:   0%
+
+
+1 update can be applied immediately.
+To see these additional updates run: apt list --upgradable
+
+
+To run a command as administrator (user "root"), use "sudo <command>".
+See "man sudo_root" for details.
+
+ubuntu@flow:~$
+
+
+

then prompt ssh to accept the localhost key:

+
ubuntu@flow:~$ ssh localhost ls -a
+The authenticity of host 'localhost (127.0.0.1)' can't be established.
+ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8.
+Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
+Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
+.
+..
+.bash_logout
+.bashrc
+.cache
+.profile
+.ssh
+ubuntu@flow:~$
+
+
+

This will provide a shell in an initialized VM. To configure it:

+
git clone -b development https://github.com/MetPX/sarracenia sr3
+cd sr3
+
+
+

There are scripts that automate the installation of necessary environment to be able to run tests:

+
travis/flow_autoconfig.sh
+travis/add_sr3.sh
+
+
+

You should be able to see an empty configuration:

+
sr3 status
+
+
+

sr3c and sr3 are now installed, and should be ready to run a flow test from the sr_insects module, which +has also been cloned:

+
+

cd ../sr_insects

+
+

The v03 branch of sr_insects supports testing of both v2 and v3, and both versions are now installed. +The flow tests are intended to be run to confirm compatibility between v2 and v3, and so one +must be able to test v2 as well:

+
ubuntu@flow:~/sr_insects$ dpkg -l | grep metpx
+ii  metpx-libsr3c                    3.21.08a1-0~202108270410~ubuntu20.04.1 amd64        C-Implementation of a Sarracenia Client
+ii  metpx-sarracenia                 2.21.08-0~202108241854~ubuntu20.04.1   all          Directory mirroring in real-time for users, file servers and web sites.
+ii  metpx-sr3                        3.00.008exp                            all          v3 Directory mirroring in real-time for users, file servers and web sites.
+ii  metpx-sr3c                       3.21.08a1-0~202108270410~ubuntu20.04.1 amd64        C-Implementation of a Sarracenia Client
+ubuntu@flow:~/sr_insects$
+
+
+

The v2 package is metpx-sarracenia, whereas the v3 one is metpx-sr3. the flow tests will detect +which version is installed and test v3 if both are present. To override that:

+
ubuntu@flow:~/sr_insects$ export sarra_py_version=2.21.08
+ubuntu@flow:~/sr_insects$
+
+
+

Then one can run flow_tests from this shell normally.

+
+
+

Ubuntu 18.04

+

A number of systems run Ubuntu 18.04 even though it is pretty old.

+

‘’’

+

multipass launch -m 8G bionic

+

‘’’

+

can run developer tests as per multipass as described above.

+
+
+

Python Wheel

+

If you have not used add_sr3.sh (which builds a debian package), then one can use this procedure +for local installation on a computer with a python wheel for testing and development:

+
python3 setup.py bdist_wheel
+
+
+

or… on newer systems, using build instead:

+
python3 -m build --no-isolation
+
+
+

Should build a wheel in the dist sub-directory. +then as root install that new package:

+
pip3 install --upgrade ...<path>/dist/metpx*.whl
+
+
+
+
+

Local Pip install

+

For local installation on a computer, using a pip +For testing and development:

+
pip3 install -e .
+export PATH=${HOME}/.local/bin:${PATH}
+
+
+

Using the local python package installer (PIP) to create a locally editable version. +The above will install the package in ~/.local/bin… so need to ensure the path includes +that directory.

+
+
+

Debian/Ubuntu

+

For local installation on a computer, using a debian package. +This process builds a local .deb in the parent directory using standard debian mechanisms. +- Check the build-depends line in debian/control for dependencies that might be needed to build from source. +- The following steps will build sarracenia but not sign the changes or the source package:

+
cd metpx/sarracenia
+sudo apt-get install devscripts
+debuild -uc -us
+sudo dpkg -i ../<the package just built>
+
+
+

which accomplishes the same thing using debian packaging. +The options are detailed below:

+
+
+

Committing Code

+

What should be done prior to committing to the development branch? +Checklist:

+
    +
  • do development on some other branch. Usually the branch will be named after the issue being +addressed. Example: issue240, if we give up on an initial approach and start another one, +there may be issue240_2 for a second attempt. There may also be feature branches, such as v03.

  • +
  • sr_insects tests works (See Testing) The development branch should always be functional, do not commit code if the sr_insects tests are not working.

  • +
  • Natural consequence: if the code changes means tests need to change, include the test change in the commit.

  • +
  • update doc/ manual pages should get their updates ideally at the same time as the code.

  • +
+

Usually there will be many such cycles on an issueXXX branch before one is ready +to issue a pull request. Eventually, we get to Commits to the Development Branch

+
+
+
+

sr_insects Tests Description

+

Before committing code to the stable branch, as a Quality Assurance measure, one should run +all available self-tests. It is assumed that the specific changes in the code have already been unit +tested. Please add self-tests as appropriate to this process to reflect the new ones. +Generally speaking one should solve problems at the first test that fails as each test +is more complicated than the previous one.

+

There is a separate git repository containing the more complex tests https://github.com/MetPX/sr_insects

+

A typical development workflow will be (Do not try this, this is just an overview of the steps that will be +explained in detail in following sections):

+
git branch issueXXX
+git checkout issueXXX
+cd sarra ; *make coding changes*
+cd ..
+debuild -uc -us
+cd ../sarrac
+debuild -uc -us
+sudo dpkg -i ../*.deb
+cd ..
+
+git clone -b development https://github.com/MetPX/sr_insects
+cd sr_insects
+sr3 status  # make sure there are no components configured before you start.
+            # test results will likely be skewed otherwise.
+for test in unit static_flow flakey_browser transform_flow dynamic_flow; do
+   cd $test
+   ./flow_setup.sh  # *starts the flows*
+   ./flow_limit.sh  # *stops the flows after some period (default: 1000) *
+   ./flow_check.sh  # *checks the flows*
+   ./flow_cleanup.sh  # *cleans up the flows*
+   cd ..
+done
+
+#assuming all the tests pass.
+git commit -a  # on the branch...
+
+
+
+

Unit

+

The unit test in sr_insects is the shortest one taking a minute or so, and not requiring +much configuration at all. They are sanity tests of code behaviour. Generally takes a minute +or two on a laptop.

+
+
+

Static Flow

+

The static_flow tests are a bit more complicated, testing more components, using single +threaded components in a linear way (all data moves uniformly forward.) It should be +more straight-forward to identify issues as there is no deletion and so it lends itself well +to repeating subset tests to identify individual issues. It takes about two minutes on a laptop.

+
+
+

Flakey Broker

+

The flakey_broker tests are the same as the static_flow, but slowed down so that they last +a few minutes, and the broker is shutdown and restarted while the posting is happenning. +Note that post_log prints before a notification message is posted (because post_log is an on_post plugin, and +that action, allows one to modify the notification message, so it needs to be before the post actually happens.)

+
+
+

Dynamic Flow

+

The dynamic_flow test add advanced features: multi-instances, the winnow component, retry logic testing, +and includes file removals as well. Most of the documentation here refers to runnig the +dynamic_flow test, as it is the most complicated one, and the ancestor of the others. The unit +test was separated out from the beginnig of the dynamic_flow test, and the static_flow is +a simplified version of the original flow test as well.

+

Generally speaking, one should run the tests in sequence and ensure the results of earlier +tests are good before proceeding to the next test.

+

Note that the development system must be configured for the sr_insects tests to run successfully. See the next +section for configuration instructions. For development with a fresh OS installation, +the configuration steps have been automated and can be applied with the flow_autoconfig.sh +script in sr_insects (https://github.com/MetPX/sr_insects/blob/stable/flow_autoconfig.sh). Blind +execution of this script on a working system may lead to undesirable side effects; you have been warned!

+

The configuration one is trying to replicate:

+../_images/Flow_test.svg

Following table describes what each element of the dynamic flow test does, and the test coverage +shows functionality covered.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Configuration

Does

Test Coverage

subscribe t_ddx

copy from data mart to local broker +posting notification messages to +local xwinno00 and xwinnow01 +exchanges.

read amqps public data mart (v02) +as ordinary user.

+

shared queue and multiple processes +3 instances download from each q

+

post amqp to a local exchange (v02) +as feeder(admin) user

+

post_exchangeSplit to xwinnow0x

+

winnow t0x_f10

winnow processing publish for xsarra +exchange for downloading.

+

as two sources identical, only half +notification messages are posted to +next

+

read local amqp v02 +as feeder user.

+

complete caching (winnow) function

+

post amqp v02 to local excchange.

+

sarra download +f20

download the winnowed data from the +data mart to a local directory +(TESTDOCROOT= ~/sarra_devdocroot)

+

add a header at application layer +longer than 255 characters.

+

read local amqp v02 (xsarra)

+

download using built-in python

+

shared queue and multiple processes +5 instances download from each q

+

download using accel_wget plugin

+

AMQP header truncation on publish.

+

post amqp v02 to xpublic +as feeder user +as http downloads from localhost

+

subscribe t

download as client from localhost +to downloaded_by_sub_t directory.

read amqp from local broker +as ordinary user/client.

+

shared queue and multiple processes +5 instances download from each q

+

watch f40

watch downloaded_by_sub_t +(post each file that appears there.)

+

memory ceiling set low

+

client v03 post of local file. +(file: url)

+

auto restarting on memory ceiling.

+

sender +tsource2send

read local file, send via sftp +to sent_by_tsource2send directory

+

post to xs_tsource_output

+

client consume v03 notification +messages. +consumer read local file.

+

send via sftp.

+

plugin replace_dir

+

posting sftp url. +post v02 (converting v03 back.)

+

test post_exchangeSuffix option.

+

subscribe +u_sftp_f60

download via sftp from localhost +putting files in downloaded_by_sub_u +directory.

client sftp download.

+

accel_sftp plugin.

+

post test2_f61

post files in sent_by_tsource2send +with ftp URL’s in the +xs_tsource_poll exchange

+

(wrapper script calls post)

+

explicit file posting

+

ftp URL posting.

+

post_exchangeSuffix option

+

poll f62

poll sent_by_tsource2send directory +posting sftp download URL’s

polling

+

post_exchangeSuffix option

+

subscribe ftp_f70

subscribe to test2_f61 ftp’ posts. +download files from localhost +to downloaded_by_sub_u directory.

ftp url downloading.

subscribe q_f71

subscribe to poll, downloading +to recd_by_srpoll_test1

confirming poll post quality.

shovel pclean f90

clean up files so they don’t +accumulate +fakes failures to exercise retries

shovel function.

+

retry logic.

+

shovel pclean f91

clean up files so they don’t +accumulate

shovel with posting v03

+

retry logic.

+

shovel pclean f92

clean up files so they don’t +accumulate

shovel with consuming v03

+

posting v02.

+

retry logic.

+
+

Assumption: test environment is a Linux PC, either a laptop/desktop, or a server on which one +can start a browser. If working with the C implementation as well, there are also the following +flows defined:

+../_images/cFlow_test.svg
+
+
+

Running Flow Tests

+

This section documents these steps in much more detail. +Before one can run the sr_insects tests, some pre-requisites must be taken care of. +Note that there is Github Actions integration for at least the development branch +to verify functionality on a variety of python version. Consult:

+
https://github.com/MetPX/sarracenia/actions
+
+
+
+

Note

+

for the latest test results. Note that the results include dozens of tests, and are +a bit unreliable, typically it may take a few retries for it to work completely +(3 or 4 fail after initial attempt, then re-run the failed ones, and then +perhaps 1 or two will be left, and on the third pass the last one passes.)

+
+
+

Install Servers on Workstation

+

To prepare a computer to run the flow test, one must install some server +software and configurations. This same work is done by travis/flow_autoconfig.sh +which is run in Prepare a Vanilla VM but if you need to configure it +manually, below is the process.

+

Install a minimal localhost broker and configure rabbitmq test users:

+
sudo apt-get install rabbitmq-server
+sudo rabbitmq-plugins enable rabbitmq_management
+
+mkdir ~/.config/sarra
+cat > ~/.config/sarra/default.conf << EOF
+declare env FLOWBROKER=localhost
+declare env MQP=amqp
+declare env SFTPUSER=${USER}
+declare env TESTDOCROOT=${HOME}/sarra_devdocroot
+declare env SR_CONFIG_EXAMPLES=${HOME}/git/sarracenia/sarra/examples
+EOF
+
+RABBITMQ_PASS=S0M3R4nD0MP4sS
+cat > ~/.config/sarra/credentials.conf << EOF
+amqp://bunnymaster:${RABBITMQ_PASS}@localhost/
+amqp://tsource:${RABBITMQ_PASS}@localhost/
+amqp://tsub:${RABBITMQ_PASS}@localhost/
+amqp://tfeed:${RABBITMQ_PASS}@localhost/
+amqp://anonymous:${RABBITMQ_PASS}@localhost/
+amqps://anonymous:anonymous@hpfx.collab.science.gc.ca
+amqps://anonymous:anonymous@hpfx1.collab.science.gc.ca
+amqps://anonymous:anonymous@hpfx2.collab.science.gc.ca
+amqps://anonymous:anonymous@dd.weather.gc.ca
+amqps://anonymous:anonymous@dd1.weather.gc.ca
+amqps://anonymous:anonymous@dd2.weather.gc.ca
+ftp://anonymous:anonymous@localhost:2121/
+EOF
+
+cat > ~/.config/sarra/admin.conf << EOF
+cluster localhost
+admin amqp://bunnymaster@localhost/
+feeder amqp://tfeed@localhost/
+declare source tsource
+declare subscriber tsub
+declare subscriber anonymous
+EOF
+
+sudo rabbitmqctl delete_user guest
+
+sudo rabbitmqctl add_user bunnymaster ${RABBITMQ_PASS}
+sudo rabbitmqctl set_permissions bunnymaster ".*" ".*" ".*"
+sudo rabbitmqctl set_user_tags bunnymaster administrator
+
+sudo systemctl restart rabbitmq-server
+cd /usr/local/bin
+sudo mv rabbitmqadmin rabbitmqadmin.1
+sudo wget http://localhost:15672/cli/rabbitmqadmin
+sudo chmod 755 rabbitmqadmin
+
+sr3 --users declare
+
+
+
+

Note

+

Please use other passwords in credentials for your configuration, just in case. +Passwords are not to be hard coded in self test suite. +The users bunnymaster, tsource, tsub, and tfeed are to be used for running tests.

+

The idea here is to use tsource, tsub, and tfeed as broker accounts for all +self-test operations, and store the credentials in the normal credentials.conf file. +No passwords or key files should be stored in the source tree, as part of a self-test +suite.

+
+
+
+

Setup Flow Test Environment

+

Once the server environment is established, the flow tests use sftp transfers to localhost.

+

It is also required that passwordless ssh access is configured on the test host +for the system user that will run the flow test. This can be done by creating +a private/public ssh key pair for the user (if there isn’t one already) and copying +the public key to the authorized_keys file in the same directory as the keys (~/.ssh). +For associated commands, see http://www.linuxproblem.org/art_9.html

+

Note that on systems where older versions of Paramiko (< 2.7.2) are installed, and the ssh key pair was generated with OpenSSH >= 6.5, manually testing the below command will work, but Paramiko will not be able to connect. This is likely the case if the ~/.ssh/id_rsa file contains BEGIN OPENSSH PRIVATE KEY. To work around this, convert the private key’s format using ssh-keygen -p -m PEM -f ~/.ssh/id_rsa.

+

To confirm that that passwordless ssh to localhost works:

+
ssh localhost ls
+
+
+

This should run and complete. If it prompts for a password, the flow tests will not work.

+

Check that the broker is working:

+
systemctl status rabbitmq-server
+
+
+

One part of the flow test runs an sftp server, and uses sftp client functions. +Need the following package for that:

+
sudo apt-get install python3-pyftpdlib python3-paramiko
+
+
+

The setup script starts a trivial web server, and ftp server, and a daemon that invokes sr_post. +It also tests the C components, which need to have been already installed as well +and defines some fixed test clients that will be used during self-tests:

+
cd
+git clone https://github.com/MetPX/sr_insects
+cd sr_insects
+cd static_flow
+. ./flow_setup.sh
+
+blacklab% ./flow_setup.sh
+cleaning logs, just in case
+rm: cannot remove '/home/peter/.cache/sarra/log/*': No such file or directory
+Adding flow test configurations...
+2018-02-10 14:22:58,944 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/cno_trouble_f00.inc to /home/peter/.config/sarra/cpump/cno_trouble_f00.inc.
+2018-02-10 09:22:59,204 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/no_trouble_f00.inc to /home/peter/.config/sarra/shovel/no_trouble_f00.inc
+2018-02-10 14:22:59,206 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpost/veille_f34.conf to /home/peter/.config/sarra/cpost/veille_f34.conf.
+2018-02-10 14:22:59,207 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd1_f04.conf to /home/peter/.config/sarra/cpump/pelle_dd1_f04.conf.
+2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd2_f05.conf to /home/peter/.config/sarra/cpump/pelle_dd2_f05.conf.
+2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f14.conf to /home/peter/.config/sarra/cpump/xvan_f14.conf.
+2018-02-10 14:22:59,209 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f15.conf to /home/peter/.config/sarra/cpump/xvan_f15.conf.
+2018-02-10 09:22:59,483 [INFO] copying /home/peter/src/sarracenia/sarra/examples/poll/f62.conf to /home/peter/.config/sarra/poll/f62.conf
+2018-02-10 09:22:59,756 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/shim_f63.conf to /home/peter/.config/sarra/post/shim_f63.conf
+2018-02-10 09:23:00,030 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/test2_f61.conf to /home/peter/.config/sarra/post/test2_f61.conf
+2018-02-10 09:23:00,299 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/tsarra_f20.conf to /home/peter/.config/sarra/report/tsarra_f20.conf
+2018-02-10 09:23:00,561 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow00_f10.conf to /home/peter/.config/sarra/report/twinnow00_f10.conf
+2018-02-10 09:23:00,824 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow01_f10.conf to /home/peter/.config/sarra/report/twinnow01_f10.conf
+2018-02-10 09:23:01,086 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sarra/download_f20.conf to /home/peter/.config/sarra/sarra/download_f20.conf
+2018-02-10 09:23:01,350 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sender/tsource2send_f50.conf to /home/peter/.config/sarra/sender/tsource2send_f50.conf
+2018-02-10 09:23:01,615 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd1_f00.conf to /home/peter/.config/sarra/shovel/t_dd1_f00.conf
+2018-02-10 09:23:01,877 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd2_f00.conf to /home/peter/.config/sarra/shovel/t_dd2_f00.conf
+2018-02-10 09:23:02,137 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cclean_f91.conf to /home/peter/.config/sarra/subscribe/cclean_f91.conf
+2018-02-10 09:23:02,400 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cdnld_f21.conf to /home/peter/.config/sarra/subscribe/cdnld_f21.conf
+2018-02-10 09:23:02,658 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cfile_f44.conf to /home/peter/.config/sarra/subscribe/cfile_f44.conf
+2018-02-10 09:23:02,921 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/clean_f90.conf to /home/peter/.config/sarra/subscribe/clean_f90.conf
+2018-02-10 09:23:03,185 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cp_f61.conf to /home/peter/.config/sarra/subscribe/cp_f61.conf
+2018-02-10 09:23:03,455 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/ftp_f70.conf to /home/peter/.config/sarra/subscribe/ftp_f70.conf
+2018-02-10 09:23:03,715 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/q_f71.conf to /home/peter/.config/sarra/subscribe/q_f71.conf
+2018-02-10 09:23:03,978 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/t_f30.conf to /home/peter/.config/sarra/subscribe/t_f30.conf
+2018-02-10 09:23:04,237 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/u_sftp_f60.conf to /home/peter/.config/sarra/subscribe/u_sftp_f60.conf
+2018-02-10 09:23:04,504 [INFO] copying /home/peter/src/sarracenia/sarra/examples/watch/f40.conf to /home/peter/.config/sarra/watch/f40.conf
+2018-02-10 09:23:04,764 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t00_f10.conf to /home/peter/.config/sarra/winnow/t00_f10.conf
+2018-02-10 09:23:05,027 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t01_f10.conf to /home/peter/.config/sarra/winnow/t01_f10.conf
+Initializing with sr_audit... takes a minute or two
+OK, as expected 18 queues existing after 1st audit
+OK, as expected 31 exchanges for flow test created.
+Starting trivial http server on: /home/peter/sarra_devdocroot, saving pid in .httpserverpid
+Starting trivial ftp server on: /home/peter/sarra_devdocroot, saving pid in .ftpserverpid
+running self test ... takes a minute or two
+sr_util.py TEST PASSED
+sr_credentials.py TEST PASSED
+sr_config.py TEST PASSED
+sr_cache.py TEST PASSED
+sr_retry.py TEST PASSED
+sr_consumer.py TEST PASSED
+sr_http.py TEST PASSED
+sftp testing start...
+sftp testing config read...
+sftp testing fake message built ...
+sftp sr_ftp instantiated ...
+sftp sr_ftp connected ...
+sftp sr_ftp mkdir ...
+test 01: directory creation succeeded
+test 02: file upload succeeded
+test 03: file rename succeeded
+test 04: getting a part succeeded
+test 05: download succeeded
+test 06: onfly_checksum succeeded
+Sent: bbb  into tztz/ddd 0-5
+test 07: download succeeded
+test 08: delete succeeded
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+/home/peter
+/home/peter
+test 09: bad part succeeded
+sr_sftp.py TEST PASSED
+sr_instances.py TEST PASSED
+OK, as expected 9 tests passed
+Starting flow_post on: /home/peter/sarra_devdocroot, saving pid in .flowpostpid
+Starting up all components (sr start)...
+done.
+OK: sr3 start was successful
+Overall PASSED 4/4 checks passed!
+blacklab%
+
+
+

As it runs the setup, it also executes all existing unit_tests. +Only proceed to the flow_check tests if all the tests in flow_setup.sh pass.

+
+
+

Run A Flow Test

+

The flow_check.sh script reads the log files of all the components started, and compares the number +of notification messages, looking for a correspondence within +- 10% It takes a few minutes for the +configuration to run before there is enough data to do the proper measurements:

+
./flow_limit.sh
+
+
+

sample output:

+
initial sample building sample size 8 need at least 1000
+sample now   1021
+Sufficient!
+stopping shovels and waiting...
+2017-10-28 00:37:02,422 [INFO] sr_shovel t_dd1_f00 0001 stopping
+2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: instances option not implemented, ignored.
+info: instances option not implemented, ignored.
+2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: report option not implemented, ignored.
+info: report option not implemented, ignored.
+2017-10-28 00:37:02,436 [INFO] sr_shovel t_dd2_f00 0001 stopping
+running instance for config pelle_dd1_f04 (pid 15872) stopped.
+running instance for config pelle_dd2_f05 (pid 15847) stopped.
+    maximum of the shovels is: 1022
+
+
+

Then check show it went with flow_check.sh:

+
TYPE OF ERRORS IN LOG :
+
+  1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/'
+  1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/'
+
+
+test  1 success: shovels t_dd1_f00 ( 1022 ) and t_dd2_f00 ( 1022 ) should have about the same number of items read
+test  2 success: sarra tsarra (1022) should be reading about half as many items as (both) winnows (2240)
+test  3 success: tsarra (1022) and sub t_f30 (1022) should have about the same number of items
+test  4 success: max shovel (1022) and subscriber t_f30 (1022) should have about the same number of items
+test  5 success: count of truncated headers (1022) and subscribed messages (1022) should have about the same number of items
+test  6 success: count of downloads by subscribe t_f30 (1022) and messages received (1022) should be about the same
+test  7 success: downloads by subscribe t_f30 (1022) and files posted by watch (1022) should be about the same
+test  8 success: posted by watch(1022) and sent by sr_sender (1022) should be about the same
+test  9 success: 1022 of 1022: files sent with identical content to those downloaded by subscribe
+test 10 success: 1022 of 1022: poll test1_f62 and subscribe q_f71 run together. Should have equal results.
+test 11 success: post test2_f61 1022 and subscribe r_ftp_f70 1021 run together. Should be about the same.
+test 12 success: cpump both pelles (c shovel) should receive about the same number of messages (3665) (3662)
+test 13 success: cdnld_f21 subscribe downloaded (1022) the same number of files that was published by both van_14 and van_15 (1022)
+test 14 success: veille_f34 should post the same number of files (1022) that subscribe cdnld_f21 downloaded (1022)
+test 15 success: veille_f34 should post the same number of files (1022) that subscribe cfile_f44 downloaded (1022)
+test 16 success: Overall 15 of 15 passed!
+
+blacklab%
+
+
+

If the flow_check.sh passes, then one has a reasonable confidence in the overall functionality of the +python application, but the test coverage is not exhaustive. This is the lowest gate for committing +changes to thy python code into the development branch. It is more qualitative sampling of the most +common use cases rather than a thorough examination of all functionality. While not +thorough, it is good to know the flows are working.

+

Note that the fclean subscriber looks at files in and keeps files around long enough for them to go through all the other +tests. It does this by waiting a reasonable amount of time (45 seconds, the last time checked.) then it compares the file +that have been posted by watch to the files created by downloading from it. As the sample now count proceeds, +it prints “OK” if the files downloaded are identical to the ones posted by sr_watch. The addition of fclean and +the corresponding cfclean for the cflow_test, are broken. The default setup which uses fclean and cfclean ensures +that only a few minutes worth of disk space is used at a given time, and allows for much longer tests.

+

By default, the flow_test is only 1000 files, but one can ask it to run longer, like so:

+
./flow_limit.sh 50000
+
+
+

To accumulate fifty thousand files before ending the test. This allows testing of long term performance, especially +memory usage over time, and the housekeeping functions of on_heartbeat processing.

+
+
+

Flow Cleanup

+

When done testing, run the ./flow_cleanup.sh script, which will kill the +running servers and daemons, and delete all configuration files installed for +the flow test, all queues, exchanges, and logs. This also needs to be done +between each run of the flow test:

+
blacklab% ./flow_cleanup.sh
+Stopping sr...
+Cleanup sr...
+Cleanup trivial http server...
+web server stopped.
+if other web servers with lost pid kill them
+Cleanup trivial ftp server...
+ftp server stopped.
+if other ftp servers with lost pid kill them
+Cleanup flow poster...
+flow poster stopped.
+if other flow_post.sh with lost pid kill them
+Deleting queues:
+Deleting exchanges...
+Removing flow configs...
+2018-02-10 14:17:34,150 [INFO] info: instances option not implemented, ignored.
+2018-02-10 14:17:34,150 [INFO] info: report option not implemented, ignored.
+2018-02-10 14:17:34,353 [INFO] info: instances option not implemented, ignored.
+2018-02-10 14:17:34,353 [INFO] info: report option not implemented, ignored.
+2018-02-10 09:17:34,837 [INFO] sr_poll f62 cleanup
+2018-02-10 09:17:34,845 [INFO] deleting exchange xs_tsource_poll (tsource@localhost)
+2018-02-10 09:17:35,115 [INFO] sr3_post shim_f63 cleanup
+2018-02-10 09:17:35,122 [INFO] deleting exchange xs_tsource_shim (tsource@localhost)
+2018-02-10 09:17:35,394 [INFO] sr3_post test2_f61 cleanup
+2018-02-10 09:17:35,402 [INFO] deleting exchange xs_tsource_post (tsource@localhost)
+2018-02-10 09:17:35,659 [INFO] sr_report tsarra_f20 cleanup
+2018-02-10 09:17:35,659 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:35,661 [INFO] deleting queue q_tfeed.sr_report.tsarra_f20.89336558.04455188 (tfeed@localhost)
+2018-02-10 09:17:35,920 [INFO] sr_report twinnow00_f10 cleanup
+2018-02-10 09:17:35,920 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:35,922 [INFO] deleting queue q_tfeed.sr_report.twinnow00_f10.35552245.50856337 (tfeed@localhost)
+2018-02-10 09:17:36,179 [INFO] sr_report twinnow01_f10 cleanup
+2018-02-10 09:17:36,180 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:36,182 [INFO] deleting queue q_tfeed.sr_report.twinnow01_f10.48262886.11567358 (tfeed@localhost)
+2018-02-10 09:17:36,445 [WARNING] option url deprecated please use post_base_url
+2018-02-10 09:17:36,446 [WARNING] use post_base_dir instead of document_root
+2018-02-10 09:17:36,446 [INFO] sr_sarra download_f20 cleanup
+2018-02-10 09:17:36,446 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:36,448 [INFO] deleting queue q_tfeed.sr_sarra.download_f20 (tfeed@localhost)
+2018-02-10 09:17:36,449 [INFO] exchange xpublic remains
+2018-02-10 09:17:36,703 [INFO] sr_sender tsource2send_f50 cleanup
+2018-02-10 09:17:36,703 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:36,705 [INFO] deleting queue q_tsource.sr_sender.tsource2send_f50 (tsource@localhost)
+2018-02-10 09:17:36,711 [INFO] deleting exchange xs_tsource_output (tsource@localhost)
+2018-02-10 09:17:36,969 [INFO] sr_shovel t_dd1_f00 cleanup
+2018-02-10 09:17:36,969 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2018-02-10 09:17:37,072 [INFO] deleting queue q_anonymous.sr_shovel.t_dd1_f00 (anonymous@dd.weather.gc.ca)
+2018-02-10 09:17:37,095 [INFO] exchange xwinnow00 remains
+2018-02-10 09:17:37,095 [INFO] exchange xwinnow01 remains
+2018-02-10 09:17:37,389 [INFO] sr_shovel t_dd2_f00 cleanup
+2018-02-10 09:17:37,389 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2018-02-10 09:17:37,498 [INFO] deleting queue q_anonymous.sr_shovel.t_dd2_f00 (anonymous@dd.weather.gc.ca)
+2018-02-10 09:17:37,522 [INFO] exchange xwinnow00 remains
+2018-02-10 09:17:37,523 [INFO] exchange xwinnow01 remains
+2018-02-10 09:17:37,804 [INFO] sr_subscribe cclean_f91 cleanup
+2018-02-10 09:17:37,804 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:37,806 [INFO] deleting queue q_tsub.sr_subscribe.cclean_f91.39328538.44917465 (tsub@localhost)
+2018-02-10 09:17:38,062 [INFO] sr_subscribe cdnld_f21 cleanup
+2018-02-10 09:17:38,062 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:38,064 [INFO] deleting queue q_tfeed.sr_subscribe.cdnld_f21.11963392.61638098 (tfeed@localhost)
+2018-02-10 09:17:38,324 [WARNING] use post_base_dir instead of document_root
+2018-02-10 09:17:38,324 [INFO] sr_subscribe cfile_f44 cleanup
+2018-02-10 09:17:38,324 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:38,326 [INFO] deleting queue q_tfeed.sr_subscribe.cfile_f44.56469334.87337271 (tfeed@localhost)
+2018-02-10 09:17:38,583 [INFO] sr_subscribe clean_f90 cleanup
+2018-02-10 09:17:38,583 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:38,585 [INFO] deleting queue q_tsub.sr_subscribe.clean_f90.45979835.20516428 (tsub@localhost)
+2018-02-10 09:17:38,854 [WARNING] extended option download_cp_command = ['cp --preserve=timestamps'] (unknown or not declared)
+2018-02-10 09:17:38,855 [INFO] sr_subscribe cp_f61 cleanup
+2018-02-10 09:17:38,855 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:38,857 [INFO] deleting queue q_tsource.sr_subscribe.cp_f61.61218922.69758215 (tsource@localhost)
+2018-02-10 09:17:39,121 [INFO] sr_subscribe ftp_f70 cleanup
+2018-02-10 09:17:39,121 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,123 [INFO] deleting queue q_tsource.sr_subscribe.ftp_f70.47997098.27633529 (tsource@localhost)
+2018-02-10 09:17:39,386 [INFO] sr_subscribe q_f71 cleanup
+2018-02-10 09:17:39,386 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,389 [INFO] deleting queue q_tsource.sr_subscribe.q_f71.84316550.21567557 (tsource@localhost)
+2018-02-10 09:17:39,658 [INFO] sr_subscribe t_f30 cleanup
+2018-02-10 09:17:39,658 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:39,660 [INFO] deleting queue q_tsub.sr_subscribe.t_f30.26453890.50752396 (tsub@localhost)
+2018-02-10 09:17:39,924 [INFO] sr_subscribe u_sftp_f60 cleanup
+2018-02-10 09:17:39,924 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,927 [INFO] deleting queue q_tsource.sr_subscribe.u_sftp_f60.81353341.03950190 (tsource@localhost)
+2018-02-10 09:17:40,196 [WARNING] option url deprecated please use post_base_url
+2018-02-10 09:17:40,196 [WARNING] use post_broker to set broker
+2018-02-10 09:17:40,197 [INFO] watch f40 cleanup
+2018-02-10 09:17:40,207 [INFO] deleting exchange xs_tsource (tsource@localhost)
+2018-02-10 09:17:40,471 [INFO] sr_winnow t00_f10 cleanup
+2018-02-10 09:17:40,471 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:40,474 [INFO] deleting queue q_tfeed.sr_winnow.t00_f10 (tfeed@localhost)
+2018-02-10 09:17:40,480 [INFO] deleting exchange xsarra (tfeed@localhost)
+2018-02-10 09:17:40,741 [INFO] sr_winnow t01_f10 cleanup
+2018-02-10 09:17:40,741 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:40,743 [INFO] deleting queue q_tfeed.sr_winnow.t01_f10 (tfeed@localhost)
+2018-02-10 09:17:40,750 [INFO] deleting exchange xsarra (tfeed@localhost)
+2018-02-10 14:17:40,753 [ERROR] config cno_trouble_f00 not found.
+Removing flow config logs...
+rm: cannot remove '/home/peter/.cache/sarra/log/sr_audit_f00.log': No such file or directory
+Removing document root ( /home/peter/sarra_devdocroot )...
+Done!
+
+
+

After the flow_cleanup.sh, to check that a test has completed, use:

+
sr3 status
+
+
+

which should show that there are no active configurations.

+

If the static_flow test works, then re-run the other tests: flakey_broker, +transform_flow, and dynamic_flow.

+
+
+

Dynamic Flow Test Length

+

While most tests have a fixed duration, the dynamic flow test queries a remote +server and can run for any length desired. The dynamic flow_test length defaults +to 1000 files being flowed through the test cases. When in rapid development, +one can supply an argument to shorten that:

+
./flow_limit.sh 200
+
+
+

Towards the end of a development cycle, longer flow_tests are adviseable:

+
./flow_limit.sh 20000
+
+
+

to identify more issues. sample run to 100,000 entries:

+
blacklab% ./flow_limit.sh 100000
+initial sample building sample size 155 need at least 100000
+sample now 100003 content_checks:GOOD missed_dispositions:0s:0
+Sufficient!
+stopping shovels and waiting...
+2018-02-10 13:15:08,964 [INFO] 2018-02-10 13:15:08,964 [INFO] info: instances option not implemented, ignored.
+info: instances option not implemented, ignored.
+2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored.
+2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored.
+running instance for config pelle_dd2_f05 (pid 20031) stopped.
+running instance for config pelle_dd1_f04 (pid 20043) stopped.
+Traceback (most recent call last):ng...
+  File "/usr/bin/rabbitmqadmin", line 1012, in <module>
+    main()
+  File "/usr/bin/rabbitmqadmin", line 413, in main
+    method()
+  File "/usr/bin/rabbitmqadmin", line 593, in invoke_list
+    format_list(self.get(uri), cols, obj_info, self.options)
+  File "/usr/bin/rabbitmqadmin", line 710, in format_list
+    formatter_instance.display(json_list)
+  File "/usr/bin/rabbitmqadmin", line 721, in display
+    (columns, table) = self.list_to_table(json.loads(json_list), depth)
+  File "/usr/bin/rabbitmqadmin", line 775, in list_to_table
+    add('', 1, item, add_to_row)
+  File "/usr/bin/rabbitmqadmin", line 742, in add
+    add(column, depth + 1, subitem, fun)
+  File "/usr/bin/rabbitmqadmin", line 742, in add
+    add(column, depth + 1, subitem, fun)
+  File "/usr/bin/rabbitmqadmin", line 754, in add
+    fun(column, subitem)
+  File "/usr/bin/rabbitmqadmin", line 761, in add_to_row
+    row[column_ix[col]] = maybe_utf8(val)
+  File "/usr/bin/rabbitmqadmin", line 431, in maybe_utf8
+    return s.encode('utf-8')
+AttributeError: 'float' object has no attribute 'encode'
+maximum of the shovels is: 100008
+
+
+

While it is running one can run flow_check.sh at any time:

+
NB retries for sr_subscribe t_f30 0
+NB retries for sr_sender 18
+
+      1 /home/peter/.cache/sarra/log/sr_cpost_veille_f34_0001.log [ERROR] sr_cpost rename: /home/peter/sarra_devdocroot/cfr/observations/xml/AB/today/today_ab_20180210_e.xml cannot stat.
+      1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/'
+      1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/'
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905. Type: <class 'urllib.error.URLError'>, Value: <urlopen error [Errno 110] Connection timed out>
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321. Type: <class 'urllib.error.URLError'>, Value: <urlopen error [Errno 110] Connection timed out>
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CS/CWEG/12/CSCN03_CWEG_101200___12074
+more than 10 TYPES OF ERRORS found... for the rest, have a look at /home/peter/src/sarracenia/test/flow_check_errors_logged.txt for details
+
+test  1 success: shovels t_dd1_f00 (100008) and t_dd2_f00 (100008) should have about the same number of items read
+test  2 success: sarra tsarra (100008) should be reading about half as many items as (both) winnows (200016)
+test  3 success: tsarra (100008) and sub t_f30 (99953) should have about the same number of items
+test  4 success: max shovel (100008) and subscriber t_f30 (99953) should have about the same number of items
+test  5 success: count of truncated headers (100008) and subscribed messages (100008) should have about the same number of items
+test  6 success: count of downloads by subscribe t_f30 (99953) and messages received (100008) should be about the same
+test  7 success: same downloads by subscribe t_f30 (199906) and files posted (add+remove) by watch (199620) should be about the same
+test  8 success: posted by watch(199620) and subscribed cp_f60 (99966) should be about half as many
+test  9 success: posted by watch(199620) and sent by sr_sender (199549) should be about the same
+test 10 success: 0 messages received that we don't know what happenned.
+test 11 success: sarra tsarra (100008) and good audit 99754 should be the same.
+test 12 success: poll test1_f62 94865 and subscribe q_f71 99935 run together. Should have equal results.
+test 13 success: post test2_f61 99731 and subscribe r_ftp_f70 99939 run together. Should be about the same.
+test 14 success: posts test2_f61 99731 and shim_f63 110795 Should be the same.
+test 15 success: cpump both pelles (c shovel) should receive about the same number of messages (160737) (160735)
+test 16 success: cdnld_f21 subscribe downloaded (50113) the same number of files that was published by both van_14 and van_15 (50221)
+test 17 success: veille_f34 should post twice as many files (100205) as subscribe cdnld_f21 downloaded (50113)
+test 18 success: veille_f34 should post twice as many files (100205) as subscribe cfile_f44 downloaded (49985)
+test 19 success: Overall 18 of 18 passed (sample size: 100008) !
+
+blacklab%
+
+
+

This test was fired up at the end of the day, as it takes several hours, and results examined the next morning.

+
+
+

High volume sample

+

Trying the flow test with higher volume of notification messages (ie. 100 000) is one step closer to the goal of having a flow test running continously. This is motivated by our testing purposes.

+
+

Limitation

+

Ubuntu have a limitation that tops inotify watches and that we encountered in #204 . We can overcome this by setting the related sysctl variable. First, check what is the limit of your system:

+
$ sysctl fs.inotify.max_user_watches
+fs.inotify.max_user_watches = 8196
+
+
+

If the limit is too low (ie. 8196), change it to a more appropriate level for the flow test:

+
$ sudo sysctl fs.inotify.max_user_watches=524288
+
+
+

To make this change permanent add this line to /etc/sysctl.conf:

+
fs.inotify.max_user_watches=524288
+
+
+

Then excute sysctl -p and the system should now support high volume of inotify events.

+
+
+

Flow Test Stuck

+

Sometimes flow tests (especially for large numbers) get stuck because of problems with the data stream (where multiple files get the same name) and so earlier versions remove later versions and then retries will always fail. Eventually, we will succeed in cleaning up the dd.weather.gc.ca stream, but for now sometimes a flow_check gets stuck ‘Retrying.’ The test has run all the notification messages required, and is at a phase of emptying out retries, but just keeps retrying forever with a variable number of items that never drops to zero.

+

To recover from this state without discarding the results of a long test, do:

+
^C to interrupt the flow_check.sh 100000
+blacklab% sr3 stop
+blacklab% cd ~/.cache/sarra
+blacklab% ls */*/*retry*
+shovel/pclean_f90/sr_shovel_pclean_f90_0001.retry        shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry        subscribe/t_f30/sr_subscribe_t_f30_0002.retry.new
+shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry        shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry.state
+shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry.state  subscribe/q_f71/sr_subscribe_q_f71_0004.retry.new
+blacklab% rm */*/*retry*
+blacklab% sr3 start
+blacklab%
+blacklab%  ./flow_check.sh 100000
+Sufficient!
+stopping shovels and waiting...
+2018-04-07 10:50:16,167 [INFO] sr_shovel t_dd2_f00 0001 stopped
+2018-04-07 10:50:16,177 [INFO] sr_shovel t_dd1_f00 0001 stopped
+2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: report option not
+implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: report option not
+implemented, ignored.
+running instance for config pelle_dd1_f04 (pid 12435) stopped.
+running instance for config pelle_dd2_f05 (pid 12428) stopped.
+maximum of the shovels is: 100075
+
+
+blacklab% ./flow_check.sh
+
+                 | dd.weather routing |
+test  1 success: sr_shovel (100075) t_dd1 should have the same number
+of items as t_dd2 (100068)
+test  2 success: sr_winnow (200143) should have the sum of the number
+of items of shovels (200143)
+test  3 success: sr_sarra (98075) should have the same number of items
+as winnows'post (100077)
+test  4 success: sr_subscribe (98068) should have the same number of
+items as sarra (98075)
+                 | watch      routing |
+test  5 success: watch (397354) should be 4 times subscribe t_f30 (98068)
+test  6 success: sr_sender (392737) should have about the same number
+of items as watch (397354)
+test  7 success: sr_subscribe u_sftp_f60 (361172) should have the same
+number of items as sr_sender (392737)
+test  8 success: sr_subscribe cp_f61 (361172) should have the same
+number of items as sr_sender (392737)
+                 | poll       routing |
+test  9 success: sr_poll test1_f62 (195408) should have half the same
+number of items of sr_sender(196368)
+test 10 success: sr_subscribe q_f71 (195406) should have about the
+same number of items as sr_poll test1_f62(195408)
+                 | flow_post  routing |
+test 11 success: sr3_post test2_f61 (193541) should have half the same
+number of items of sr_sender(196368)
+test 12 success: sr_subscribe ftp_f70 (193541) should have about the
+same number of items as sr3_post test2_f61(193541)
+test 13 success: sr3_post test2_f61 (193541) should have about the same
+number of items as shim_f63 195055
+                 | py infos   routing |
+test 14 success: sr_shovel pclean_f90 (97019) should have the same
+number of watched items winnows'post (100077)
+test 15 success: sr_shovel pclean_f92 (94537}) should have the same
+number of removed items winnows'post (100077)
+test 16 success: 0 messages received that we don't know what happenned.
+test 17 success: count of truncated headers (98075) and subscribed
+messages (98075) should have about the same number of items
+                 | C          routing |
+test 18 success: cpump both pelles (c shovel) should receive about the
+same number of messages (161365) (161365)
+test 19 success: cdnld_f21 subscribe downloaded (47950) the same
+number of files that was published by both van_14 and van_15 (47950)
+test 20 success: veille_f34 should post twice as many files (95846) as
+subscribe cdnld_f21 downloaded (47950)
+test 21 success: veille_f34 should post twice as many files (95846) as
+subscribe cfile_f44 downloaded (47896)
+test 22 success: Overall 21 of 21 passed (sample size: 100077) !
+
+NB retries for sr_subscribe t_f30 0
+NB retries for sr_sender 36
+
+
+

So, in this case, the results are still good in spite of not quite being +able to terminate. If there was a significant problem, the cumulation +would indicate it.

+
+
+
+

Flow tests with MQTT

+

Flow tests can be run where certain components use the MQTT protocol, instead of AMQP.

+

FIXME: steps missing, more clarity required.

+
    +
  • MQTT broker is installed

  • +
  • the bunnymaster tsource, tfeed, tsub users defined and given passwords (broker dependent.)

  • +
  • for each user: an mqtt://user:pw@brokerhost url’s line is added to ~/.config/sr3/credentials.conf

  • +
  • edit the variable MQP in ~/.config/sr3/default.conf, MQP is used by the flow tests.

  • +
+

Most components will use MQTT instead of amqp and can be run normally.

+
+
+
+

Commits to the Development Branch

+

Aside from typos, language fixups in the documentation, and incrementing +the version, developers are not expected to commit to stable. All work +happens on development branches, and all testing is expected to pass before +one considers affecting stable. Once the branch development is complete, +or a unit of work-in-progress is felt to be worth merging to stable, one +must summarize the changes from the branch for the debian change log, +request on github.

+
git checkout issueXXX  # v02_issueXXX for v2 work., github suggested branch names are fine also.
+vi CHANGES.rst # summarize the changes in Restructured Text
+dch # copy/paste from CHANGES.rst, inserting one leading space.
+vi doc/UPGRADING.rst # rarely, if code has user impact.
+vi doc/fr/UPGRADING.rst # bon... ceci est visible aux usagers, donc...
+git commit -a
+git push
+# issue a pull request on github.com.
+
+
+

A Second developer will review the pull request and the reviewer will decide on whether +merging is appropriate. The developer is expected to examine each commit, and +understand it to some degree.

+

If the pull-request has one of the following (substantial changes, new functionality, modifications to critical code structure) , it is recommended to have a Third developer also review the pull request. The expectation from this developer are the same as from the previous.

+

The github Actions looks at pull requests and will flow tests on them. +If the tests pass, then that is good qualitative indicator, however the tests are a bit +fragile at the moment, so if they fail, it would be ideal for the reviewer to run +the tests in their own development environment. If it passes in the local developer +environment one can approve a merge in spite of Github Actions’ complaints.

+
+
+

Key Branches

+

There is a long running discussion about Which Version is stable +The current set up is that there are four principal branches:

+
    +
  • stable branch is the release version of sr3, merging from pre-release. used to build sr3 packages in the +MetPX repository.

  • +
  • development … The version 3 work in progress branch is a next version of sarracenia. +the development branch is used to build sr3 packages for the Daily +and Pre-Release repositories on launchpad.net.

  • +
  • stable_py36 and pre-relrease-36 are branched from stable and pre_release respectively to adjust for building +packages on older operating systems that have older versions of python (and no support for hatchling.)

  • +
  • issue branches to be merged to development, it should be start with issueXXX or suggested branch names from github are ok also.

  • +
  • sometimes, multiple branches are needed for a single issue, say for variations of a fix, eg. issueXXX_2_do_it_this_way .

  • +
  • v2_dev … the integration branch for v2 maintenance used prior to promotion to v2_stable.

  • +
  • v2_stable … generally this branch gets code via merges from v2_dev, after the pre-release has been tested on a +as many systems as possible. used to build packages on the stable: MetPX

  • +
+
+
+

Repositories

+

For Ubuntu operating systems, the launchpad.net site is the best way to provide packages that are fully integrated +( built against current patch levels of all dependencies (software components that Sarracenia relies +on to provide full functionality.)) Ideally, when running a server, a one should use one of the repositories, +and allow automated patching to upgrade them as needed.

+

Repositories:

+ +

for more discussion see Which Version is stable

+
+

Local Python

+

Working with a non-packaged version:

+

notes:

+
pip install -e .
+
+
+
+
+

Windows

+

Install winpython from github.io version 3.5 or higher. Then use pip to install from PyPI.

+
+
+
+

Conventions

+

Below are some coding practices that are meant to guide developers when contributing to sarracenia. +They are not hard and fast rules, just guidance.

+
+

When to Report

+

sr_report(7) notification messages should be emitted to indicate final disposition of the data itself, not +any notifications or report messages (don’t report report messages, it becomes an infinite loop!) +For debugging and other information, the local log file is used. For example, sr_shovel does +not emit any sr_report(7) messages, because no data is transferred, only messages.

+
+
+
+

Adding a New Dependency

+

Dependency Management is a complicated topic, because python has many different installation methods into disparate environments, and Sarracenia is multi-platform. Standard python practice for dependencies is to make +them required by listing them in requirements.txt or setup.py, and require all users to install them. +In most python applications, if a dependency is missing, it just crashes with a import failure message +of some kind.

+

In Sr3, we have found that there are many different environments being deployed into where satisfying +dependencies can be more trouble than they are worth, so each of the dependencies in setup.py are also +dealt with in sarracenia/featuredetection, and the feature detection code allows the application to +keep working, just without the functionality provided by the missing module. This is called degradation +or degraded mode. The idea being to help the user do as much as they can, in the environment they have, +while telling them what is missing, and what would ideally be added.

+

for a full discussion see:

+

Managing Dependencies (Discussion) <https://github.com/MetPX/sarracenia/issues/741>

+

Short version:

+

In addition to requirements.dev/setup.py, if you need to add a new library that isn’t part of +batteries included, typically provided by a separate os or pip package, then you want to +provide for the package to still work in the event that the package is not available (albeit +without the function you are adding) and to add support for explaining what’s missing using +the sarracenia/featuredetection.py module.

+

In that module is a features data structure, where you add an entry explaining the import +needed, and the functionality it brings to Sr3. You also add if feature[‘x’][‘present’] guards +in the code where you are using the feature, in order to allow the code to degrade elegantly.

+

If the dependency is added in a plugin, then there is also a method for that described here:

+

Plugin Developer Guide <../Explanation/SarraPluginDev.html#callbacks-that-need-python-modules>

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Documentation.html b/Contribution/Documentation.html new file mode 100644 index 000000000..ca5077d5d --- /dev/null +++ b/Contribution/Documentation.html @@ -0,0 +1,365 @@ + + + + + + + Documentation Standards — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Documentation Standards

+
+

Folder Structure

+

Before starting work with documentation read the entire divo documentation article (and the links on the left hand sidebar). +It’ll take no longer than 30 minutes and you’ll gain a complete understanding of the +expected structure, style and content of the documentation here.

+https://documentation.divio.com/_images/overview.png + +

We look to the unix directive in our documentation, each documentation file does one thing +and does it well with respect to the 4 quadrants.

+
+

Note

+

TODO:Add example links to each of the below sections:

+
+
+

Processing

+

The entire documentation is under the docs/source tree. It is processed using sphinx, invoked +using the Makefile in docs/. One can install sphinx locally, and run make to build locally +and debug. The result is produced in docs/build/html:

+
pip install -f requirements-dev.txt
+cd docs
+make html
+
+
+

then point a browser to docs/build/html.

+

There is a github Actions jobs that does this on each push to appropriate branchs to update +the main documentation. the main url for the resulting web-site is:

+
+
+

The publication process is automated by two github CI/CD actions:

+
+
+
+

It is very good to have a look at the sphinx error messages generated by the first job. There are typically +hundreds of small problems to fix (links that are not quite right, broken table formatting, etc…)

+
+
+

Tutorials

+
    +
  • Learning-oriented, specifically learning how rather than learning that.

  • +
  • Allow the user to learn by doing to get them started, be sure your tutorial works and users can see results immediately.

  • +
  • Tutorials must be reliably repeatable focused on concrete steps (not abstract concepts) with the minimum necessary explanation.

  • +
+

Many of the Tutorials are built using jupyter notebooks. See docs/source/Tutorials/README.md for +how to work with them.

+
+
+

How2Guides

+
    +
  • Problem and goal oriented: “I want to… How do I…” Differing from tutorials in that tutorials are for complete beginners, how to guides assume some knowledge and understanding with a basic setup and tools.

  • +
  • Provide a series of steps focused on the results of some particular problem.

  • +
  • Don’t explain concepts, if they are important, they can be linked to in ../Explanation/

  • +
  • There are multiple ways to skin a cat, remain flexible in your guide so users can see how things are done.

  • +
  • NAME GUIDES WELL enough to tell the user exactly what they do at a glance.

  • +
+
+
+

Explanation

+
    +
  • Understanding-oriented: can be equally considered discussions. Much more relaxed version of documentation where concepts are explored from a higher level or different perspectives.

  • +
  • Provide context, discuss alternatives and opinions while providing technical reference (for other sections).

  • +
+
+
+

Reference

+
    +
  • Dictionary style.

  • +
  • Information oriented: code-determined descriptions of functionality.

  • +
  • Strictly for the man pages and direct reference of the various programs, protocols, and functions.

  • +
+
+
+

Contribution

+
    +
  • Information critical to the enhancement and progression of the Sarracenia project, ie: for those that are looking to develop Sarracenia.

  • +
  • Style guide(s)

  • +
  • Template(s)

  • +
+
+
+
+

Process

+

The development process is to write up what one intends to do or have done into +a reStructuredText +file in /docs/Explanation/Design/. Ideally, the discussion of information there acts +as a starting point which can be edited into documentation for the feature(s) as they +are implemented. Each new component sr_<whatever>, needs to have relevant man pages +implemented. The how to guides and tutorials should also be revised to reflect additions +or changes of the new component.

+
+

Error

+

Need Peter to help ID worthy information in doc/design to pull over to +/docs/Explanation/Design/

+
+
+
+

Style Guide

+

Command line execution shall be written in the style of:

+
An initial comment describing the following steps or processes::
+
+  $ command 1
+    relevant output
+  $command 2
+    .
+    .
+    relevant output
+    newline relevant output
+
+
+

Important notes:

+
    +
  • Initial comment ends with :: followed by an empty newline

  • +
  • Thereafter lies the (two space) indented code block

  • +
  • Commands syntax: ‘$ <cmd>

    +
      +
    • Alternatively indicate root level commands with ‘# <cmd>

    • +
    +
  • +
  • Command output is (two space) indented from leading command.

    +
      +
    • Irrelevant lines of output may be substituted for dots or outright omitted.

    • +
    +
  • +
+

pick and stick to a default header hierarchy (ie : = > ~ > - > … for title > h1 > h2 > h3… etc)

+
+

Code Style

+

We generally follow PEP 8 standards for code formatting, and use YAPF to automatically re-format code. One exception to PEP 8 is that we use a 119 character line length.

+

For docstrings in code, we are following the Google Style Guide. These docstrings will be parsed into formatted documentation by Sphinx.

+

Detailed examples can be found in the Napoleon Sphinx plugin’s docs and the Google Python Style Guide.

+

Selected examples from credentials.py:

+
class Credential:
+    """An object that holds information about a credential, read from a
+    credential file, which has one credential per line, format::
+        url option1=value1, option2=value2
+
+    Examples::
+        sftp://alice@herhost/ ssh_keyfile=/home/myself/mykeys/.ssh.id_dsa
+        ftp://georges:Gpass@hishost/  passive = True, binary = True
+
+    `Format Documentation. <https://metpx.github.io/sarracenia/Reference/sr3_credentials.7.html>`_
+
+    Attributes:
+        url (urllib.parse.ParseResult): object with URL, password, etc.
+        ssh_keyfile (str): path to SSH key file for SFTP
+        passive (bool): use passive FTP mode, defaults to ``True``
+        binary (bool): use binary FTP mode, defaults to ``True``
+        tls (bool): use FTPS with TLS, defaults to ``False``
+        prot_p (bool): use a secure data connection for TLS
+        bearer_token (str): bearer token for HTTP authentication
+        login_method (str): force a specific login method for AMQP (PLAIN,
+            AMQPLAIN, EXTERNAL or GSSAPI)
+    """
+
+    def __init__(self, urlstr=None):
+        """Create a Credential object.
+
+            Args:
+                urlstr (str): a URL in string form to be parsed.
+        """
+
+
+
def isValid(self, url, details=None):
+    """Validates a URL and Credential object. Checks for empty passwords, schemes, etc.
+
+    Args:
+        url (urllib.parse.ParseResult): ParseResult object for a URL.
+        details (sarracenia.credentials.Credential): sarra Credential object containing additional details about
+            the URL.
+
+    Returns:
+        bool: ``True`` if a URL is valid, ``False`` if not.
+    """
+
+
+
+
+

Why rST?

+

reStructuredText was chosen primarily as it supports the auto-creation of a table of contents with the ‘.. contents::’ directive. +Like many other markup languages, it also supports inline styling, tables, headings and literal blocks.

+

In Jupyter Notebooks, unfortunately, only Markdown is supported, elsewhere RST is great.

+
+
+
+

Localization

+

This project is intended to be translated in French and English at a minimum as it’s +used across the Government of Canada which has these two official languages.

+

The French documentation has the same file structure and names as the English, but +is placed under the fr/ sub-directory. It’s easiest if the documentation is produced +in both languages at once. At the very least use an auto translation tool (such as +deepl) to provide a starting point. Same procedure in +reverse for Francophones.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Evolution.html b/Contribution/Evolution.html new file mode 100644 index 000000000..d65ee5e81 --- /dev/null +++ b/Contribution/Evolution.html @@ -0,0 +1,172 @@ + + + + + + + Design Changes since Original (2015) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Design Changes since Original (2015)

+

as of 2022/03, the design has not changed much, but sr3’s implementation +is totally different from v2. Design changes:

+
    +
  • explicit core application support for reports were ripped out +as they were never used, can easily be re-inserted as callbacks +and conventions.

  • +
  • nobody used segmented files, and they were very complicated, +but everyone finds them fantastic in theory. Need to re-implement +post sr3 re-factor.

  • +
  • mirroring was a use case we had to address, had to add metadata, +and evolve a bit.

  • +
  • the cluster routing concepts have been removed (cluster_from, cluster_to, etc…) +it got in the analysts’ way more than it helped. Dead easy to +implement using flow callbacks, if we ever want them back.

  • +
  • the Flow algorithm +emerged as a unifying concept for all the different components +originally envisaged. In early work on v2, we didn’t know if +all the components would work the same, so they were written +separately from scratch. A lot of copy/paste code among +entry points.

  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/AboutTime.html b/Contribution/Philosophy/AboutTime.html new file mode 100644 index 000000000..aca212b57 --- /dev/null +++ b/Contribution/Philosophy/AboutTime.html @@ -0,0 +1,954 @@ + + + + + + + STATUS: WIP — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

STATUS: WIP

+

work in progress. Not worth reading yet. more notes than anything else.

+
+
+

It´s About Time

+

Say you are a weather forecaster, charged with producing a prediction of the weather, perhaps for the public, or perhaps for a vertical domain, such as aviation, or transport. In normal situations, the forecaster will be asked to produce a forecast at a specific time of day, to help her clients plan their business.

+

How does a forecaster build a forecast?

+

Assuming the forecaster is in the americas, one could use a browser:

+
    +
  • look at a picture of the sky from a GOES satellite image (visible spectrum), to see where it is cloudy at the moment.

  • +
  • look at a different web site for closer in shots from polar orbiting satellites (e.g. HRDPS) that cover a smaller area.

  • +
  • look at a United States Geological Survey, or Water Survey of Canada web site to see the current stream flows.

  • +
  • look at a some weather web sites, to see the current observations from surface stations.

  • +
  • look at some Aviation (FAA or NAVCANADA web sites) to find weather observations at airports (which tend to have a tightly controlled quality, and so are relatively reliable.)

  • +
  • Find RADAR imagery on the net for the areas of interest (it might be that two or three RADAR stations have coverage of the area one is looking at.

  • +
+

If you want to look at all that information together… you build a really large spreadsheet where each cell describes one part of the sky, and for each cell, you would store something like temperature, barometric pressure, and wind (direction and speed.)

+
+
Now to get movement, you need to be looking at a trend, not just a snapshot.
+
From one picture, you don´t know the direction that anything is moving. So you really need at least two snapshots to get speed.
+
+

Cloud is a one location, p0 at an initial time, and and has moved to p1 at time t1.

+
+
[ ]:
+
+
+
+#illustration with two points.
+
+
+
+

Now to predict the future, with just two points in time, you have to assume stuff is always moving at the same speed. If you think air masses are accellerating or slowing down, you need another picture. With three of them you can compare the speeds between the first two pictures, and the last two pictures, and see a change in speed.

+
+
[ ]:
+
+
+
+#illustration with three points.
+
+
+
+

Now with three points, you have constant accelleration… but if the acceleration itself is varying, then you need more again.

+

The more points in time you have, the better you can understand the movement to establish the wind speed and direction at a given point in time, and how it will change in future.

+

So once you have good information about the clouds that exist, and the rate they are moving at, and whether they are speeding up, or building up, or dissipating… then a forecaster is trained in physics, and applies the rules of physics to understand how clouds will move in the future. Essentially it uses the spreadsheet to calculate what the sky should look like one small step later in time, and then another step, and another, until you have arrived as far in time as needed for the forecast +product.

+

At this point, the result is a spreadsheet. The forecaster then can use the spreadsheet and write descriptions of weather for their client, or have an automated process do that, or produce simulated ¨satellite” imagery to show where the clouds till in the future.

+

Restating things:

+
    +
  • step 1: Acquisiion: gather a time series of data for kinds of data.

  • +
  • step 2: Assimilation: put all the different data into a pile of spreadsheets.

  • +
  • step 3: Numerical Model: hit calculate on the spreadsheet for the number of timestesp you need.

  • +
  • step 4: Services: translating the spreadsheets back into things people can understand (maps, text, and simulated future images.)

  • +
+

Looking at all these steps, it is obvious that they are extremely tedious for a human to do, and things that a computer, in principle should be great at. Rather than having a human look at web sites, and extract data, have a computer do it, and present the forecaster with a best first guess product.

+
+

Let the Computer Do It

+

Instead of a human browsing dozens of web sites and mapping stuff into a spreadsheet manually, and then hitting calculate, and then mapping back the spreadsheet back to something his clients will understand, the forecaster hits a button, and has the computer does all the tedious work. So when the forecaster pushes the button, the computer:

+
    +
  • scans the entire world for a number of hours of satellite, RADAR, and point observations from weather stations, airports, ships, and planes. looking for information for the last hour or two to get a good trend.

  • +
  • stuffs that into a spreadsheet.

  • +
  • runs the spreadsheet.

  • +
  • extracts the interesting numbers or pictures from the spreadsheet.

  • +
+

Great, how long will that take, and how good will the result be?

+

Well the quality of the result will vary with the quality of the input. To make a spreadsheet, the forecaster decides how big an volume of space to cover with each cell. The bigger the volume covered by each cell, the more you are taking different data points and averaging them to get one value for the the cell, so the “fuzzier” the pictures that result.

+

How big are the spreadsheets? 30 years ago, the “high resolution” spreadsheet covering North America had each cell was 150 km. on a side, At that time, computers were not big enough to cover the whole world. Today, the high resolution models are around 10km on a side (100 sq. km.) As the area of the earth is around 500 million sq. km. that means that the grid today should be about 5 million cells. per level, models typically have 25 levels representing different heights of air in the atmosphere, +so that means 125 million cells to calculate.

+
+
[5]:
+
+
+
+import plotly.express as px
+import pandas as pd
+
+df = pd.DataFrame([
+    dict(Task="Acquisition", Start='12:05:00', Finish='12:45:00'),
+    dict(Task="Assimilation", Start='12:45:00', Finish='13:05:00'),
+    dict(Task="Model", Start='13:05:00', Finish='13:45:00'),
+    dict(Task="Services", Start='13:45:00', Finish='14:10:00')
+
+])
+
+fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task")
+fig.update_yaxes(autorange="reversed") # otherwise tasks are listed from the bottom up
+fig.show()
+
+
+
+
+
+
+
+
+/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1753: UserWarning:
+
+Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
+
+/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1754: UserWarning:
+
+Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.
+
+
+
+
+
+
+
+
+
+

Each cell has information about different phenomena, called variables, say 25 of them. so to get a full picture, one needs 125 million x 25 variables x 8 bytes per variable = 6.2 billion variables in about 50 gbytes of memory. To calculate the what the values of those variables are at the next unit in time involve many calculations using that data, and the prediction’s accuracy involves some tradeoffs:

+

The simpler the model, the simpler the math, the less calculation time to get a result. The more complicated the model & math, the better the result, but the longer it takes to do.

+ +
+
[6]:
+
+
+
+import matplotlib.pyplot as plt
+import numpy as np
+import matplotlib.dates as mdates
+from datetime import datetime
+
+try:
+    # Try to fetch a list of Matplotlib releases and their dates
+    # from https://api.github.com/repos/matplotlib/matplotlib/releases
+    import urllib.request
+    import json
+
+    url = 'https://api.github.com/repos/matplotlib/matplotlib/releases'
+    url += '?per_page=100'
+    data = json.loads(urllib.request.urlopen(url, timeout=1).read().decode())
+
+    dates = []
+    names = []
+    for item in data:
+        if 'rc' not in item['tag_name'] and 'b' not in item['tag_name']:
+            dates.append(item['published_at'].split("T")[0])
+            names.append(item['tag_name'])
+    # Convert date strings (e.g. 2014-10-18) to datetime
+    dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates]
+
+except Exception:
+    # In case the above fails, e.g. because of missing internet connection
+    # use the following lists as fallback.
+    names = ['v2.2.4', 'v3.0.3', 'v3.0.2', 'v3.0.1', 'v3.0.0', 'v2.2.3',
+             'v2.2.2', 'v2.2.1', 'v2.2.0', 'v2.1.2', 'v2.1.1', 'v2.1.0',
+             'v2.0.2', 'v2.0.1', 'v2.0.0', 'v1.5.3', 'v1.5.2', 'v1.5.1',
+             'v1.5.0', 'v1.4.3', 'v1.4.2', 'v1.4.1', 'v1.4.0']
+
+    dates = ['2019-02-26', '2019-02-26', '2018-11-10', '2018-11-10',
+             '2018-09-18', '2018-08-10', '2018-03-17', '2018-03-16',
+             '2018-03-06', '2018-01-18', '2017-12-10', '2017-10-07',
+             '2017-05-10', '2017-05-02', '2017-01-17', '2016-09-09',
+             '2016-07-03', '2016-01-10', '2015-10-29', '2015-02-16',
+             '2014-10-26', '2014-10-18', '2014-08-26']
+
+    # Convert date strings (e.g. 2014-10-18) to datetime
+    dates = [datetime.strptime(d, "%Y-%m-%d") for d in dates]
+
+
+
+
+
[3]:
+
+
+
+# Choose some nice levels
+levels = np.tile([-5, 5, -3, 3, -1, 1],
+                 int(np.ceil(len(dates)/6)))[:len(dates)]
+
+# Create figure and plot a stem plot with the date
+fig, ax = plt.subplots(figsize=(8.8, 4), layout="constrained")
+ax.set(title="Matplotlib release dates")
+
+ax.vlines(dates, 0, levels, color="tab:red")  # The vertical stems.
+ax.plot(dates, np.zeros_like(dates), "-o",
+        color="k", markerfacecolor="w")  # Baseline and markers on it.
+
+# annotate lines
+for d, l, r in zip(dates, levels, names):
+    ax.annotate(r, xy=(d, l),
+                xytext=(-3, np.sign(l)*3), textcoords="offset points",
+                horizontalalignment="right",
+                verticalalignment="bottom" if l > 0 else "top")
+
+# format x-axis with 4-month intervals
+ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4))
+ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y"))
+plt.setp(ax.get_xticklabels(), rotation=30, ha="right")
+
+# remove y-axis and spines
+ax.yaxis.set_visible(False)
+ax.spines[["left", "top", "right"]].set_visible(False)
+
+ax.margins(y=0.1)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_AboutTime_12_0.png +
+
+
+
[7]:
+
+
+
+import pandas as pd
+import numpy as np
+import matplotlib
+import matplotlib.pyplot as plt
+import datetime as dt
+
+# from https://www.datacamp.com/tutorial/how-to-make-gantt-chart-in-python-matplotlib
+
+
+
+
+
+
+
[11]:
+
+
+
+df = pd.DataFrame({'task': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'],
+                  'team': ['R&D', 'Accounting', 'Sales', 'Sales', 'IT', 'R&D', 'IT', 'Sales', 'Accounting', 'Accounting', 'Sales', 'IT'],
+                  'start': pd.to_datetime(['20 Oct 2022', '24 Oct 2022', '26 Oct 2022', '31 Oct 2022', '3 Nov 2022', '7 Nov 2022', '10 Nov 2022', '14 Nov 2022', '18 Nov 2022', '23 Nov 2022', '28 Nov 2022', '30 Nov 2022']),
+                  'end': pd.to_datetime(['31 Oct 2022', '28 Oct 2022', '31 Oct 2022', '8 Nov 2022', '9 Nov 2022', '18 Nov 2022', '17 Nov 2022', '22 Nov 2022', '23 Nov 2022', '1 Dec 2022', '5 Dec 2022', '5 Dec 2022']),
+                  'completion_frac': [1, 1, 1, 1, 1, 0.95, 0.7, 0.35, 0.1, 0, 0, 0]})
+print(df)
+
+
+
+
+
+
+
+
+   task        team      start        end  completion_frac
+0     A         R&D 2022-10-20 2022-10-31             1.00
+1     B  Accounting 2022-10-24 2022-10-28             1.00
+2     C       Sales 2022-10-26 2022-10-31             1.00
+3     D       Sales 2022-10-31 2022-11-08             1.00
+4     E          IT 2022-11-03 2022-11-09             1.00
+5     F         R&D 2022-11-07 2022-11-18             0.95
+6     G          IT 2022-11-10 2022-11-17             0.70
+7     H       Sales 2022-11-14 2022-11-22             0.35
+8     I  Accounting 2022-11-18 2022-11-23             0.10
+9     J  Accounting 2022-11-23 2022-12-01             0.00
+10    K       Sales 2022-11-28 2022-12-05             0.00
+11    L          IT 2022-11-30 2022-12-05             0.00
+
+
+
+
[17]:
+
+
+
+df['days_to_start'] = (df['start'] - df['start'].min()).dt.days
+df['days_to_end'] = (df['end'] - df['start'].min()).dt.days
+df['task_duration'] = df['days_to_end'] - df['days_to_start'] + 1  # to include also the end date
+df['completion_days'] = df['completion_frac'] * df['task_duration']
+print(df)
+
+
+
+
+
+
+
+
+   task        team      start        end  completion_frac  days_to_start  \
+0     A         R&D 2022-10-20 2022-10-31             1.00              0
+1     B  Accounting 2022-10-24 2022-10-28             1.00              4
+2     C       Sales 2022-10-26 2022-10-31             1.00              6
+3     D       Sales 2022-10-31 2022-11-08             1.00             11
+4     E          IT 2022-11-03 2022-11-09             1.00             14
+5     F         R&D 2022-11-07 2022-11-18             0.95             18
+6     G          IT 2022-11-10 2022-11-17             0.70             21
+7     H       Sales 2022-11-14 2022-11-22             0.35             25
+8     I  Accounting 2022-11-18 2022-11-23             0.10             29
+9     J  Accounting 2022-11-23 2022-12-01             0.00             34
+10    K       Sales 2022-11-28 2022-12-05             0.00             39
+11    L          IT 2022-11-30 2022-12-05             0.00             41
+
+    days_to_end  task_duration  completion_days
+0            11             12            12.00
+1             8              5             5.00
+2            11              6             6.00
+3            19              9             9.00
+4            20              7             7.00
+5            29             12            11.40
+6            28              8             5.60
+7            33              9             3.15
+8            34              6             0.60
+9            42              9             0.00
+10           46              8             0.00
+11           46              6             0.00
+
+
+
+
[18]:
+
+
+
+plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_AboutTime_16_0.png +
+
+
+
[3]:
+
+
+
+import plotly.express as px
+import pandas as pd
+
+df = pd.DataFrame([
+    dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
+    dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15'),
+    dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30')
+])
+
+fig = px.timeline(df, x_start="Start", x_end="Finish", y="Task")
+fig.update_yaxes(autorange="reversed") # otherwise tasks are listed from the bottom up
+fig.show()
+
+
+
+
+
+
+
+
+
+
+
[70]:
+
+
+
+import plotly.express as px
+import copy
+import pandas as pd
+import datetime as dt
+
+today=dt.datetime.now()
+first_task_start=dt.time(hour=12,minute=5)
+time_of_start=today.combine(today,first_task_start)
+
+df = pd.DataFrame([
+    dict(Task="Acquisition", Start_time=time_of_start, Duration='00:40:00', End_time=today),
+    dict(Task="GOES images T-12", Start_with='Acquisition', Duration='00:05:00', End_time=today),
+    dict(Task="GOES images T-11", Start_after='GOES images T-12', Duration='00:05:00', End_time=today),
+    dict(Task="GOES images T-10", Start_after='GOES images T-11', Duration='00:05:00', End_time=today),
+    dict(Task="GOES images T-09", Start_after='GOES images T-10', Duration='00:05:00', End_time=today),
+    dict(Task="Assimilation", Start_after='Acquisition', Duration='00:30:00'),
+    dict(Task="Model", Start_after='Assimilation', Duration='00:45:00')
+])
+
+df['Duration']=pd.to_timedelta(df['Duration'])
+
+for ti in df.index:
+    if pd.isna(df['Start_time'][ti]):
+        for predecessor in df.index:
+            if df['Task'][predecessor] == df['Start_after'][ti]:
+                df.loc[ti, 'Start_time'] = df['End_time'][predecessor]
+            elif df['Task'][predecessor] == df['Start_with'][ti]:
+                df.loc[ti, 'Start_time'] = df['Start_time'][predecessor]
+    else:
+        pd.to_datetime(df['Start_time'][ti])
+
+    df.loc[ti, 'End_time'] = df['Start_time'][ti]+df['Duration'][ti]
+
+
+print(df)
+
+
+fig = px.timeline(df, x_start="Start_time", x_end="End_time", y="Task")
+fig.update_yaxes(autorange="reversed") # otherwise tasks are listed from the bottom up
+fig.show()
+
+
+
+
+
+
+
+
+               Task          Start_time        Duration            End_time  \
+0       Acquisition 2023-09-08 12:05:00 0 days 00:40:00 2023-09-08 12:45:00
+1  GOES images T-12 2023-09-08 12:05:00 0 days 00:05:00 2023-09-08 12:10:00
+2  GOES images T-11 2023-09-08 12:10:00 0 days 00:05:00 2023-09-08 12:15:00
+3  GOES images T-10 2023-09-08 12:15:00 0 days 00:05:00 2023-09-08 12:20:00
+4  GOES images T-09 2023-09-08 12:20:00 0 days 00:05:00 2023-09-08 12:25:00
+5      Assimilation 2023-09-08 12:45:00 0 days 00:30:00 2023-09-08 13:15:00
+6             Model 2023-09-08 13:15:00 0 days 00:45:00 2023-09-08 14:00:00
+
+    Start_with       Start_after
+0          NaN               NaN
+1  Acquisition               NaN
+2          NaN  GOES images T-12
+3          NaN  GOES images T-11
+4          NaN  GOES images T-10
+5          NaN       Acquisition
+6          NaN      Assimilation
+
+
+
+
+
+
+
+/home/peter/.local/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:
+
+The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result
+
+
+
+
+
+
+
+
+
+
+
[71]:
+
+
+
+import plotly.express as px
+
+# Create a simple line plot
+data = {'x': [1, 2, 3, 4, 5], 'y': [2, 4, 1, 6, 3]}
+fig = px.line(data, x='x', y='y')
+
+# Display the plot
+fig.show()
+
+
+
+
+
+
+
+
+
+
+
[ ]:
+
+
+
+
+
+
+
+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/AboutTime.ipynb b/Contribution/Philosophy/AboutTime.ipynb new file mode 100644 index 000000000..45eaab6ea --- /dev/null +++ b/Contribution/Philosophy/AboutTime.ipynb @@ -0,0 +1,4250 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc172731", + "metadata": {}, + "source": [ + "# STATUS: WIP\n", + "work in progress. Not worth reading yet. more notes than anything else." + ] + }, + { + "cell_type": "markdown", + "id": "6395a549", + "metadata": {}, + "source": [ + "# It´s About Time\n", + "\n", + "Say you are a weather forecaster, charged with producing a prediction of the weather, perhaps for the public, or perhaps for a vertical domain, such as aviation, or transport. In normal situations, the forecaster will be asked to produce a forecast at a specific time of day, to help her clients plan their business.\n", + "\n", + "How does a forecaster build a forecast? \n", + "\n", + "Assuming the forecaster is in the americas, one could use a browser:\n", + "\n", + "* look at a picture of the sky from a GOES satellite image (visible spectrum), to see where it is cloudy at the moment.\n", + "* look at a different web site for closer in shots from polar orbiting satellites (e.g. HRDPS) that cover a smaller area.\n", + "* look at a United States Geological Survey, or Water Survey of Canada web site to see the current stream flows.\n", + "* look at a some weather web sites, to see the current observations from surface stations.\n", + "* look at some Aviation (FAA or NAVCANADA web sites) to find weather observations at airports (which tend to have a tightly controlled quality, and so are relatively reliable.)\n", + "* Find RADAR imagery on the net for the areas of interest (it might be that two or three RADAR stations have coverage of the area one is looking at.\n", + "\n", + "If you want to look at all that information together... you build a really large spreadsheet where each cell describes one part of the sky, and for each cell, you would store something like temperature, barometric pressure, and wind (direction and speed.)\n" + ] + }, + { + "cell_type": "markdown", + "id": "2502a307", + "metadata": {}, + "source": [ + "Now to get movement, you need to be looking at a trend, not just a snapshot. \n", + "From one picture, you don´t know the direction that anything is moving. \n", + "So you really need at least two snapshots to get speed.\n", + "\n", + "Cloud is a one location, p0 at an initial time, and and has moved to p1 at time t1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "703ed5c2", + "metadata": {}, + "outputs": [], + "source": [ + "#illustration with two points." + ] + }, + { + "cell_type": "markdown", + "id": "8d6d3a94", + "metadata": {}, + "source": [ + "\n", + "Now to predict the future, with just two points in time, you have to assume stuff is always moving at the same speed. If you think air masses are accellerating or slowing down, you need another picture. With three of them you can compare the speeds between the first two pictures, and the last two pictures, and see a change in speed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c1a4040", + "metadata": {}, + "outputs": [], + "source": [ + "#illustration with three points." + ] + }, + { + "cell_type": "markdown", + "id": "e5006a41", + "metadata": {}, + "source": [ + "Now with three points, you have constant accelleration... but if the acceleration itself is varying, then you need more again." + ] + }, + { + "cell_type": "markdown", + "id": "eb70daaa", + "metadata": {}, + "source": [ + "The more points in time you have, the better you can understand the movement to establish the wind speed and direction at a given point in time, and how it will change in future.\n", + "\n", + "So once you have good information about the clouds that exist, and the rate they are moving at, and whether they are speeding up, or building up, or dissipating... then a forecaster is trained in physics, and applies the rules of physics to understand how clouds will move in the future. Essentially it uses the spreadsheet to calculate what the sky should look like one small step later in time, and then another step, and another, until you have arrived as far in time as needed for the forecast product.\n", + "\n", + "At this point, the result is a spreadsheet. The forecaster then can use the spreadsheet and write descriptions of weather for their client, or have an automated process do that, or produce simulated ¨satellite\" imagery to show where the clouds till in the future.\n", + "\n", + "Restating things:\n", + "\n", + "* step 1: Acquisiion: gather a time series of data for kinds of data.\n", + "* step 2: Assimilation: put all the different data into a pile of spreadsheets.\n", + "* step 3: Numerical Model: hit calculate on the spreadsheet for the number of timestesp you need.\n", + "* step 4: Services: translating the spreadsheets back into things people can understand (maps, text, and simulated future images.)\n", + "\n", + "\n", + "Looking at all these steps, it is obvious that they are extremely tedious for a human to do, and things that a computer, in principle should be great at. Rather than having a human look at web sites, and extract data,\n", + "have a computer do it, and present the forecaster with a best first guess product.\n", + "\n", + "## Let the Computer Do It\n", + "\n", + "Instead of a human browsing dozens of web sites and mapping stuff into a spreadsheet manually, and then hitting calculate, and then mapping back the spreadsheet back to something his clients will understand, the forecaster hits a button, and has the computer does all the tedious work. So when the forecaster pushes the button, the computer:\n", + "\n", + "* scans the entire world for a number of hours of satellite, RADAR, and point observations from weather stations, airports, ships, and planes. looking for information for the last hour or two to get a good trend.\n", + "* stuffs that into a spreadsheet.\n", + "* runs the spreadsheet.\n", + "* extracts the interesting numbers or pictures from the spreadsheet.\n", + "\n", + "Great, how long will that take, and how good will the result be?\n", + "\n", + "Well the quality of the result will vary with the quality of the input. To make a spreadsheet, the forecaster decides how big an volume of space to cover with each cell. The bigger the volume covered by each cell, the more you are taking different data points and averaging them to get one value for the the cell, so the \"fuzzier\" the pictures that result. \n", + "\n", + "How big are the spreadsheets? 30 years ago, the \"high resolution\" spreadsheet covering North America had each cell was 150 km. on a side, At that time, computers were not big enough to cover the whole world. Today, the high resolution models are around 10km on a side (100 sq. km.) As the area of the earth is around 500 million sq. km. that means that the grid today should be about 5 million cells. per level, models typically have 25 levels representing different heights of air in the atmosphere, so that means 125 million cells to calculate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9881d7f4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1753: UserWarning:\n", + "\n", + "Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.\n", + "\n", + "/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1754: UserWarning:\n", + "\n", + "Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "12:05:00", + "12:45:00", + "13:05:00", + "13:45:00" + ], + "hovertemplate": "Start=%{base}
Finish=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 2400000, + 1200000, + 2400000, + 1500000 + ], + "xaxis": "x", + "y": [ + "Acquisition", + "Assimilation", + "Model", + "Services" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Acquisition\", Start='12:05:00', Finish='12:45:00'),\n", + " dict(Task=\"Assimilation\", Start='12:45:00', Finish='13:05:00'),\n", + " dict(Task=\"Model\", Start='13:05:00', Finish='13:45:00'),\n", + " dict(Task=\"Services\", Start='13:45:00', Finish='14:10:00')\n", + " \n", + "])\n", + "\n", + "fig = px.timeline(df, x_start=\"Start\", x_end=\"Finish\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3fdd3540", + "metadata": {}, + "source": [ + "Each cell has information about different phenomena, called variables, say 25 of them. so to get a full picture, one needs 125 million x 25 variables x 8 bytes per variable = 6.2 billion variables in about 50 gbytes of memory. To calculate the what the values of those variables are at the next unit in time involve many calculations using that data, and the prediction's accuracy involves some tradeoffs:\n", + "\n", + "The simpler the model, the simpler the math, the less calculation time to get a result. The more complicated the model & math, the better the result, but the longer it takes to do." + ] + }, + { + "cell_type": "markdown", + "id": "64fb191a", + "metadata": {}, + "source": [ + "* https://en.wikipedia.org/wiki/History_of_numerical_weather_prediction#cite_note-RFE-43 note on Weather model from 1989\n", + "\n", + "* \n", + "https://devops.com/dont-build-microservices-pursue-loose-coupling/\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "958fed92", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import matplotlib.dates as mdates\n", + "from datetime import datetime\n", + "\n", + "try:\n", + " # Try to fetch a list of Matplotlib releases and their dates\n", + " # from https://api.github.com/repos/matplotlib/matplotlib/releases\n", + " import urllib.request\n", + " import json\n", + "\n", + " url = 'https://api.github.com/repos/matplotlib/matplotlib/releases'\n", + " url += '?per_page=100'\n", + " data = json.loads(urllib.request.urlopen(url, timeout=1).read().decode())\n", + "\n", + " dates = []\n", + " names = []\n", + " for item in data:\n", + " if 'rc' not in item['tag_name'] and 'b' not in item['tag_name']:\n", + " dates.append(item['published_at'].split(\"T\")[0])\n", + " names.append(item['tag_name'])\n", + " # Convert date strings (e.g. 2014-10-18) to datetime\n", + " dates = [datetime.strptime(d, \"%Y-%m-%d\") for d in dates]\n", + "\n", + "except Exception:\n", + " # In case the above fails, e.g. because of missing internet connection\n", + " # use the following lists as fallback.\n", + " names = ['v2.2.4', 'v3.0.3', 'v3.0.2', 'v3.0.1', 'v3.0.0', 'v2.2.3',\n", + " 'v2.2.2', 'v2.2.1', 'v2.2.0', 'v2.1.2', 'v2.1.1', 'v2.1.0',\n", + " 'v2.0.2', 'v2.0.1', 'v2.0.0', 'v1.5.3', 'v1.5.2', 'v1.5.1',\n", + " 'v1.5.0', 'v1.4.3', 'v1.4.2', 'v1.4.1', 'v1.4.0']\n", + "\n", + " dates = ['2019-02-26', '2019-02-26', '2018-11-10', '2018-11-10',\n", + " '2018-09-18', '2018-08-10', '2018-03-17', '2018-03-16',\n", + " '2018-03-06', '2018-01-18', '2017-12-10', '2017-10-07',\n", + " '2017-05-10', '2017-05-02', '2017-01-17', '2016-09-09',\n", + " '2016-07-03', '2016-01-10', '2015-10-29', '2015-02-16',\n", + " '2014-10-26', '2014-10-18', '2014-08-26']\n", + "\n", + " # Convert date strings (e.g. 2014-10-18) to datetime\n", + " dates = [datetime.strptime(d, \"%Y-%m-%d\") for d in dates]\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "69ddd6c2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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qC+fAgMDdW7jcePGadYpnU/llbt8+bKI6M6nnJwciYyMFFdXV1GpVDJo0CC5evWqNYetU+ltacnY6NtnSu6fpWOTnJwsAwcOFE9PT7G3txdvb28ZM2aMnDt3ztqh0CjKypLTPr7iYVerTByWLVsm7dq1EycnJ3FxcZHOnTvLypUrtb7O3NZypDR9OWJObGbPnGlyjohU3+Nxyf0pPzPTYnHav3+/BAQESL169aR27drSqlUref3117UeN9QVp9Kq42TPkvnE87th5ca7TRtRKRRSR6mUzh07GnVsy8jIkPHjx0v9+vXF1dVVhgwZonWcqsxjfE2/NqkqCpH//5wAVRvF2dn4s8s/t919T8RD6eRUxT2iBw1z0HZwW5bFmFQeW4u1rY3HkAdtvLaupm/Pmt7/qsI/vUBERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHpGJirOz8UfrNvijdRsUZ2dXdXeohmM+lY/xYQzKw9gYh3EyDuNUuRjvysHJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42atEqampGDNmDHx9faFUKjF9+nSTyqelpcHb2xsKhQJ3794td92IiAi0bNkSKpUKHh4eCAsLw7lz58zvPJGJtm3bhqCgIHh4eMDFxQUBAQHYtWuXwXJXr15FaGgonJ2d4e7ujqlTpyI/P7/cMqtXr0afPn3g4uJi1P5RnWz77js8d+0qeiZdQP1GjYyK08mTJzF69Gg0bdoUKpUKbdq0wdKlSw22tToqqsbGqTzm5Nq53Fy8kpKM5j4+JsVQTUQwcOBAKBQKfPvttxUcQdU5fPgwevbsCTc3N6hUKrRu3RqffPKJwXLTpk2Dv78/HB0d0alTJ6PamjRlSqWcl8wZU1paGoKDg+Hl5QVHR0c0bdoUkZGRyMzM1FsmPT0dU6ZMga+vL5ycnNCsWTNMnToVGRkZlh5SpdAVtyWffmp0eV6jWIa5+yQArF27Fn5+fqhduzYaNWqEyMjIctevyedOMh4ne5UoLy8PHh4emDVrFjp27Ghy+eeeew5+fn5Grevv74/o6Gj88ccf2LVrF0QE/fv3R1FRkcntEpnj4MGDCAoKwo8//oj4+Hg8/vjjCA0NRUJCgt4yRUVFCAkJQVZWFg4fPoxNmzZh69atmDFjRrltZWdnIzg4GDNnzrT0MKzu0OHDeMzJGauaeOPY4cNGxSk+Ph4eHh5Yv349EhMTMWvWLLz55ptYvnx5uW3l1OA4lcecXEvMy0UDu1pY99//mhRDtSVLlkChUFhqCFXG2dkZkZGROHjwIP744w+89dZbeOutt7B69epyy4kIxo8fj5EjRxrdVpfOnSvlvGTOmJRKJcLCwrB9+3acP38ea9euxd69ezFp0iS9ZVJSU5GSkoJFixbh9OnTWLt2LXbu3InnnnvOouOpLLri9va8edhs5ASA1yiWYe4++fHHH2PWrFl44403kJiYiH379mHAgAHllqnJ504ygZDFrFq1Sry8vKSoqEhreWhoqIwdO1ZrWWBgoEybNk1nPUVZWXLWt7Wc9W0tRVlZIiKycuVKCQwMlH379gkAuXPnjkl9O3nypACQpKQkk8pRWbq2j60xZoym5Lta27Zt5Z133tHb7o8//ihKpVKSk5M1y2JiYsTR0VEyMjIM9js2Ntas/cOaDMVJV6wNxUmXF154QR5//PEyy3XVXx3jpE9RVpbMadhQPGvVkoJ797ReMzfX9OW3vhiW9vvvv4u3t7ekpqYKAPnmm2+MH5AZKnLMMWc/HTJkiDz11FNG1T9nzhzp2LGj3tfL67u556WSYypZ/6B//9siY1JbunSpeHt7Gz0eEZHNmzeLg4ODFBQUmNSWtZm7H/1n8GAJdXExmHu2co1SGed3a+yT6enpolKpZO/eveW2rW98VXVOMDXeD8L1lzXwzp4FDR8+HLdv30ZsbKxm2Z07d7Br1y6Eh4ebXe/Zs2cxb948rFu3Dkql6ZssKysL0dHRaNGiBZo2bWp2P4hKMjXfi4uLce/ePbi6uuqt88iRI2jfvj28vLw0ywYMGIC8vDzEx8dbdgCVxBpx0iUjI8PkMjXFgLouuFNUhNgDBzTLqiqG2dnZGD16NJYvX45GjRqZVH9VMDX/EhISEBcXh8DAQKv2qyLnJV1jyigqwu69ey02ppSUFGzbts3kOGRkZMDFxQW1atUyqVxlMHU/SkhIwJFff0U3lVO59fIaxTTW2Cf37NmD4uJiJCcno02bNvD29saIESNw7do1q4yBahZO9izI1dUVwcHB2Lhxo2bZli1b4Orqin79+plVZ15eHkaPHo2PPvoIzZo1M6nsypUrUadOHdSpUwc7d+7Enj174ODgYFY/iEozNd8XL16MrKwsjBgxQm+dN27cQMOGDbWWNWjQAA4ODrhx44blOl+JrBGn0o4cOYLNmzcjIiLCIn2uburb2eFfzs6I2bxZs6yqYvjSSy/hscceQ1hYmPEDqELG5p+3tzccHR3RtWtXvPjii5gwYYJV+mOJ85KuMe26dw+uDRpUeEyjR4+Gk5MTmjRpAhcXF3z++edG9ystLQ3z58+vtvuhsftRybi9EBGBYfXr662T1yims8Y+eenSJRQXF2PhwoVYsmQJvv76a6SnpyMoKMjgZ97J9nGyZ2Hh4eHYunUr8vLyAAAbNmzAqFGjYGdnZ1Z9M2fPRps2bfDUU0+Z1ZeEhAQcOHAArVq1wogRI5Cbm2tWP4h0MTbfY2JiMHfuXHz11Vfw9PQst05dn4MSkRr9+Sij47R5s9FxUktMTERYWBhmz56NoKAgi/e9uhhU1wXbvvvOormmZmwMt2/fjv3792PJkiVmj6MqGJN/hw4dwvHjx7Fq1SosWbIEMTExVuuLJc5Lpce0IzMDI4cNq/CYPvnkE5w4cQLffvstLl68iJdfftmo/mRmZiIkJARt27bFnDlzTB5PZTFmPyoZt6UrVuCHcr6k5s033+Q1ihksvU8WFxejoKAAy5Ytw4ABA9CjRw/ExMTgwoULWncQ6QFV1c+R2prs7GypW7eubN26Va5evSoKhUKOHz9eZj1jP7PXsUMHUSqVYmdnJ3Z2dqJUKgWA2NnZyezZs43uV15enjg5OcnGjRvNHRr9fw/CM+PGjtGYfN+0aZOoVCrZsWOHwXbffvtt8fPz01qWnp4uAGT//v0Gy1fXz6KVFyd1rBc39jI6TmqJiYni6ekpM2fO1LuOLXxm76xvaznRysdiuVYyJqePHzcYQ7Vp06aJQqHQHI/t7OwEgCiVSgkMDKzIMI3urznHHGPPS2rz588XHx8fo+quyGf2KnJeUo9py8aNsu/hlqIA5LfDh/Wub8qY1A4dOiQAJCUlRbNM13gyMzMlICBA+vXrJzk5OSaPpTKYsh+VNG/2bHnI3kFv7nXs2NGmrlEq6/xu6X0yKipKAMi1a9e0lnt6esrq1as1v/Mzew+m6vdQeQ2nUqkwdOhQbNiwAUlJSfDx8YG/v7/Z9W3ZuBF5JX4/duwYxo8fj0OHDqFly5Ym1SUimneRiCzBUL7HxMRg/PjxiImJQUhIiMH6AgICsGDBAqSmpqJx48YAgN27d8PR0bFC+1FVMxSnHzIz8daNVGw0Mk7AP3ej+vbti3HjxmHBggXW6nq1UVupxJDBgy2WawBwIS8Pzw8ciHHPPGNUDN94440yj1J16NABn3zyCUJDQ00bUCUy9bxUmecKc9tSj2njV1/h4cxMPOTgAP/OnS3ajogAQLnlMjMzMWDAADg6OmL79u2oXbu2SW1UNkP7UWkignwp1vv61q1bkZOTo/md1yjGsfQ+2bNnTwDAn3/+CW9vbwD//GmQ27dvo3nz5pbtPNU8VTfPtF27d+8WR0dH8fX1lfnz52u9lpCQIAkJCeLv7y9jxoyRhIQESUxM1Ly+bds28fXx0fvOha53X65fvy6+vr5y9OhRERG5ePGiLFy4UI4fPy5XrlyRuLg4CQsLE1dXV7l586b1Bv6AeBDeWTJljPryfePGjVKrVi1ZsWKFpKaman7u3r2rWWfbtm3i6+ur+b2wsFDat28v/fr1kxMnTsjevXvF29tbIiMjNeuUzncRkdTUVElISJA1a9YIADl48KAkJCRIWlqaJcJhEfritD46WmoB8rZnQ0m+eNGoOJ05c0Y8PDwkPDxcK7a3bt3SrKOO05EDBzTbMvnixWofp9JK5uLO7dstkmtFWVny3UMtxNXOTsaMHGkwhiVzrTRU82/jVNOXf8uXL5ft27fL+fPn5fz58xIVFSUuLi4ya9YszTql809E5MKFC5KQkCARERHi4+OjObfl5eWJSNn829XiYVkwd65Fz0vqMbVwcJCp7u6a2Jgzph9++EGioqLk9OnTcvnyZfnhhx+kXbt20rNnT806169fF18fH9nUrLmc9W0td2/ckO7du0uHDh0kKSlJK48KCwvNGpO1GLMf6YtbhKubJvd05UJJNf0apTLP75beJ8PCwqRdu3byyy+/yOnTp2XQoEHStm1byc/PF5Gy+VuUlVXl507e2ascnOxZQWFhoTRu3FgAyMWLF7VeA1Dmp3nz5prXo6OjBYBJk73Lly8LAImNjRURkeTkZBk4cKB4enqKvb29eHt7y5gxY+TcuXPWGvID5UE42JgyRn35HhgYqDPfx40bp1lHne8lXblyRUJCQkSlUomrq6tERkZKbm6u5vXS+S7yz6NkutqKjo6uUBwsSW+cevUyOU76xlvyWKKO076fftJsy9kzZ1b7OJVWMhfzMzMtkmtFWVnygpub0TEsmWul1ZTJnr78W7ZsmbRr106cnJzExcVFOnfuLCtXrtT6Wnhd+6m+mF++fFlEyubfzy1bSnD//hY9LxUWFkrjRo0EgOxq8bAmNuaMaf/+/RIQECD16tWT2rVrS6tWreT111/Xea5d27SpnPVtLft++klnDErGobowZj/SFbflS5bIGR9fTe7pyoWSavo1SmWe3y29T2ZkZMj48eOlfv364urqKkOGDJGrV69qXi+dv0VZWVV+7uRkr3IoRP7/cwpUbRRnZ+PPLv/czvc9EQ+lU/lfe0yV60HYPg/CGKsLa8e6pm9La/S/psWkpvW3JFvL75q6Lcztd00dr7lsfbzVbXym9qe69b+m4LdxEhERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHKM7Oxh+t2+CP1m1QnJ1d1d0hsirmu3EYp4ph/CrGFuNni2OyNsaseuB2qNk42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENqhWVXfA1hUVFeHQoUNITU1F48aN0atXL9jZ2Rks82tWFn7LzoLrvHnoGxSEPn36GCxXkT7+lp2FvwuLkHrwIAKDgqzWVlUzZ3voqqO6x6ui46yqMVpi+xjTRmXuX8b0x9xYGxMvU+ovWZ9b3brYm3YbyYWF8F++HJHTp8PBwaHC4zWVMf03NW/y8/PxRXoarhUUWG1slmzD3G1orX3IFObmd3nxKzlGj3r1cC3rPtKKii16rNIXx/LGYyj25sTCUtvT3LZjf/4ZW/++BUCBobGx6BscXOH9z9r9NrZeU7dvZanqeOpqv1hPOWtfa5Suv0enTpW+farbMdUsQlazdetWeeihhwSA5uehhx6SrVu3llumoaenVplatWqJh4dHueUq1MfmzU3qY01lzvbQWUc1j1dFx1lVY7TE9jGmjcrcv4zpj7mxNiZeptSvq75atWpp/u/o6CivvvqqRcdviDH9NzVvXn31VXF0dNRa39Jjs2QbFd2GVXl8Mje/y4ufoTy1xHj1xfHVV1/VOx5DsTcnFpbanua23bBhQ4PHSmvmnLXOReZs38pS1fHU1X7Dhg3Fw8NDd7yseK2hqy+ljwvW3j7V7ZhqLk72rGTr1q2iUCgkNDRUjhw5Ivfu3ZMjR45IaGioKBQKvSdqhUIhgwYN0iozaNAgUSgUAsDiF76m9rGmssRYa0K8KtrHqhpjZbRb2fuXsf0xZ8zGlDWlfn3rqmPz3nvvyaBBgwRApU34LD1GkX8mEQB05oClxmbJNiyxDavq+GRufwzFD0C5eVrR8Za3LwCQrl276hyPrn6pX3v11VfNvh6o6Pa05rWINXPOWnWbu30rY/+p6njqWyckJESzf1kqXob6o2+fUefg+vXrrb59qtsxtSIUIiIgiyoqKsIjjzyCDh064Ntvv4VS+b+PRhYXF+M///kPTp8+jZMnT2o9/uHn54cOHTrgu+++01nm559/hqurK06dOlXhW8jq9vz8/IzuY01libHWhHhVtI9VNcbKaLey9y9j+2POmI0tW1xcjI4dOxqsH4DB+s6cOYM///wTQ4cOxZ49e5CammrVRzotPUY7Ozvk5+ejcePGCAoK0pkDYWFhFR6bJdswJUcAw9uwMo9P5ua3MfHbtWsXMjMzUbt27TJ1qvP0ySefNGu8hvodFhaGxMREXLhwQVNvcXExBg8ejIMHDyItLQ329vZl+rV79269YyrveqCi29Ocekw5VoqI0fufKax1TjB3+1bG/mPN86CljqdnzpzRxKYi8TKmP4b2GXVfFAqFVbaPMX0sGY9qr4onmzYpNjZWAMiRI0d0vh4XF6d1S7jkjzllKvJT2e1V5Y8lxloT4lXRPlbVGCuj3eq2/SrSH2PKmlK/oXVjY2MrPU6WHmNNzDNLbsOakt+Gyn3yySd6X7NEnhqzLxizvORrK1asMDkWltqe1rwWsWbOWatuc7dvdd5nLFW3KbGpaLwMtWVon1G3a83tY2quVFec7FnBxo0bBYDcu3dP5+uZmZl6E8ucMhX5qez2qvLHEmOtCfGqaB+raoyV0W51234V6Y8xZU2p39C6GzdurPQ4WXqMNTHPLLkNa0p+GyoXGRmp9zVL5Kkx+4Ixy0u+FhUVZXIsLLU9rXktYs2cs1bd5m7f6rzPWKpuU2JT0XgZasvQPqNu15rbx9Rcqa74pxesoHHjxgCAM2fO6HxdvfzHH3/E/fv3cf/+ffz4449GlSldztwfY9uzRFtV/WOJsdaEeFW0j1U1xspot7L3L2uO2ZSxGFO/sfU1btxY8//333+/WsTH2DHev38f77//vlHrV2RslmzDlBypbscnc/tjbPxatmyp97WSeWrqeE3ZF4xZXvK1nJwco2Nhqe1pTj2m7HuW6KOlj4/W3L7W3H+suQ9b8nhaMjbmxsvY/hjaZ0pfa1ty+5ibK9VWVc82bVFhYaE89NBDEhoaKkVFRVqvFRUVSWhoqLRo0UIKCwvLlAkJCdFbpm7duvLQQw9plavMPtZUlhhrTYhXRftYVWOsjHYre/8ytj/mjNnYss2bNzeqfmPry8/Pl0GDBknt2rUlLy/PQpHQzdJjFBHJy8sTR0dHGTRokM71LTE2S7ZhSo5Ut+OTuf0xJn729vaSk5Ojt878/Hyzx2uo34MGDSpTb1FRkYSEhEjdunUlPz9fZ7/KG1N51wMV3Z7WvhYxZf8zhbXy2dztWxn7jzX3YWscTysSL2P6Y+w+Y63tU92OqRXFyZ6VlPwWn7i4OMnMzJS4uDijvgErJCREq0xlfBunsX2sqSwx1poQr4r2sarGWBntVvb+ZWx/zBmzMWVNqV/fuurYLFy4sEq/jdMSYxTR/qbH0uO01Ngs2YYltmF1+DZOU/pjKH4Ays1TS35bo672u3btqnM8uvql65sFTb0eqOj2tOa1iDVzzlp1m7t9K/vbOKsinvrWUX8b58KFCy0WL0P90bfPlPw2Tmtvn+p2TK0ITvasSNffEGnRooXBizhdf9vG09PTaslc+m+IGOpjTWXO9tBZRzWPV0XHWVVjtMT2MaaNyty/jOmPubE2Jl6m1K+rvpJ/v6x27dpV83f2DPTf1LzR9TfcLD02S7ZR0W1Ylccnc/O7vPgZylNLjFdfHHX9XTF1e4Zib04sLLU9zW1b198kLX2stGbOWetcZM72rSxVHU9d7Tdq1KjM39nTxMuK1xq6+lL6uGDt7VPdjqnm4p9esLKCe/ewsV07/F1YhC5R/0VgUJDBr2ktKipC7K5d2Pr00wAUGLruC/QNDrbqV/4eOnQIqampaNy4MXr16lUzvkrWDOZsj9JqQrwqOs6qGqMlto8hlb1/GdMfc2NtTLxMqb9kfe0/XYb9kyYhubAQ/m++icjp06365xb0Mab/puZN7t27mOvjg2sFBVYbmyXbMHcbWmsfMoW5+V1e/EqOsdPna6BwdMStW7cseqzSF8fyxmMo9ubEwlLb09y217dth9+ys+D6/PPoGxSEPn36VHj/s3a/jWHO9q0sVR1PXe0D0FnO2tcapev/V9++iIuLq9TtU92OqebgZM/KirOz8WcXfwCA74l4KJ2crFqOyvegxLWmjrOy+l1T41OapcdRsr5WvxzGhZ7/sljd1mRqHCpj+1dVjj0IuV1dt581+lWV29PYtmtizlXnPld130xp39p9repYVJc+VBS/jZOIiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2aBaVd0BosqkdHJCm3N/VHU3qIoxDwxTqlSMUQUwx6znQYrtgzTWysS4WgbjWDPwzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72yGpSU1MxZswY+Pr6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7dlQCu7d0zvW/fv3S0BAgNSrV09q164tze3tZYKrq6SV+LMxNTGPREyf7J328ZXGjRpZJIce5GuB/MxM8bCrJQDkwpkzmuXmxFFEJCMjQ8aPHy/169cXV1dXGTJkiFZMSsdx308/VZtzaXU4J1WHPlSUQkTUTxyQFRRnZ+PPLv/cgvc9EQ+lk5NVyxEBNTd/amq/q4ql41VT429qv2vqOI1hK2Or6nGY035V99nSjB2PrY27qlV1PKu6/erWl+rQh4riZ/aIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDapV1R2wdUonJ7Q590dVd4MeMMy7BwO38z8Yh/9hLCyDcWQMqgrj/j+MhWXwzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBNjXZS01NxZgxY+Dr6wulUonp06ebVD4tLQ3e3t5QKBS4e/duuevm5eVhypQpcHd3h7OzMwYPHozr16+b33mqsczNO4VCUeZn1apVetdPT0/HlClT4OvrCycnJzRr1gxTp05FRkaGhUZC5dm2bRuCgoLg4eEBFxcXBAQEYNeuXQbLTZs2Df7+/nB0dESnTp2Mamv16tXo06cPXFxcjDoeVabDhw+jZ8+ecHNzg0qlQuvWrfHJJ58YLHf16lWEhobC2dkZ7u7umDp1KvLz88stU52Ps+bGwdbyoaLMiePJkycxevRoNG3aFCqVCm3atMHSpUvLLVPdj5/m5pMar1/MY27cbe38XTIOzm5uCLl8CV+kp5dbZu3atTrjoFAocOvWLb3lmH+Vz6a+jTMvLw8eHh6YNWuWSQdJteeeew5+fn5ITk42uO706dPx/fffY9OmTXBzc8OMGTMwaNAgxMfHw87OzpzuUw1VkbyLjo5GcHCw5vd69erpXTclJQUpKSlYtGgR2rZtiytXrmDSpElISUnB119/bXb/yTgHDx5EUFAQFi5ciPr16yM6OhqhoaE4evQoOnfurLeciGD8+PE4evQoTp06ZVRb2dnZCA4ORnBwMN58801LDcEinJ2dERkZCT8/Pzg7O+Pw4cOIiIiAs7MzJk6cqLNMUVERQkJC4OHhgcOHDyMtLQ3jxo2DiODTTz/V21Z1Ps6aEwfA9vKhovTFUWVvj0A9ZeLj4+Hh4YH169ejadOmiIuLw8SJE2FnZ4fIyEidZar78dPcfFLj9Yt5KhJ3Wzp/l4yDSqnElr798M7NG3goKgqT9OxTI0eO1Bo/ADzzzDPIzc2Fp6en3raYf1VAapBVq1aJl5eXFBUVaS0PDQ2VsWPHai0LDAyUadOmGV33ypUrJTAwUPbt2ycA5M6dO3rXvXv3rtjb28umTZs0y5KTk0WpVMrOnTuNbrM8RVlZcta3tZz1bS1FWVkWqZPMY628AyDffPNNhfq2efNmcXBwkIKCggrVU11UZd6bsp3V2rZtK++8845R9c+ZM0c6duxoUp9iY2MNHo8sqSgrS+Y0bCietWpJwb17Wq+VF4chQ4bIU089pbfeH3/8UZRKpSQnJ2uWxcTEiKOjo2RkZOgsY83jrDF5Zk4+GIpDSTUhHyzB3DiGjxpl0rHghRdekMcff9ykvln6+FleXlkzn6r6+qU6X69YK+417fxtShzU2/OJOnUkfNQoo9u4deuW2Nvby7p16/SuUxnXz5ZWnfPbWDXqMc7hw4fj9u3biI2N1Sy7c+cOdu3ahfDwcLPrPXv2LObNm4d169ZBqTQckvj4eBQUFKB///6aZV5eXmjfvj3i4uLM7gdVT9bKOwCIjIyEu7s7unXrhlWrVqG4uNik8hkZGXBxcUGtWjZ1k75KmLqdi4uLce/ePbi6ulZmN61uQF0X3CkqQuyBA5pl5cUhISEBcXFxCAzUdx8GOHLkCNq3bw8vL6//tTNgAPLy8hAfH6+zTFUfZ03NB2Pi8CAyN469e/UyqZ2MjAyT98XKPH5aK594/VI+a+7HNen8bWoczubmIiEnx6T9cN26dXBycsKwYcP0rvOg5V91UaMme66urggODsbGjRs1y7Zs2QJXV1f069fPrDrz8vIwevRofPTRR2jWrJlRZW7cuAEHBwc0aNBAa3nDhg1x48YNs/pB1Zc18g4A5s+fjy1btmDv3r0YNWoUZsyYgYULFxpdPi0tDfPnz0dERITZfaD/MXU7L168GFlZWRgxYkRldtPq6tvZ4V/OzojZvFmzTFccvL294ejoiK5du+LFF1/EhAkT9NZ548YNNGzYUGtZgwYN4ODgoPeYWdXHWWPzwZQ4PIjMjuMzzxjdxpEjR7B582aTjoWVffy0Rj7x+sUwa+3HNe38bUocVA0aYMSVvzCmfgOT9sOoqCiMGTMGKpVK7zoPWv5VFzVqsgcA4eHh2Lp1K/Ly8gAAGzZswKhRo8x+zvfNN99EmzZt8NRTT1W4byIChUJR4Xqo+rF03gHAW2+9hYCAAHTq1AkzZszAvHnz8NFHHxlVNjMzEyEhIWjbti3mzJljdh9Im7HbOSYmBnPnzsVXX31V7mcTaqpBdV2w7bvvyo3DoUOHcPz4caxatQpLlixBTExMuXXqOjaac8yszOOsMflgahweRGbFscSbDeVJTExEWFgYZs+ejaCgIKPKVNXx09L5xOsX41hjP66J529j4/DboUOY07AR1t1JN3o/PHLkCM6ePYvnnnvOrL7Zcv5VC1X6EKkZsrOzpW7durJ161a5evWqKBQKOX78eJn1jP3sVMeOHUWpVIqdnZ3Y2dmJUqkUAGJnZyezZ8/WWUb9XHx6errWcj8/P71lTGULzwjbEkvnnS6HDx8WAHLjxo1y18vMzJSAgADp16+f5OTkmNVWdVXVeW/Mdt60aZOoVCrZsWOHSXXXhM9oqeN/opWPUfmuNn/+fPHx8dH7+ttvvy1+fn5ay9LT0wWA7N+/X2cZax5njc0zY/d7NUNxKKkm5IOlmBXHVq0MbqPExETx9PSUmTNnGt0Xax4/DeWVpfOpuly/VPVx2xBr7sdqNeH8bWwc1Ntzqru7+LRqZVTd48ePl06dOhlcrzKuny2tuue3MWrcB31UKhWGDh2KDRs2ICkpCT4+PvD39ze7vq1btyInJ0fz+7FjxzB+/HgcOnQILVu21FnG398f9vb22LNnj+YRrtTUVJw5cwYffvih2X2h6svSeadLQkICateujfr16+tdJzMzEwMGDICjoyO2b9+O2rVrW7QPDzpD2zkmJgbjx49HTEwMQkJCqrCn1lVbqcSQwYONzncR0bxbrEtAQAAWLFiA1NRUNG7cGACwe/duODo66q23OhxnTd3vDcXhQWV2HFVOetdJTExE3759MW7cOCxYsMCoflT18dPS+cTrF+NUxn5cE87fpscBRsXh/v372Lx5M9577z2D6z6I+VctVOlU00y7d+8WR0dH8fX1lfnz52u9lpCQIAkJCeLv7y9jxoyRhIQESUxM1Ly+bds28fX11Vu3rndOr1+/Lr6+vnL06FHNskmTJom3t7fs3btXTpw4IX379pWOHTtKYWGhRcZoC+8k2BpL5t327dtl9erVcvr0aUlKSpI1a9aIi4uLTJ06VbNO6bzLzMyU7t27S4cOHSQpKUlSU1M1P5bKu6pWHfJe33beuHGj1KpVS1asWKEV+7t372rW0XV8uXDhgiQkJEhERIT4+PhociUvL09EdB9fUlNTJSEhQdasWSMA5ODBg5KQkCBpaWlWHXvJ+O/cvl1nHJYvXy7bt2+X8+fPy/nz5yUqKkpcXFxk1qxZeuNQWFgo7du3l379+smJEydk79694u3tLZGRkZp1KvM4a0qe6csHc+IgUrPywZJMjePM117TbKOvY2K04njmzBnx8PCQ8PBwrX3x1q1bmnWq4vhpTF5ZOp9Kqqrrl+pw3DbEknGvyedvY+Jw7uRJebdRI6mjVMrM117TrKMv/z7//HOpXbt2mbt1IlVz/WxpNSG/DamRk73CwkJp3LixAJCLFy9qvQagzE/z5s01r0dHR0t5c1xdB8vLly8LAImNjdUsy8nJkcjISHF1dRWVSiWDBg2Sq1evWmqINpFctsaSeffTTz9Jp06dpE6dOuLk5CTt27eXJUuWaH0Fc+m8U+emrp/Lly9bc+iVpjrkvb7tHBgYqDP248aN06yj6/iir5x6m+k6vsyZM0dnmejoaCuOXDv++ZmZOuOwbNkyadeunTg5OYmLi4t07txZVq5cqfWV3rricOXKFQkJCRGVSiWurq4SGRkpubm5mtcr8zhrSp7pywdz41CT8sGSTI1jwb17mm3031WrtOKoLx4lj7lVcfw0Jq8snU8lVdX1S3U4bhtiybjX5PO3sXFo4+gosz0bav0JHn35FxAQIGPGjNHZXlVcP1taTchvQxQiIkbeBKRKVJydjT+7/HN73fdEPJRO+h9nIbIVzPuq9aDE/0EZZ01WE7dRTeyzJTyo47ZV3J7abCEeNe7bOImIiIiIiMgwTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbpBARqepOEBERERERkWXxzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72AKSmpmLMmDHw9fWFUqnE9OnTTSqflpYGb29vKBQK3L17V+966enpmDJlCnx9feHk5IRmzZph6tSpyMjIqNgAiMywbds2BAUFwcPDAy4uLggICMCuXbsMlrt69SpCQ0Ph7OwMd3d3TJ06Ffn5+eWWycvLw5QpU+Du7g5nZ2cMHjwY169ft9RQaiRz4n/y5EmMHj0aTZs2hUqlQps2bbB06VKDbVVl/A8fPoyePXvCzc0NKpUKrVu3xieffGKwnKl5xuOreSozD/v06QOFQqH1M2rUKLP6bU5epaWlITg4GF5eXnB0dETTpk0RGRmJzMzMcsutXr0affr0gYuLi8HzvLWZuz+p8XqleqnI9ly7di38/PxQu3ZtNGrUCJGRkeWuXxPOw+bEY+3atWWOK+qfW7du6SzzoOV3raruQHWQl5cHDw8PzJo1y6SDptpzzz0HPz8/JCcnl7teSkoKUlJSsGjRIrRt2xZXrlzBpEmTkJKSgq+//trc7hOZ5eDBgwgKCsLChQtRv359REdHIzQ0FEePHkXnzp11likqKkJI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8+Y6y1Sn87C19uv79+9j8+bNeO+994zqxwOV31U506xsu3fvFkdHR/H19S3zyElCQoIkJCSIv7+/jBkzRhISEiQxMVHz+rZt28TX11dv3bre8bt+/br4+vrK0aNHReSfdxC6d+8uHTp0kKSkJElNTdX8FBYWWnawRP+fvrzfuHGj1KpVS1asWKGVi3fv3tWsUzrvCwsLpX379tKvXz85ceKE7N27V7y9vSUyMlKzTum8FxGZNGmSeHt7y969e+XEiRPSt29f6dix4wOR95aMv/qRufDwcK0yt27d0qxTVfHXN87ly5fL9u3b5fz583L+/HmJiooSFxcXmTVrlt5xmpNnPL6Wr6rzMCkpSd555x05duyYXL58WX744Qdp3bq1dO7cudztY8m8+uGHHyQqKkpOnz6t6UO7du2kZ8+eevst8s9jnwkJCbJmzRoBIAcPHpSEhARJS0szOv6msuS4S+P1SuWz9PYMCwuTdu3ayS+//CKnT5+WQYMGSdu2bTWPIVb387A18vvzzz+X2rVrl7l7KcL8fqAme4WFhdK4cWMBIBcvXtR6DUCZn+bNm2tej46OlvLmxroOnpcvXxYAEhsbq7WOrp/Lly9bcKRE/6Mv7wMDA3Xm4rhx4zTr6Mr7K1euSEhIiKhUKnF1dZXIyEjNs/ciZfNeRCQnJ0ciIyPF1dVVVCqVDBo0SK5evWq1MVcnloz/nDlzDB6rqir++sa5bNkyadeunTg5OYmLi4t07txZVq5cqfXV25bIMx5fy1fVeXj16lXp3bu3uLq6ioODg7Rs2VKmTp1qcMJkybzav3+/BAQESL169aR27drSqlUref3118s9b5c33ujoaANRN5+l96eSeL1S+Sy9PTMyMmT8+PFSv359cXV1lSFDhmgd06v7edga+R0QECBjxozR2d6Dnt8Kkf//DAMRERERERHZjAfiTy8QERERERE9aDjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZINqVXUHLE1EcO/evaruBhERERERkVXVrVsXCoVC7+s2N9m7d+8e6tWrV9XdICIiIiIisqqMjAy4uLjofV0hIlKJ/bE6S9zZy8zMRNOmTXHt2rVyg2dJld2mrbdXFW3aensPSpu2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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Choose some nice levels\n", + "levels = np.tile([-5, 5, -3, 3, -1, 1],\n", + " int(np.ceil(len(dates)/6)))[:len(dates)]\n", + "\n", + "# Create figure and plot a stem plot with the date\n", + "fig, ax = plt.subplots(figsize=(8.8, 4), layout=\"constrained\")\n", + "ax.set(title=\"Matplotlib release dates\")\n", + "\n", + "ax.vlines(dates, 0, levels, color=\"tab:red\") # The vertical stems.\n", + "ax.plot(dates, np.zeros_like(dates), \"-o\",\n", + " color=\"k\", markerfacecolor=\"w\") # Baseline and markers on it.\n", + "\n", + "# annotate lines\n", + "for d, l, r in zip(dates, levels, names):\n", + " ax.annotate(r, xy=(d, l),\n", + " xytext=(-3, np.sign(l)*3), textcoords=\"offset points\",\n", + " horizontalalignment=\"right\",\n", + " verticalalignment=\"bottom\" if l > 0 else \"top\")\n", + "\n", + "# format x-axis with 4-month intervals\n", + "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4))\n", + "ax.xaxis.set_major_formatter(mdates.DateFormatter(\"%b %Y\"))\n", + "plt.setp(ax.get_xticklabels(), rotation=30, ha=\"right\")\n", + "\n", + "# remove y-axis and spines\n", + "ax.yaxis.set_visible(False)\n", + "ax.spines[[\"left\", \"top\", \"right\"]].set_visible(False)\n", + "\n", + "ax.margins(y=0.1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d38bd681", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import datetime as dt\n", + "\n", + "# from https://www.datacamp.com/tutorial/how-to-make-gantt-chart-in-python-matplotlib\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6cc70c33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " task team start end completion_frac\n", + "0 A R&D 2022-10-20 2022-10-31 1.00\n", + "1 B Accounting 2022-10-24 2022-10-28 1.00\n", + "2 C Sales 2022-10-26 2022-10-31 1.00\n", + "3 D Sales 2022-10-31 2022-11-08 1.00\n", + "4 E IT 2022-11-03 2022-11-09 1.00\n", + "5 F R&D 2022-11-07 2022-11-18 0.95\n", + "6 G IT 2022-11-10 2022-11-17 0.70\n", + "7 H Sales 2022-11-14 2022-11-22 0.35\n", + "8 I Accounting 2022-11-18 2022-11-23 0.10\n", + "9 J Accounting 2022-11-23 2022-12-01 0.00\n", + "10 K Sales 2022-11-28 2022-12-05 0.00\n", + "11 L IT 2022-11-30 2022-12-05 0.00\n" + ] + } + ], + "source": [ + "df = pd.DataFrame({'task': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'],\n", + " 'team': ['R&D', 'Accounting', 'Sales', 'Sales', 'IT', 'R&D', 'IT', 'Sales', 'Accounting', 'Accounting', 'Sales', 'IT'],\n", + " 'start': pd.to_datetime(['20 Oct 2022', '24 Oct 2022', '26 Oct 2022', '31 Oct 2022', '3 Nov 2022', '7 Nov 2022', '10 Nov 2022', '14 Nov 2022', '18 Nov 2022', '23 Nov 2022', '28 Nov 2022', '30 Nov 2022']),\n", + " 'end': pd.to_datetime(['31 Oct 2022', '28 Oct 2022', '31 Oct 2022', '8 Nov 2022', '9 Nov 2022', '18 Nov 2022', '17 Nov 2022', '22 Nov 2022', '23 Nov 2022', '1 Dec 2022', '5 Dec 2022', '5 Dec 2022']),\n", + " 'completion_frac': [1, 1, 1, 1, 1, 0.95, 0.7, 0.35, 0.1, 0, 0, 0]})\n", + "print(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2bea7be6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " task team start end completion_frac days_to_start \\\n", + "0 A R&D 2022-10-20 2022-10-31 1.00 0 \n", + "1 B Accounting 2022-10-24 2022-10-28 1.00 4 \n", + "2 C Sales 2022-10-26 2022-10-31 1.00 6 \n", + "3 D Sales 2022-10-31 2022-11-08 1.00 11 \n", + "4 E IT 2022-11-03 2022-11-09 1.00 14 \n", + "5 F R&D 2022-11-07 2022-11-18 0.95 18 \n", + "6 G IT 2022-11-10 2022-11-17 0.70 21 \n", + "7 H Sales 2022-11-14 2022-11-22 0.35 25 \n", + "8 I Accounting 2022-11-18 2022-11-23 0.10 29 \n", + "9 J Accounting 2022-11-23 2022-12-01 0.00 34 \n", + "10 K Sales 2022-11-28 2022-12-05 0.00 39 \n", + "11 L IT 2022-11-30 2022-12-05 0.00 41 \n", + "\n", + " days_to_end task_duration completion_days \n", + "0 11 12 12.00 \n", + "1 8 5 5.00 \n", + "2 11 6 6.00 \n", + "3 19 9 9.00 \n", + "4 20 7 7.00 \n", + "5 29 12 11.40 \n", + "6 28 8 5.60 \n", + "7 33 9 3.15 \n", + "8 34 6 0.60 \n", + "9 42 9 0.00 \n", + "10 46 8 0.00 \n", + "11 46 6 0.00 \n" + ] + } + ], + "source": [ + "df['days_to_start'] = (df['start'] - df['start'].min()).dt.days\n", + "df['days_to_end'] = (df['end'] - df['start'].min()).dt.days\n", + "df['task_duration'] = df['days_to_end'] - df['days_to_start'] + 1 # to include also the end date\n", + "df['completion_days'] = df['completion_frac'] * df['task_duration']\n", + "print(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "de5e70ba", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bf7f4335", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "2009-01-01", + "2009-03-05", + "2009-02-20" + ], + "hovertemplate": "Start=%{base}
Finish=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 5011200000, + 3542400000, + 8553600000 + ], + "xaxis": "x", + "y": [ + "Job A", + "Job B", + "Job C" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Job A\", Start='2009-01-01', Finish='2009-02-28'),\n", + " dict(Task=\"Job B\", Start='2009-03-05', Finish='2009-04-15'),\n", + " dict(Task=\"Job C\", Start='2009-02-20', Finish='2009-05-30')\n", + "])\n", + "\n", + "fig = px.timeline(df, x_start=\"Start\", x_end=\"Finish\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "663b0fcd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Task Start_time Duration End_time \\\n", + "0 Acquisition 2023-09-08 12:05:00 0 days 00:40:00 2023-09-08 12:45:00 \n", + "1 GOES images T-12 2023-09-08 12:05:00 0 days 00:05:00 2023-09-08 12:10:00 \n", + "2 GOES images T-11 2023-09-08 12:10:00 0 days 00:05:00 2023-09-08 12:15:00 \n", + "3 GOES images T-10 2023-09-08 12:15:00 0 days 00:05:00 2023-09-08 12:20:00 \n", + "4 GOES images T-09 2023-09-08 12:20:00 0 days 00:05:00 2023-09-08 12:25:00 \n", + "5 Assimilation 2023-09-08 12:45:00 0 days 00:30:00 2023-09-08 13:15:00 \n", + "6 Model 2023-09-08 13:15:00 0 days 00:45:00 2023-09-08 14:00:00 \n", + "\n", + " Start_with Start_after \n", + "0 NaN NaN \n", + "1 Acquisition NaN \n", + "2 NaN GOES images T-12 \n", + "3 NaN GOES images T-11 \n", + "4 NaN GOES images T-10 \n", + "5 NaN Acquisition \n", + "6 NaN Assimilation \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/peter/.local/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:\n", + "\n", + "The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "2023-09-08T12:05:00", + "2023-09-08T12:05:00", + "2023-09-08T12:10:00", + "2023-09-08T12:15:00", + "2023-09-08T12:20:00", + "2023-09-08T12:45:00", + "2023-09-08T13:15:00" + ], + "hovertemplate": "Start_time=%{base}
End_time=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 2400000, + 300000, + 300000, + 300000, + 300000, + 1800000, + 2700000 + ], + "xaxis": "x", + "y": [ + "Acquisition", + "GOES images T-12", + "GOES images T-11", + "GOES images T-10", + "GOES images T-09", + "Assimilation", + "Model" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import copy\n", + "import pandas as pd\n", + "import datetime as dt\n", + "\n", + "today=dt.datetime.now()\n", + "first_task_start=dt.time(hour=12,minute=5)\n", + "time_of_start=today.combine(today,first_task_start)\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Acquisition\", Start_time=time_of_start, Duration='00:40:00', End_time=today),\n", + " dict(Task=\"GOES images T-12\", Start_with='Acquisition', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-11\", Start_after='GOES images T-12', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-10\", Start_after='GOES images T-11', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-09\", Start_after='GOES images T-10', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"Assimilation\", Start_after='Acquisition', Duration='00:30:00'),\n", + " dict(Task=\"Model\", Start_after='Assimilation', Duration='00:45:00')\n", + "])\n", + "\n", + "df['Duration']=pd.to_timedelta(df['Duration'])\n", + "\n", + "for ti in df.index:\n", + " if pd.isna(df['Start_time'][ti]):\n", + " for predecessor in df.index:\n", + " if df['Task'][predecessor] == df['Start_after'][ti]:\n", + " df.loc[ti, 'Start_time'] = df['End_time'][predecessor]\n", + " elif df['Task'][predecessor] == df['Start_with'][ti]:\n", + " df.loc[ti, 'Start_time'] = df['Start_time'][predecessor]\n", + " else:\n", + " pd.to_datetime(df['Start_time'][ti])\n", + "\n", + " df.loc[ti, 'End_time'] = df['Start_time'][ti]+df['Duration'][ti]\n", + "\n", + "\n", + "print(df)\n", + "\n", + "\n", + "fig = px.timeline(df, x_start=\"Start_time\", x_end=\"End_time\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "f8713f24", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "x=%{x}
y=%{y}", + "legendgroup": "", + "line": { + "color": "#636efa", + "dash": "solid" + }, + "marker": { + "symbol": "circle" + }, + "mode": "lines", + "name": "", + "orientation": "v", + "showlegend": false, + "type": "scatter", + "x": [ + 1, + 2, + 3, + 4, + 5 + ], + "xaxis": "x", + "y": [ + 2, + 4, + 1, + 6, + 3 + ], + "yaxis": "y" + } + ], + "layout": { + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "x" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "y" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "\n", + "# Create a simple line plot\n", + "data = {'x': [1, 2, 3, 4, 5], 'y': [2, 4, 1, 6, 3]}\n", + "fig = px.line(data, x='x', y='y')\n", + "\n", + "# Display the plot\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be2f421c", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05119583", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/Amdahl_Applied.html b/Contribution/Philosophy/Amdahl_Applied.html new file mode 100644 index 000000000..5b1092e4c --- /dev/null +++ b/Contribution/Philosophy/Amdahl_Applied.html @@ -0,0 +1,818 @@ + + + + + + + Amdahl’s Law Applied — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Amdahl’s Law Applied

+
    +
  • Ideally: code on 4 cores runs 4x faster than on 1.

  • +
  • That is rare. There is a name for it: “Embarassingly Parallel.”

  • +
  • It should really be called “Perfectly Parallel.”

  • +
  • Usually, application Speedup is limited by how it is designed or implemented.

  • +
  • When using large numbers or processes and processors, this hits HARD.

  • +
+
+

Speedup

+

If you have a single processor to run code, then assuming perfect parallism (the so-called “embarrasingly parallel” case) then your code will run twice as fast on two processors, four times as fast on four, etc… This is the ideal case. Much real-life code has “dependencies” that limit speedup.

+

Speedup of a task, when adding processors is limited by how much of the code can run in parallel. If you had an infinite number of processors to apply to a problem, then performance is limited by the p the parallelizability of your code.

+

The graph below plots the speedup available against the degree to which the code is parallelizable (from 5% to 95% in steps of 5%.) For example, if the algorithm is only 50% parallelizable, then throwing an infinite number of processors at the problem will give you a 25% speedup ( 1.25 on the plot.) so it isn’t worthwhile.

+

Note that, towards the right edge of the plot, the speedup is nearly vertical. This means that the difference in real life performance near that limit, say between 98% and 99% parallelizable, is huge (as in closer to ten fold than 10%)

+
+
[3]:
+
+
+
+import numpy as np
+from matplotlib import pyplot as plt
+
+
+
+
+
[4]:
+
+
+
+%matplotlib inline
+P = np.linspace(0.05,0.95)
+
+speedup = lambda p: 1/(1-p)
+
+S = np.vectorize(speedup)(P)
+
+f1=plt.figure()
+plt.plot(P,S)
+plt.xlabel("Proportion of code that is Parallelizable")
+plt.ylabel("Maximum theoretical Speedup")
+plt.title( "Speedup vs. Algorithmic Parallelism")
+
+plt.show()
+
+
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Amdahl_Applied_3_0.png +
+
+
+
+

What is Amdahl’s Law?

+
    +
  • Speedup a program can achieve is limited by the level of parallelization of its design.

  • +
  • If code is 95% parallelized, then 5%, or 1 part in 20, is serial… that means, no matter how many processors are put on the job, eventually you are limited by the part that isn’t parallel. It’s asymptotic speedup (that it will never reach, is 20x.)

  • +
  • The more processors you want to use, the more this law bites.

  • +
+
+
[5]:
+
+
+
+nproc=np.logspace(0,16,num=17,base=2,dtype=int)
+
+# p is the parallelizable part of the code, that can be sped up.
+# n is the number of processors applied to the problem.
+def t(p,n):
+    return (1-p)+(p)/n
+
+A = {}
+for p in [ 0.5, 0.75, 0.9, 0.95 ]:
+    i="%d" %p
+    A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)
+    plt.plot(nproc,A[i], label="%02d%% parallel code" %int(p*100))
+
+plt.xscale('log',base=2)
+plt.xlabel("number of processors")
+plt.ylabel("speedup")
+plt.title("Amdahl's law")
+plt.legend()
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Amdahl_Applied_5_0.png +
+
+
+
+

Large Numbers of Processors Need High Parallelism

+

In the previous picture, we could see that:

+
    +
  • we could never get more than a 20 fold speedup if 5% of the code is serial.

  • +
  • To use higher number of processors, we need higher and higher levels of parallelization.

  • +
  • There limit is, of course, 100% parallel code, usually referred to as “embarrassingly parallel.”

  • +
  • to use larger numbers of processors, the ideal is to write embarassingly (aka Perfectly) parallel code.

  • +
+
+
[6]:
+
+
+
+A = {}
+for p in [ 0.99, 0.995, 0.997, 0.999 ]:
+    i="%d" %p
+    A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)
+    plt.plot(nproc,A[i], label=f"{p*100} parallel" )
+
+plt.xscale('log',base=2)
+plt.xlabel("number of processors")
+plt.ylabel("speedup")
+plt.title("Amdahl's law with p>99%")
+plt.legend()
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Amdahl_Applied_7_0.png +
+
+
+
+

Serial Example

+

Below is a simple example of a python snippet that we want to accelerate. It does a math function and calculates “tot” over a range of numbers.

+
+
[7]:
+
+
+
+%%time
+max=1000000
+counter=0
+tot=1
+
+def count():
+   global tot,counter
+   counter=0
+   while counter < max:
+      tot += 1/tot
+      counter += 1
+
+count()
+print( f" total is: {tot}" )
+
+
+
+
+
+
+
+
+ total is: 1414.21661385363
+CPU times: user 161 ms, sys: 0 ns, total: 161 ms
+Wall time: 160 ms
+
+
+

So it calculates the answer, and it does it in 122 milliseconds, entirely in user space (0 ns of system time.) We want to make it faster, so let’s add threading.

+
    +
  • import threads

  • +
  • run four threads in parallel.

  • +
+

should finish four times faster, right?

+
+
[8]:
+
+
+
+%%time
+import threading
+
+max=1000000
+tot=1
+
+def count():
+   global tot
+   counter=0
+   while counter < max:
+      tot += 1/tot
+      counter += 1
+
+threadMax=4
+threads = []
+threadNumber=1
+while threadNumber < threadMax:
+   t = threading.Thread(target=count)
+   threads.append(t)
+   threadNumber += 1
+
+for t in threads:
+    t.start()
+
+for t in threads:
+    t.join()
+
+print( f" total is: {tot}" )
+
+
+
+
+
+
+
+
+ total is: 2449.49161668406
+CPU times: user 404 ms, sys: 3.8 ms, total: 408 ms
+Wall time: 405 ms
+
+
+

OK, so the answer is wrong, and it took 3 times more cpu time to do it. How come?

+
    +
  • Because the overhead for setting up the threads is much more than the size of the problem. (amortization)

  • +
  • But the answer is still wrong. Why?

    +
      +
    • the four tasks are changing the same counters and writing on eachothers work,

    • +
    • it is a pile of race conditions.

    • +
    +
  • +
  • How can we get the parallel code to get the right answer?

  • +
  • We add locks, to serialize access to the variables the tasks are contending for.

  • +
+
+
[9]:
+
+
+
+%%time
+import threading
+
+max=1000000
+counter=0
+tot=1
+counterLock = threading.Lock()
+totLock = threading.Lock()
+
+def count():
+   global counter,tot
+   while counter < max:
+      totLock.acquire()
+      tot += 1/tot
+      totLock.release()
+
+      counterLock.acquire()
+      counter += 1
+      counterLock.release()
+
+threadMax=4
+threads = []
+threadNumber=1
+while threadNumber < threadMax:
+   t = threading.Thread(target=count)
+   threads.append(t)
+   threadNumber += 1
+
+for t in threads:
+    t.start()
+
+for t in threads:
+    t.join()
+
+print( f" total is: {tot}" )
+
+
+
+
+
+
+
+
+ total is: 1414.2180280637874
+CPU times: user 2.7 s, sys: 2.3 s, total: 5 s
+Wall time: 2.78 s
+
+
+

OK, now instead of 3 times slower it is now almost 20 times slower, and spending half the time in the kernel, but the answer is correct. Why did synchronizing access to get the correct the answer make the cpu time double?

+

locking is expensive To parallelize code, use locks sparingly. locks are signs of serial code, and serial code fundamentally limits performance.

+
+
[4]:
+
+
+
+%%time
+
+import threading
+
+max=1000000
+counter=0
+tot=1
+critLock = threading.Lock()
+
+def count():
+   global counter,tot
+   while counter < max:
+      critLock.acquire()
+      tot += 1/tot
+      counter += 1
+      critLock.release()
+
+threadMax=4
+threads = []
+threadNumber=1
+while threadNumber < threadMax:
+   t = threading.Thread(target=count)
+   threads.append(t)
+   threadNumber += 1
+
+for t in threads:
+    t.start()
+
+for t in threads:
+    t.join()
+
+print( f" total is: {tot}" )
+
+
+
+
+
+
+
+
+
+ total is: 1414.2180280637874
+CPU times: user 687 ms, sys: 810 ms, total: 1.5 s
+Wall time: 917 ms
+
+
+
+
Here, instead of using a lock for each variable, we use the concept of a shared lock for when both variables.
+
It eliminates about 40% of the execution time, so now it is only 9x slower than serial.
+
+
+
+

NEWS FLASH: Serial 9x Faster than Parallel!

+
    +
  • Sometimes the thing you are trying to do is inherently serial.

  • +
  • Do something else instead.

  • +
  • more hardware can slow things down because of co-ordination.

  • +
  • locks are hard to get right, and can super easily kill performance.

  • +
  • Also: headlines never tell the whole truth.

  • +
+
+
+

Summary

+
    +
  • Application design limits speedup.

  • +
  • Perfectly Parallel is the gold standard.

  • +
  • almost all parallel, counts for little when things scale.

  • +
  • Locks, joins, waits… they mean you failed to do the above.

  • +
  • (Synchronous) API calls… mean you failed to do the above.

  • +
  • Every small percentage of synchronization or global state, limits ultimate performance.

  • +
  • As the number of processes and processors rises, these small things dominate.

  • +
  • locks, joins, waits… are all expensive, in terms of cpu time. They add overhead to any application. If you use them, make sure they are worth it, and amortize them by making them as coarse as possible.

  • +
  • Ideally, try to formulate the algorithm so there are no synchronization points.

  • +
+
+
+

Relevance?

+

Note that on our main cluster (ddsr.cmc) the configuration is 128 processors:

+
    +
  • 16 processors / node

  • +
  • 8 nodes in the cluster

  • +
  • 600 configurations/node (identical on all nodes)

  • +
  • ~900 processes/node = 7200 processes in the application.

  • +
  • at 7200 processes, 99.9% parallel isn’t good enough to make good use of hardware.

  • +
  • At this scale, you really want p to be around 99.99%…

  • +
+
+
+

So Sarracenia is Not a Parallel App!

+

It is just an ordinary python script that launches processes. It’s not a real parallel app.

+
    +
  • it is not based on threads !

  • +
  • it does not use global locks, no joins, no synchronization!

  • +
  • it is not leveraging multiprocessing!

  • +
  • it is launching completely independent processes that never synchronize!

  • +
+
+

Yes, Exactly.

+
+
+
+

THANKS!

+

light reading:

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/Amdahl_Applied.ipynb b/Contribution/Philosophy/Amdahl_Applied.ipynb new file mode 100644 index 000000000..4190541e6 --- /dev/null +++ b/Contribution/Philosophy/Amdahl_Applied.ipynb @@ -0,0 +1,656 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c565b776", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Amdahl's Law Applied\n", + "==================\n", + "\n", + "\n", + "* Ideally: code on 4 cores runs 4x faster than on 1.\n", + "* That is rare. There is a name for it: \"Embarassingly Parallel.\"\n", + "* It should really be called \"Perfectly Parallel.\"\n", + "* Usually, application *Speedup* is limited by how it is designed or implemented.\n", + "* When using large numbers or processes and processors, this hits *HARD.*\n" + ] + }, + { + "cell_type": "markdown", + "id": "bbac9108", + "metadata": { + "slideshow": { + "slide_type": "notes" + } + }, + "source": [ + "\n", + "Speedup\n", + "-------------\n", + "\n", + "If you have a single processor to run code, then assuming perfect parallism (the so-called \"embarrasingly parallel\" case) then your code will run twice as fast on two processors, four times as fast on four, etc... This is the ideal case. Much real-life code has \"dependencies\" that limit speedup.\n", + "\n", + "Speedup of a task, when adding processors is limited by how much of the code can run in parallel. If you had an infinite number of processors to apply to a problem, then performance is limited by the p the parallelizability of your code.\n", + "\n", + "The graph below plots the speedup available against the degree to which the code is parallelizable (from 5% to 95% in steps of 5%.) For example, if the algorithm is only 50% parallelizable, then throwing an infinite number of processors at the problem will give you a 25% speedup ( 1.25 on the plot.) so it isn't worthwhile.\n", + "\n", + "Note that, towards the right edge of the plot, the speedup is nearly vertical. This means that the difference in real life performance near that limit, say between 98% and 99% parallelizable, is huge (as in closer to ten fold than 10%)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "588983a6", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7bf6e23a", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input", + "remove-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "P = np.linspace(0.05,0.95)\n", + "\n", + "speedup = lambda p: 1/(1-p)\n", + "\n", + "S = np.vectorize(speedup)(P)\n", + "\n", + "f1=plt.figure()\n", + "plt.plot(P,S)\n", + "plt.xlabel(\"Proportion of code that is Parallelizable\")\n", + "plt.ylabel(\"Maximum theoretical Speedup\")\n", + "plt.title( \"Speedup vs. Algorithmic Parallelism\")\n", + "\n", + "plt.show()\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "204608a1", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "What is Amdahl's Law?\n", + "---------------------------------\n", + "\n", + "* Speedup a program can achieve is limited by the level of parallelization of its design.\n", + "\n", + "* If code is 95% parallelized, then 5%, or 1 part in 20, is serial... that means, no matter how many processors are put on the job, eventually you are limited by the part that isn't parallel. It's asymptotic speedup (that it will never reach, is 20x.)\n", + "\n", + "* The more processors you want to use, the more this law bites.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4be08b2a", + "metadata": { + "scrolled": false, + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input", + "hide_code" + ] + }, + "outputs": [ + { + "data": { + "image/png": "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yLXABmAIgIi0xbmddbCqukRkNPApzm2uL6nqL0GMNai+/vpr2rZtS1ZWFoMGDaJbt26cdNJJlbbv1asX3333HQALFy6kbdu2qCpDhw4lMjKSSZMm0bp1632Wu/TSS8vex4wZAzi3+Q4dOpSMjAyKi4vL3f553nnnERsbCzjPi4wePZply5YRHh7Ob7/9VuVnmjdvHrNnz2bixImAc7fYhg0bKm3/v//9j5EjR5adymrZsiXLly+nc+fOHHbYYYBzFPXss88yYMAAOnfuTFpaGgDDhg1jxowZVW63e/fuVcbbGKnHQ/Gff1K0ahWFq1ZR+MuvFK5a5fz6BxAhok0bIpJbEdmpE7F9+hBZurP32fGHt2hhv+AbqWDexfQV/q8lAJzqp/0WYLDP+BxgTk3GtL9f+sHStm1bAJKTk7ngggv44YcfOOmkk2jdujUZGRmkpKSQkZFBcnL56/Wqyr///W/efvttRo8ezQMPPEB6ejpPP/00Dz300D7b8b0+UTp80003cdttt3HeeeexYMECxo8fX9bG91f3k08+SevWrVm+fDkej4eYmJgqP5Oq8t5779G1a9dy0zMzMyttX/H6SVX9oVd2raWy7TZ2WlxM0bp13iODVRT++itFq1c75/sBIiOJ7tKFhIGnENP9cGIO705M166E2ZGXqYKV2giy/Px8du3aVTY8b948jjzySMD5BT9z5kwAZs6cuc+59JkzZ3L22WfTokULCgoKCAsLIywsjILS/+krePvtt8vejz/+eAByc3Np165d2foqk5ubS0pKCmFhYbz22mu493NnxxlnnMEzzzxTtpP/6aefqmx/+umnM23aNFzei5Pbt2+nW7dupKens3btWgBee+01Tj75ZLp168b69etZt24dAG+++eYBb7ch8hQUULD0J7a//jpb7ruPPy68kDXH9GH9BReSce+97HzfudzX7IILSHno33R+/z26LVnMIR+8T9uHHqLlsMuJ693bkoPZL7t1JcgyMzO54IILAOduocsuu4wzzzwTgHHjxnHJJZfw4osv0rFjR959992y5QoKCpg5cybz5s0D4LbbbuOiiy4iKiqq3A7TV1FREf369cPj8ZS1GT9+PEOGDKFdu3Ycd9xxrF+/3u+yN9xwAxdddBHvvvsup5xyyn7P6f/jH//g1ltvpWfPnqgqqampVd6Get111/Hbb7/Rs2dPIiMjGT58OKNHj+bll19myJAhZRepR44cSXR0NDNmzODss88mKSmJ/v37s3LlygPabkPh2r6dvP+bQ+5HH1G4YkXZBeLw5s2JOfxwEq66kuju3YnpfjhRnTraKSFTI6Sqw/z6pk+fPlrxwuqqVasaxfnp0r4wKt5K2xg1lL+5p6iI3fPnkztrNrsXLQKXi+ju3WkycCAxRxxOTPfuRLRpYw8HmoMiIktUtY+/eXYEYUwdoh4Pe5YuJXfWLPLmfopn1y4ikpNpedWVNDvvfGK6HhbqEE0jYgmigUhPTw91COYgFK1fT+7s2eTN/oiSzZuRuDiaDhpEs/PPI65fPztlZELCEoQxIeLasYO8OXPInT2bwuU/Q1gY8ccfT6tbbqbJaacRFhcX6hBNI2cJwpha5CkuZvf8BeTOns3uL790rit07UryHXfQ9JxziGztrzSZMaFhCcKYIFNV9vz0E7mzZpP3ySd48vKIaNWKlldcQbPzzyOmW7dQh2iMX5YgjAmiwlWryLj3Pgp//RWJjaXJaafR7PzziT/+OLuuYOo8e1AuyNasWUOvXr3KXk2bNuWpp54CnGcU2rVrVzZvzhznwfGvv/6anj17cuyxx5Y9RLZz507OOOOMKp8+Dob09PSyB/sWLFjAOeecU2X7QNpUVzDWGWye4mKyn36a9UMuoSQri5SH/k3aokW0e/wxEvqfYMnB1At2BBFkXbt2ZdmyZYBT0bRdu3ZlD84BjBkzhrFjx5Zbxsp91297Vqwg4557KPp9Lc3OP5/Wd48jvHnzUIdlTLXZEUQt+vzzzzn00EPp1KlTle0OtNz3XXfdRd++fenbt2/ZkcdHH31Ev379OProoznttNPKaiWNHz+eESNGcPrpp3PllVeSnp7OiSeeSO/evenduzfffPNNlTHm5+dzzTXXcOyxx3L00Ucza5a/orx7ud1uxo4dS48ePejZsyfPPPNM2b/J0UcfTY8ePbjmmmsoKioCYO7cuXTr1o3+/fvz/vvvl62nututTZ7CQrImTiR96N9w5+2iw/RptH10giUHU281rp+Nn4yDrStqdp1tesBZEwJq+tZbb5VVXC01ZcoUXn31Vfr06cOkSZNo0aKFlfuuh+W+C5b+RMa991K8fj3Nhwwh+c47CG/SJKQxGXOw7AiilhQXFzN79myGDBlSNm3UqFGsW7eOZcuWkZKSwu233w7sLfc9f/58/vjjj3LlvocNG1ZpxVTfct/ffvst4JT7PuOMM+jRowePP/44v/yyt2p6xXLfw4cPp0ePHgwZMoRff626++958+YxYcIEevXqxYABAw6o3PeaNWv2Kfe9cOFCVq9eXVbuW0QYNmzYAW832DwFBWx9+GH+vPxytKiIji+9SMqD/7LkYBqExnUEEeAv/WD45JNP6N27d7l+HHyHhw8fvs+FWCv3Hfh2QyH/+x/IuO8+SjZupMVll5F8+21WIdU0KHYEUUvefPPNfU4vZWRklA1/8MEHZXcLlbJy33Wz3Ld7dz5b//UvNlx1FYjQ6bVXaXP/Pyw5mAancR1BhEjp+f3p06eXm37nnXeybNkyRITU1NRy863cd90s9737q6/JuP8fuDK20vLqq2l1y82EeU/TGdPQBK3ct4i8BJwDZKnqkd5pbwOl5waaAztVtZefZdOBXYAbcFVWirYiK/dt5b4hOH9zd14emY89Ru5/3yPqkENIeejfxB19dI1uw5hQCFW571eAKcCrpRNUtex2FBGZBPjpIb3MKaq6LWjRGROgXQsWsPWf43FlZ5M4fDhJo28kLDo61GEZE3TB7JN6oYik+psnzhXIS4CBwdp+Y2Plvmuee+dOtj78MHmzPyI6LY32U6YQ2+PI/S9oTAMRqmsQJwKZqvp7JfMVmCciCkxX1RmVrUhERgAjADp27FjjgZrGadeCBWTc9w/cO3eSdMMNJI28HomKCnVYxtSqUCWISwH/V1odJ6jqFhFJBj4TkdWqutBfQ2/ymAHONYiaD9U0Nru//JJNo28iOi2Nji88b9VWTaNV6wlCRCKAC4FjKmujqlu871ki8gHQF/CbIIypSQVLf2LTLbcSc9hhdHx1JuEJCaEOyZiQCcVzEKcBq1V1k7+ZIhIvIk1Kh4HTgZW1GJ9ppAp/+42NI0cS2bo1HZ6fYcnBNHpBSxAi8ibwLdBVRDaJyLXeWX+jwuklEWkrInO8o62Br0RkOfAD8H+qOjdYcdaGyZMnc+SRR3LEEUeUlfoG52GxQYMGkZaWxqBBg9ixYwdg5b5rY50VFW/azMbrhhMWE0OHF18kIjExqNszpj4IWoJQ1UtVNUVVI1W1vaq+6J1+tapOq9B2i6oO9g7/oapHeV9HqOq+NSXqkZUrV/L888/zww8/sHz5cj7++GN+/925Nj9hwgROPfVUfv/9d0499VQmTHBKgZSW+3744YeZOnUqQNDLfTdmrpwcNl57LZ7CQjq88DxR7duFOiRj6gQrtRFkq1at4rjjjiMuLo6IiAhOPvlkPvjgAwBmzZrFVVddBTiF6j788EPAyn1D7ZX7du/ezcbhIyjJzKTDtGnEeAsHGmMaWamNR394lNXbV9foOru17MZdfe+qdP6RRx7JvffeS05ODrGxscyZM4c+fZyHFjMzM0lJSQEgJSWFrKwsACv3XUvlvj1FRWy6cTSFv/1Gh2enENfbnow2xlejShCh0L17d+666y4GDRpEQkICRx111H57byst9w2wcOHCcuW+IyMjmTRpUrlKsKV8y32PGTMGcMp9Dx06lIyMDIqLi+ncuXNZ+4rlvkePHs2yZcsIDw/nt99+qzLGefPmMXv2bCZOnAhwQOW+ly9fvk+572effZYBAwaUlfsGGDZsGDNmzKhyu9UtraFuN1vG3kHB99/T9vHHSKji6MyYxqpRJYiqfukH07XXXsu11zrX6O+55x7at28POOW+MzIySElJISMjg+Tk5HLLWbnvwLdbHarK1vEPsOuzz2h9z900O/fcA16XMQ2ZXYOoBaWnjjZs2MD7779f9kv/vPPOKyvBPXPmTM4///xyy1m57+CU+85+ajI7332XxOuvp+WVV1Z7eWMai0Z1BBEqF110ETk5OURGRvLss8/SokULAMaNG8cll1zCiy++SMeOHXn33XfLlrFy38Ep97195kxypk+n+SWX0OrWWwJezpjGKGjlvkPByn1buW+o/G+eO2sWW+4aR5NBg2j31JNIeHgIojOmbqmq3LedYjKNwq4FC9hyz73E9etH24mPW3IwJgB2iqmBsHLflStYupTNt44hpmtX2j87xfpyMCZAdgRhGrTCNb+xceQoItu0sfpKxlSTJQjTYBVv2sTG665z6iu98ILVVzKmmuwUk2mQXDk5bLj2WjzFxXR67VWrr2TMAbAEYRoc9XjYMHw4rswsOr70ktVXMuYA2SmmWlBZue/x48fTrl07evXqRa9evZgzx6l4buW+D3yd6vHg3r6dot9+p/3Tk62+kjEHwRJEkFVV7htgzJgxLFu2jGXLljF48GDAyn0fKFWlZNMmtLiYto88TMJJJ4U6JGPqNUsQQVZVue/KWLnvAyv37crIwJ2XR3jTplZfyZga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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "nproc=np.logspace(0,16,num=17,base=2,dtype=int)\n", + "\n", + "# p is the parallelizable part of the code, that can be sped up.\n", + "# n is the number of processors applied to the problem.\n", + "def t(p,n):\n", + " return (1-p)+(p)/n\n", + "\n", + "A = {}\n", + "for p in [ 0.5, 0.75, 0.9, 0.95 ]:\n", + " i=\"%d\" %p\n", + " A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)\n", + " plt.plot(nproc,A[i], label=\"%02d%% parallel code\" %int(p*100))\n", + "\n", + "plt.xscale('log',base=2)\n", + "plt.xlabel(\"number of processors\")\n", + "plt.ylabel(\"speedup\")\n", + "plt.title(\"Amdahl's law\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "338525a3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Large Numbers of Processors Need High Parallelism\n", + "---------------------------------------------------------------------------\n", + "\n", + "In the previous picture, we could see that:\n", + "\n", + "* we could never get more than a 20 fold speedup if 5% of the code is serial. \n", + "* To use higher number of processors, we need higher and higher levels of parallelization. \n", + "* There limit is, of course, 100% parallel code, usually referred to as \"embarrassingly parallel.\" \n", + "* to use larger numbers of processors, the ideal is to write embarassingly (aka Perfectly) parallel code." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0d21732c", + "metadata": { + "scrolled": false, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "image/png": "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hhdeeIH777+fv/3tb/ts7/f7ufrqq5kzZw7p6ekcccQRnHnmmQwa1HBdE/v9frxeb+0rGmMaTaCwkNINGynbsIGyDRso3bCesg0bKdu4EQ1qwvXExxPRsyexo0cT0bMnEb16EtmzJ57o6AaPqTGfhjq/hkUn1rD+VJxO36vOn08r6Xt35cqVjB49mpiYGADGjBnDu+++y80338zq1as57jinW+mTTjqJcePG7Zcs5s2bR58+fejVqxcA5513Hu+///5+yeLiiy8mKiqK5cuXk5mZyYMPPsjpp5/Oxo0b+fWvf01hYSEAjz/+OEcffTRz587lrrvuonPnzixevJgVK1YwceJEtmzZQklJCddddx2XX375Ad/b/fffz6xZsygtLeWss87irrvuapC/mTGtmapSunIlRQsWUrZhPaXrneTgy8zcu5LHQ3h6OhE9exD7s58R0asXET17ENmrF94mvAfbast91OauD5azYlv1TT31NahLAneeMbjG5Ycddhi33347OTk5REdH8/HHHzNq1KjKZbNnz2bChAm8+eabbNmyZb/tt27dSteue2/tpKen8/3331d7rI0bN/Lll1/y008/MXbsWNatW0dKSgpz5swhKiqKtWvXcv7551eWRJk3bx7Lli2rfELphRdeIDExkeLiYo444ggmTZpEUlJStcf69NNPWbt2LfPmzUNVOfPMM/nqq68qk58xZq9AURGF335LwdwvKfjqq8rEsN9VQs+eRPbqSXj37ngiDrWL9UPXZpNFKAwcOJBbbrmFk046ibi4OIYNG0ZYmPNP8MILL3Dttdfy17/+lTPPPJOIaj4c1fU9UtO3inPOOQePx0Pfvn3p1asXq1atomfPnlxzzTUsXrwYr9fLmjVrKtc/8sgj93mU9dFHH+Xdd98FYMuWLaxdu/aAyeLTTz/l8MMPB6CgoIC1a9dasjDGVbZli5McvvySonnz0LIyPLGxxB5zDHHHH0/sMUcTlpLSrB/+aLPJ4kBXAI3p0ksv5dJLLwXgtttuIz3dedhrwIABfPqp8xTDmjVr+Oijj/bbNj09fZ8rjoyMDLp06VLtcap+6ESEhx56iNTUVJYsWUIgECAqKqpyeWxsbOX43Llz+c9//sO3335LTEwMxx9//AEfdVVVbr31Vq644ora3r4xbYKWl1O0aBEFX35JwdwvKfvpJwAievSg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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "A = {}\n", + "for p in [ 0.99, 0.995, 0.997, 0.999 ]:\n", + " i=\"%d\" %p\n", + " A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)\n", + " plt.plot(nproc,A[i], label=f\"{p*100} parallel\" )\n", + "\n", + "plt.xscale('log',base=2)\n", + "plt.xlabel(\"number of processors\")\n", + "plt.ylabel(\"speedup\")\n", + "plt.title(\"Amdahl's law with p>99%\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7653b5af", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Serial Example\n", + "---------------------\n", + "\n", + "Below is a simple example of a python snippet that we want to accelerate. It does a math function and calculates \"tot\" over a range of numbers. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "70f5c093", + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "subslide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.21661385363\n", + "CPU times: user 161 ms, sys: 0 ns, total: 161 ms\n", + "Wall time: 160 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "\n", + "def count():\n", + " global tot,counter\n", + " counter=0\n", + " while counter < max:\n", + " tot += 1/tot\n", + " counter += 1\n", + "\n", + "count()\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "511d4722", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "\n", + "So it calculates the answer, and it does it in 122 milliseconds, entirely in user space (0 ns of system time.)\n", + "We want to make it faster, so let's add threading.\n", + "\n", + "* import threads\n", + "* run four threads in parallel.\n", + "\n", + "should finish four times faster, right?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a8c41990", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 2449.49161668406\n", + "CPU times: user 404 ms, sys: 3.8 ms, total: 408 ms\n", + "Wall time: 405 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "import threading\n", + "\n", + "max=1000000\n", + "tot=1\n", + "\n", + "def count():\n", + " global tot\n", + " counter=0\n", + " while counter < max:\n", + " tot += 1/tot\n", + " counter += 1\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "84d5090d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "\n", + "OK, so the answer is wrong, and it took 3 times more cpu time to do it. How come?\n", + "\n", + "* Because the overhead for setting up the threads is much more than the size of the problem. (amortization)\n", + "* But the answer is still wrong. Why?\n", + " * the four tasks are changing the same counters and writing on eachothers work, \n", + " * it is a pile of race conditions. \n", + " \n", + "* How can we get the parallel code to get the right answer?\n", + "\n", + "* We add locks, to serialize access to the variables the tasks are contending for." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9910d980", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.2180280637874\n", + "CPU times: user 2.7 s, sys: 2.3 s, total: 5 s\n", + "Wall time: 2.78 s\n" + ] + } + ], + "source": [ + "%%time\n", + "import threading\n", + "\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "counterLock = threading.Lock()\n", + "totLock = threading.Lock()\n", + "\n", + "def count():\n", + " global counter,tot\n", + " while counter < max:\n", + " totLock.acquire()\n", + " tot += 1/tot\n", + " totLock.release()\n", + "\n", + " counterLock.acquire()\n", + " counter += 1\n", + " counterLock.release()\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "f721701a", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "OK, now instead of 3 times slower it is now almost 20 times slower, and spending half the time in the kernel,\n", + "but the answer is correct. Why did synchronizing access to get the correct the answer make the cpu time double? \n", + "\n", + "**locking is expensive** To parallelize code, use locks sparingly. locks are signs of serial code, and serial code fundamentally limits performance. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f978a69e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.2180280637874\n", + "CPU times: user 687 ms, sys: 810 ms, total: 1.5 s\n", + "Wall time: 917 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "import threading\n", + "\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "critLock = threading.Lock()\n", + "\n", + "def count():\n", + " global counter,tot\n", + " while counter < max:\n", + " critLock.acquire()\n", + " tot += 1/tot\n", + " counter += 1\n", + " critLock.release()\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )\n" + ] + }, + { + "cell_type": "markdown", + "id": "28d52604", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Here, instead of using a lock for each variable, we use the concept of a shared lock for when both variables. \n", + "It eliminates about 40% of the execution time, so now it is only 9x slower than serial." + ] + }, + { + "cell_type": "markdown", + "id": "628b9fd7", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## NEWS FLASH: Serial 9x Faster than Parallel!\n", + "\n", + "* Sometimes the thing you are trying to do is inherently serial. \n", + "* Do something else instead.\n", + "* more hardware can slow things down because of co-ordination.\n", + "* locks are hard to get right, and can super easily kill performance.\n", + "* Also: headlines never tell the whole truth.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "9a8ff838", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Summary\n", + "--------------\n", + "\n", + "* Application design limits speedup.\n", + "\n", + "* **Perfectly Parallel** is the gold standard.\n", + "\n", + "* *almost all parallel*, counts for little when things scale.\n", + "\n", + "* Locks, joins, waits... they mean you failed to do the above.\n", + "\n", + "* (Synchronous) API calls... mean you failed to do the above.\n", + "\n", + "* Every small percentage of synchronization or global state, limits ultimate performance. \n", + "\n", + "* As the number of processes and processors rises, these small things dominate.\n", + " \n", + "* locks, joins, waits... are all expensive, in terms of cpu time. They add overhead to any application. If you use them, make sure they are worth it, and amortize them by making them as coarse as possible.\n", + "\n", + "* Ideally, try to formulate the algorithm so there are no synchronization points.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d0f6f04d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Relevance?\n", + "\n", + "Note that on our main cluster (ddsr.cmc) the configuration is 128 processors:\n", + "\n", + "* 16 processors / node\n", + "* 8 nodes in the cluster\n", + "* 600 configurations/node (identical on all nodes)\n", + "* ~900 processes/node = 7200 processes in the application.\n", + "* at 7200 processes, 99.9% parallel isn't good enough to make good use of hardware.\n", + "* At this scale, you really want p to be around 99.99%...\n" + ] + }, + { + "cell_type": "markdown", + "id": "226c39b7", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## So Sarracenia is Not a Parallel App!\n", + "\n", + "\n", + "It is just an ordinary python script that launches processes. It's not a real parallel app.\n", + "\n", + "* it is not based on threads !\n", + "* it does not use global locks, no joins, no synchronization!\n", + "* it is not leveraging multiprocessing!\n", + "* it is launching completely independent processes that never synchronize!\n", + "\n", + "### Yes, Exactly." + ] + }, + { + "cell_type": "markdown", + "id": "f9b00250", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "## THANKS!\n", + "\n", + "\n", + "light reading:\n", + "\n", + "* https://en.wikipedia.org/wiki/Amdahl%27s_law\n", + "* https://jenkov.com/tutorials/java-concurrency/amdahls-law.html\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/CAP_Theorem_Applied.html b/Contribution/Philosophy/CAP_Theorem_Applied.html new file mode 100644 index 000000000..fc69a5e68 --- /dev/null +++ b/Contribution/Philosophy/CAP_Theorem_Applied.html @@ -0,0 +1,769 @@ + + + + + + + CAP Theorem Applied — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

CAP Theorem Applied

+
    +
  • Peter Silva, 2023

  • +
+
+

CAP Theorem

+
    +
  • Originally proposed by Eric A. Brewer in 2000.

  • +
  • Proved in 2002 by Gilbert & Lynch.

  • +
  • Definitions from Gilbert & Lynch

  • +
+
+
+

Definitions

+
    +
  • Consistency - any read operation that begins after a write operation completes must return that value, or the result of a later write operation

  • +
  • Availability - every request received by a non-failing node in the system must result in a response

  • +
  • Partition (Tolerance) - the network will be allowed to lose arbitrarily many messages sent from one node to another

  • +
+
+

Cap Theorem

+
    +
  • An ideal distributed system will feature all Consistency, Availability, and Partition Tolerance.

  • +
  • In reality, the best one can do is 2 out of three.

  • +
+
+
+

Proof

+ +
+
+
+
+

Storage/State

+
    +
  • What is a File System? – an ordered list of writes (with a path)

  • +
  • What is a SQL Database? – an ordered list of writes (with indices)

  • +
  • What is a File? - an ordered list of bytes, with an id (path.)

  • +
  • What is an Object (in object storage context) – an ordered list of bytes, with an id (opaque hash)

  • +
+
    +
  • Traditional databases and file systems are just really large files.

  • +
  • files and objects are the same thing

  • +
  • They are units of synchronization/state.

  • +
  • Consistency == order == synchronization == global state.

  • +
  • When Distributed…

    +
      +
    • User expectation is coherence/consistency

    • +
    • Coherence is the same thing as shared state.

    • +
    +
  • +
  • Amdahl’s Law tells us that scaling global state is really, really hard.

  • +
+
+
[96]:
+
+
+
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch
+
+def three_circles(ax, x, y, box_bg, arrow1):
+    patches = [
+        Circle( (x,y), 0.3, fc=box_bg),
+        Circle( (x+2,y), 0.3, fc=box_bg),
+        Circle( (x+1, y-1), 0.3, fc=box_bg),
+        FancyArrow( x+0.4, y, 1.2, 0, fc=arrow1, width=0.05, head_width=0.05, head_length=0 ),
+        FancyArrow( x+0.25, y-0.3, 0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),
+        FancyArrow( x+1.7, y-0.3, -0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),
+    ]
+
+    for p in patches:
+        ax.add_patch(p)
+    plt.text(x-0.15,y+0.4, 'G1', fontsize=20)
+    plt.text(x+1.85,y+0.4, 'G2', fontsize=20)
+    plt.text(x+0.70,y-1.6, 'Client', fontsize=20)
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(8, 4.2), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 8)
+    ax.set_ylim(0, 6)
+
+    x=3
+    y=3
+    three_circles(ax, x,y,box_bg,arrow1)
+
+
+
+
+
+
+
[95]:
+
+
+
+# Proof of IOPS ceiling
+SAN_iops=100000
+time_for_1_iop=1/SAN_iops
+distance=600
+c=300000
+tprop=distance/c
+max_iops=1/(2*(time_for_1_iop+tprop))
+
+print( f"Assuming a file system can do {SAN_iops} IOPS")
+print( f"distance between data centres={distance}, speed of light={c} km/s,  time to travel (1way)={tprop} sec. " )
+
+
+
+
+
+
+
+
+
+Assuming a file system can do 100000 IOPS
+distance between data centres=600, speed of light=300000 km/s,  time to travel (1way)=0.002 sec.
+
+
+
+
[92]:
+
+
+
+create_base()
+plt.text(1.5, 4, 'Max IOPS for Coherent Filesystem',fontsize=24)
+plt.text(3.3, 3.3, '|<-- 600km == 0.002 sec. -->|', fontsize=7)
+plt.text(2.5, 1.0, f"maximum iops when {distance} km apart: {int(max_iops)}", fontsize=12)
+
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_CAP_Theorem_Applied_8_0.png +
+
+
    +
  • CA: Consistency and Availability

  • +
  • AP: Availabilty and Partition Tolerance.

  • +
  • CP: Consistency and Partition Tolerance

  • +
+
+

Choosing CA:

+
    +
  • At best, you need a quorum system.

  • +
  • quorum means obtaining a consensus among the existing nodes about the current state.

  • +
  • usually means voting for primaries (at best, sharded masters) and triggering a pause to vote for a new primary whenever it goes down.

  • +
  • The system as a whole, cannot respond to queries while voting is in progress, and so is unavailable.

  • +
  • but at least it is up after the vote is done, in spite of the formerly primary node dying.

  • +
+
+
+

Quorum and Voting:

+
    +
  • Multiple Algorithms:

    +
      +
    • Paxos: “Paxos is exceptionally difficult to understand”

    • +
    • “few people were comfortable with Paxos even among seasoned researchers”

    • +
    +
  • +
  • papers like “In Search of an Understandable Consensus Algorithm (Extended Version)” resulted in RAFT

  • +
  • Things you want:

    +
      +
    • quickly convergent?

    • +
    • a single leader elected?

    • +
    • low overhead once leader in place.

    • +
    • quick detection of loss of leader.

    • +
    • known behaviour when: flapping, partitioning, WAN.

    • +
    +
  • +
  • If it partitions, tradeoffs:

    +
      +
    • AP: both sides will elect new leaders and “fork” the file system (writing inconsistently thereafter.) OR:

    • +
    • CA: One side will know it has a minority of nodes and shutdown (losing a lot of nodes, but hoping the other side is still up.) OR:

    • +
    • CP: both sides refuse to write. (no A for writing.)

    • +
    +
  • +
+
+
+

Choosing AP:

+
    +
  • incoming requests are routed to the nodes that remain up. They provide the data they have.

  • +
  • you don’t need a master. nodes receive updates, and tell the others,

  • +
  • changes propagate through the cluster… eventually consistent.

  • +
  • no voting, no loss of availability.

  • +
+
+
+

Voting Failure Modes

+ + + + + + + + + + + + + + + + + + + + +

Number of Masters

< 1

1

>1

All Talk

Down wait

Up OK

Bickering

Partitioned

Unstable LR

Unstable LR

LR

+

(LR… partitioned into left and right clusters.)

+
+
+

Choosing CP:

+
    +
  • as long as the backups can reach the primary, all can answer read requests.

  • +
  • when a write is received, the backup passes it to the primary.

  • +
  • when a backup loses access to the primary, for coherency, it must stop answering. It no longer knows.

  • +
  • A Manual intervention would be required to tell the backup that it is now the primary in order for availability to be restored, because there is no way to tell the difference between a primary failure and a network partition.

  • +
+
+

Bringing Amdahl & CAP Together

+
    +
  • Amdahl’s Law is about dealing with large numbers of processors sharing resources.

  • +
  • CAP is about tradeoffs inherent using different methods of sharing resources.

  • +
+
+
+

Almost-Coherent File System or DB Performance

+
    +
  • A bandaid on Traditional single node

  • +
  • a synchronized file system is going to have ordered operations, it is going to be a 1:1 connection with shared state.

  • +
  • usually a “journal” to sync between sides.

  • +
  • in the meantime, readers on the wrong side get the wrong data.

  • +
  • sometimes you establish an upper bound (if you hit it, you hang so that it doesn’t get too out of sync.)

  • +
  • synchronization is a struggle against Amdahl’s Law.

  • +
  • will not scale.

  • +
+
+
+

Universal Write Scaling

+
    +
  • obtain delegation for a subdomain, write locally.

    +
      +
    • write behind cache … the write will get to the other side eventually, but G1 and G2 will be out of synch for a while.

    • +
    • sharding (dividing domain so every node is writing locally.)

    • +
    • route requests to appropriate shard.

    • +
    +
  • +
  • locks over smaller subsets –> more write parallelism.

  • +
  • writes must be localized (no solution for scaled distributed writes.)

  • +
+
+
+

Non-Shared Approaches.

+
    +
  • the only thing that does scale.

  • +
  • transfer files or objects, rather than synchronizing file systems. maximally distributed synchronization.

    +
      +
    • explicit sharding.

    • +
    +
  • +
  • any number of transfers can occur in parallel.

  • +
  • the underlying file systems are local stores.

    +
      +
    • are not the same, they just contain the same files, eventually.

    • +
    • are not synchronized at any point, they receive a sum of changes that add up to the same stored state.

    • +
    +
  • +
  • lag can occur, but can run an unlimited number of processes to do transfers, none limited by propagation delay. Transfers occur in parallel to the degree desired.

  • +
  • Because the transfers are independent, no fundamental performance limit, beyond the perfomance of hardware on each end, and the pipe between.

  • +
+
+
+

Examples of Non-Shared Approaches

+
+
+
+

Object Stores

+
    +
  • An object is a self-contained item that can be distributed.

  • +
  • Every object is equivalent to a db or a file system.

  • +
  • with many objects, state is distributed among subsets of nodes, not global.

  • +
  • distribution of objects (with lag) is much easier to achieve than a coherent view.

  • +
  • Different Object Stores lean in different directions in CAP.

  • +
  • requires application adaptation, perhaps profound.

  • +
+

CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg

+
    +
  • Source: Fotis Nikolaidis from “Tromos : a software development kit for virtual storage systems” (PhD thesis 2020/01/17)

  • +
+
+

Cloud Solves This!

+
    +
  • cloud vendors talk about “availability zones” there is no HA or synch across availabilty zones, they talk about times like 15 minutes, to do periodic synching. Object syncing takes time (and money.)

  • +
  • 2006, HP DC Consolidation project established 200 miles as the longest distance between Data Centres. So that would give them 500 IOPS or so on a synchronous store.

  • +
  • Note, putting a file system or a db inside a single object store means no scaling.

  • +
  • Individual objects don’t scale, using many objects does.

  • +
+

from: https://aws.amazon.com/rds/features/multi-az/

+
    +
  • Failover time depends on length of replica lag

  • +
  • Any data updates that occurred after the latest restorable time (typically within the last 5 minutes) will not be available

  • +
+

From Google: https://cloud.google.com/compute/docs/disks#repds * Regional persistent disks have storage qualities that are similar to zonal persistent disks. However, regional persistent disks provide durable storage and replication of data between two zones in the same region. * Zonal SSD PD multi-writer mode Throughput per GB (MB/s): 0.48

+

the google performance numbers are within a zone (multi-DC but not too far apart.) not multi-zone.

+
+
+
+

Sarracenia: File Systems Flying in Formation

+
    +
  • Another distributed approach

  • +
  • transfers files between file systems without synchronizing.

  • +
  • Every file is equivalent to an object, a db or a file system.

  • +
  • no locking, architecturally unlimited scaling.

  • +
  • generalized buffering and parallelism of transfer at scale.

  • +
  • file systems at all nodes are totally independent, limited only by local hardware on each node.

  • +
  • transfers limited by bandwidth, not latency.

  • +
  • works with legacy code un-changed, or new (agnostic.)

  • +
  • In terms of CAP–> heavily AP.

  • +
+
+
+
+

Thanks!

+

Sources:

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/CAP_Theorem_Applied.ipynb b/Contribution/Philosophy/CAP_Theorem_Applied.ipynb new file mode 100644 index 000000000..05d44d8af --- /dev/null +++ b/Contribution/Philosophy/CAP_Theorem_Applied.ipynb @@ -0,0 +1,583 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b3167c7f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# CAP Theorem Applied\n", + "\n", + "* Peter Silva, 2023\n" + ] + }, + { + "cell_type": "markdown", + "id": "9160eaed", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "### CAP Theorem \n", + " * Originally proposed by Eric A. Brewer in 2000. \n", + " * Proved in 2002 by Gilbert & Lynch.\n", + " * Definitions from Gilbert & Lynch\n", + " \n", + "### Definitions\n", + "* Consistency - _any read operation that begins after a write operation completes must return that value, or the result of a later write operation_ \n", + "* Availability - _every request received by a non-failing node in the system must result in a response_ \n", + "* Partition (Tolerance) - _the network will be allowed to lose arbitrarily many messages sent from one node to another_ " + ] + }, + { + "cell_type": "markdown", + "id": "1ce1e254", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Cap Theorem\n", + "\n", + "* An ideal distributed system will feature all Consistency, Availability, and Partition Tolerance. \n", + "\n", + "* In reality, the best one can do is 2 out of three. " + ] + }, + { + "cell_type": "markdown", + "id": "0bc777a6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Proof\n", + "\n", + "* walk through: Gilbert & Lynch (Thanks to MWhittaker): https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/ ?\n", + "\n", + "* Real usage of CAP theoream involves looking at what is pragmatic in real-life cases." + ] + }, + { + "cell_type": "markdown", + "id": "bc391fe2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Storage/State\n", + "\n", + "* What is a File System? -- an ordered list of writes (with a path)\n", + "\n", + "* What is a SQL Database? -- an ordered list of writes (with indices)\n", + "\n", + "* What is a File? - an ordered list of bytes, with an id (path.)\n", + "\n", + "* What is an Object (in object storage context) -- an ordered list of bytes, with an id (opaque hash)" + ] + }, + { + "cell_type": "markdown", + "id": "26baf6e2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## They Are All Units of Synchronization\n", + "* Traditional databases and file systems are just really large files. \n", + "* files and objects are the same thing\n", + "* They are units of synchronization/state.\n", + "* Consistency == order == synchronization == global state.\n", + "* When Distributed...\n", + " * User expectation is coherence/consistency\n", + " * Coherence is the same thing as shared state.\n", + "* Amdahl's Law tells us that scaling global state is really, really hard.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "e53833fe", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch\n", + "\n", + "def three_circles(ax, x, y, box_bg, arrow1):\n", + " patches = [\n", + " Circle( (x,y), 0.3, fc=box_bg),\n", + " Circle( (x+2,y), 0.3, fc=box_bg),\n", + " Circle( (x+1, y-1), 0.3, fc=box_bg),\n", + " FancyArrow( x+0.4, y, 1.2, 0, fc=arrow1, width=0.05, head_width=0.05, head_length=0 ),\n", + " FancyArrow( x+0.25, y-0.3, 0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),\n", + " FancyArrow( x+1.7, y-0.3, -0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),\n", + " ]\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(x-0.15,y+0.4, 'G1', fontsize=20)\n", + " plt.text(x+1.85,y+0.4, 'G2', fontsize=20)\n", + " plt.text(x+0.70,y-1.6, 'Client', fontsize=20)\n", + " \n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(8, 4.2), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 8)\n", + " ax.set_ylim(0, 6)\n", + "\n", + " x=3\n", + " y=3\n", + " three_circles(ax, x,y,box_bg,arrow1)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "703693f9", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Assuming a file system can do 100000 IOPS\n", + "distance between data centres=600, speed of light=300000 km/s, time to travel (1way)=0.002 sec. \n" + ] + } + ], + "source": [ + "# Proof of IOPS ceiling\n", + "SAN_iops=100000\n", + "time_for_1_iop=1/SAN_iops\n", + "distance=600\n", + "c=300000\n", + "tprop=distance/c\n", + "max_iops=1/(2*(time_for_1_iop+tprop))\n", + "\n", + "print( f\"Assuming a file system can do {SAN_iops} IOPS\")\n", + "print( f\"distance between data centres={distance}, speed of light={c} km/s, time to travel (1way)={tprop} sec. \" )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "id": "4db28587", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "image/png": "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pfo5OvJI6++ZU+fdkSNHTPXq1dOsMyAgwGzbts1af9GiRenGUalSpQxn3kTuYjYz5FleXl569NFH9emnn+rYsWNav369mjVrlqLcrFmzZIyRZN+a44gbLS7e3t6qVq2add8SY4z+/fdf/f7774qMjFR8fLxefvllSdJLL72Ual3NmjXTyy+/rIkTJ0qS+vXrp927d6tYsWJ25a5cuaIePXpYV7Z69uyZ6dakrLj5alhWt/n4449bdR07dkzHjx9XaGjoLcWWfDyKJAUEBFjPixQposDAQKtL26RJk9SuXbtcmUmsc+fOql69uqTrx9mNsTy9e/dWoUKF0l13yJAh+uijj6zXfn5+uu+++3THHXcoIiJCa9as0eXLlxUTE6MRI0bo9OnTmjx5coYxGWPUs2dPLVmyRDabTXfddZeqVKkiY4z27t2bpc9l9erV1nMvLy917do103Vk5L333rPr0ujt7a1mzZqpdOnSOn/+vNasWaNz584pMTFRU6ZM0fHjx/X99987tD9jxozRm2++Ken/xsx5enpq586d2r59uyQpPDxcnTp10rZt26zxeKn5z3/+Y9e6VaRIETVs2FAlSpRQTEyMduzYob1798oYoy+//FL//POPli5dmmHXw6ioKLVr104nTpyQj4+PmjRpotDQUF26dCnF1fUDBw6oZcuW1uyMNptNtWvXVrVq1eTr66tTp05p/fr1unTpkv755x+1bt1aP/30U4atYtL1Founn35aklSpUiXVr19fBQoU0MGDB/Xrr7/KGKOoqCh17NhRBw4cSDGz4I2ZCX/44Qf9888/ktKebaxKlSoZxuMqsuu4SExMVPv27bVt2zZrWfXq1a1ZGWNiYhQREaFdu3alOTvnwIEDNWnSJEnSt99+qylTpsjPzy/d+JN3L37kkUdSfH/16tXLrptp+fLlVadOHQUFBSk+Pl5nz57Vnj17dOzYsRR1+/v7W8eFI9+TqR0rOXXeRUdH64EHHtCRI0cUGBio5s2bKyQkRH///bdWr16tuLg4Xbx4UW3bttWff/6p7du3q2vXroqPj1doaKgaNWqkggULat++fdq0aZOk662aPXv21MqVK9PdNnKR8/IowN7NLTPG2N/b44knnkh1vfLlyxtJJjAw0Fy7ds0Y43jLzNNPP22WLl1qrl69mur7CQkJZsaMGcbPz89I11tojh49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aIxRfHy8IiMjtXXrVn300Ue33EVQut660LVrV61evVr//e9/7badm93NkrdsONqdL3m55P/cU3Oju1R2Sn5s3MpNK1OT059HdkzRnR2tDxn9sHHk75YbceTGlOa3i5z6e7zwwgtav3692rZta9cSEx0drRUrVmjEiBGqXLmyWrZsqT179qRb/8CBA63nN3c127Bhg9XjoGTJkmrbtm2a9Tz88MPaunWrHnnkEXl5eVnLr127pnXr1unNN99U/fr1Vb9+fa1fvz79nXZQXjzeOT9cE8kMXEKhQoWsK067d+/Wyy+/bH2JZfbeMje899571vPx48dr8eLF6tmzp6pVq6bAwEC7L3Qp4x9VN4uNjVWPHj3s7l5/9epV9evXL0vx3qrkCUNkZKT+/PPPLNXz+++/272+9957bymunPCf//zHLmncsGFDrm07efclR7vzJS+X0f1rckLyv+GuXbvsjtlb5Qqfx81J1oULF1Ik4Bk90rureVbieOGFFzIdgzHGad2xbkc5eVzce++9Wr58uU6fPq0FCxbo+eefV926de2Sm9WrV6tBgwb69ddf04zxkUcesboVLlu2zO7eNMnvLdOvX78MuzbXqFFD8+fP19mzZ7VkyRK99NJLatSokd0FuW3btqlFixb67rvv0q3LEXnlvIPrI5mBy0ietHz11VeSrg/ey2hAa2r+/vtvHTlyRNL1PrBp3Qjzhujo6Aybs2/20ksvWVfVSpUqZX1x//TTT04ZmH7zj5yvv/46S/XMnTvXeh4aGmp3VT8vuf/++63nad18Lick707k6ODp48ePW8+Dg4OzPaaMJB9QGxsbq++//z7b6naFzyMwMFDe3t7W6xvfDbkt+Q12nRUD/k9uHBfBwcHq2rWrpkyZom3btun06dP64IMPrOP+2rVreuqpp9Jcv0CBAurZs6ek612AZ86cKen6xbcbk6vYbDY98cQTDsfk7++v9u3b6+2339Zvv/2myMhIffXVV9Z3fVJSkp555hlrDFpW5ZXzDq6PZAYuo1WrVipRooTdsoceesjhPvPJJe+bXqlSpRSDJm+2ceNGGWMcrn/FihVWwuLm5qa5c+fa3d34pZdespsUIDeEhYWpTZs21uvPP/880xMBbN++XevWrbNe37iTeF7k4+NjPU/+DzOn1alTx3r+22+/ObRO8iuvGc32lROaNGliN0vPlClTHO5KmRFX+Tzuvvtu6/mKFStyZZs3a9CggfV83bp12dpClt3yS3ec3D4ugoOD9eyzz2rRokXWsn379uno0aNprpNaV7N58+ZZ3+/33XffLY3T9Pf3V58+fbR69WrruzQyMtJu8P4NmT0u8sJ5B9dHMgOX4e7urh49etgty2oXs+RN+VevXs2w/CeffOJw3WfPnlXfvn2t5Ofll19W06ZNNWDAAHXp0kXS9emab+6ClhuGDx9uPT916pRGjBjh8Lrx8fEaOHCgtV+FChWy+yea1+zcudN6nnzq0ZyWvJVj2bJl1lS7aYmIiLDGf928fm6x2Wx2rZNbtmzR5MmTM11PQkJCiqmUk+/Pjh07tGvXrnTruHbtmubNm5fq+jmpQ4cO1vNPP/1UMTExubLd5Bo3bmx1Gbp8+bKmTZuW6zE4KvnFgtt5kgBnHReNGjWym1zl9OnTaZatUaOGGjZsKOl668b69evtxs+kNfA/s8LCwuxmB0stpsweF3nhvIPrI5mBS3nttde0ZcsW65HegMb0hIWFWVeQ9u7dq7/++ivNst9++62WLFnicN1PPPGEIiIiJF2/N0ryaYGnTZumkiVLSrp+v5qXX345K+FnWatWrdS3b1/r9Ycffmg3digtCQkJ6tatm7Zt22Ytmzp1aoYzumWHuLg4DRkyxG46zowcOHDAbmac5PdxyWlt2rSxroLGxsbqhRdeSLOsMUbPPfec4uLiJF2/X1KrVq1yI8wUevbsaffD4uWXX9bnn3/u8PrHjx9Xy5YtrW4uN1SuXFlNmza1Xj/77LPp/sh5/fXXrQTQ398/xQWMnPLUU09ZicTJkyf1zDPPONwaGxkZaTcteFZ5e3vbHS+vvvpqhgPAk0vvB292Sz5986lTp3Jtu7ktu4+LjGbnu+H8+fN2k2dkNBvek08+aT1/5ZVXtHnzZknXu1HfuIiWXpyOSEhIsOuym1pMmT0u8sJ5B9dHMgOXEhgYaM2oUr9+/SzfqyU4ONjq0pGUlKRHHnkkxTSoSUlJ+uijj9SrVy+5u7vbXXFKy8cff2wlPn5+fpo7d67d4MmgoCDNnDnTSqQ++OCDXG9anzp1qmrWrGm9Hj58uLp27ZrqfXaMMVq/fr3uuusuLVy40Frev3//LLeKZdaNv0O5cuXUrVs3LVu2LM2rdwkJCfrmm2/UvHlzqz+3n5+fnn322VyJVbre6vfWW29Zr7/55hs9+eSTKe7fcunSJfXv399uIO3bb7+dqftMZCebzaaZM2eqQoUKkq73v3/qqafUsWNHbdmyJc0fGHv37tULL7ygihUrpjnL0VtvvWWdqxs2bFDXrl1TtFjFxcXptdde06RJk6xlo0aNyvabeKYlICDArjVqxowZ6tixow4ePJhqeWOMNm3apCFDhig0NPSWxw/cMHToUOvq96VLl3Tvvfdq2rRpVsJ7s6ioKH3xxReqV6+e3n333WyJwRHJ71ezYMGCTHXDdSXZfVw8+uijat++vb777rs0u/meOHFC3bp1s/7mFSpUUPny5dONs1u3btY9v5J3/+rZs2eG3WyHDx+uJk2aaObMmWmODT179qyeeOIJK5nx9/dPdRbK5MfF/Pnz092ulHfOO7g27jODfGvcuHFq06aNkpKStGPHDtWoUUONGzdW2bJldfnyZW3YsMH64h4/frw+//xzu4HJNztw4ICGDRtmvZ48eXKqd6pv2bKlXnzxRU2aNEnGGPXt21d79uzJtYHffn5+WrdunTp16mSNf1m4cKEWLlyomjVrqnLlygoICNDZs2e1ffv2FIO2//Of/9j94MwtcXFx+vbbb/Xtt9/K09NTtWvXVpkyZVS4cGElJCTo1KlT+uOPP+z+GXt4eOjLL7/M8KaK2e3RRx/V+vXrrZuFfvHFF/r222/VokULFStWTGfOnNHq1avtZsh74YUXMrz7dE4LCgrSpk2b1LVrV+vYWLJkiZYsWaI77rhD9erVU0hIiNzc3BQREaF9+/alOCdSm32sUaNGeuutt6xujosXL1bp0qXVokUL3XnnnTp//rzWrl1rd4W4c+fO+s9//pODe5tS3759dfToUY0dO1aStHTpUi1btkzVq1dX9erV5e/vrytXrujUqVPasWNHtt+TR7o++9uiRYvUqlUrhYeHKzo6WgMHDtTw4cPVqFEjlSxZUjabTefOndOBAwd06NAha3xTixYtsj2etHTp0kUjRoyQMUZLly5VzZo1dc8999j9/bt165Zr93fKSdl5XCQlJWnZsmVatmyZPD09Vb16dVWsWFEBAQG6dOmSjh8/rs2bN1t/U3d3d33wwQcZxujr66vHH388RZfoAQMGZLiuMUYbN27Uxo0b5e7urkqVKqlq1arWjWZPnjyp3377zS6hfu+991Kdbrxr16769NNPJV3vnr19+3bVrVvXurWCdH2sZbly5azXeeG8g4szQB7RrFkzI8lIMp988skt1fXYY49ZdY0aNSrNcp988onx8PCwyt78cHNzM2+88YZJSkoyoaGh1vLw8HC7emJjY03t2rWt9zt16pRufDeXf/DBB29pf9esWWMXtyNiY2PNe++9ZwoXLpzm/id/VKxY0fzwww85Fk9a4uPjTdeuXY2fn59Dcd54VKlSxaxZs+aWtp1cen//tIwdO9Z4e3unG6ePj4+ZMGFCuvUk/zybNWt26zuTgbi4OPPBBx+Y4sWLO/x5lytXzkyfPt0kJiamWe8XX3xh/P39063H3d3dPP/88yYhISHNesLDw63yoaGhDu1TZv5+3377rSlRooTD+3733XebmJiYFPWMGjXKoe+h1ERFRZlHHnnE2Gw2h2IIDAw0X331Vap1ZfZcTP5dnN45NHLkyHRjmjFjRqb2Oa2Ys3Iez5gxw1q/T58+qZbJ7HGUHcdFhw4dHF6/aNGi5scff3R4n3fs2GG3fv369R1ab8iQIQ7HVKhQIfP555+nW1/Pnj3TrSOtv6ezzjtHj/cbMns+IefRMoN8bdCgQWrcuLEmT56sNWvW6J9//lGBAgVUsmRJ3XfffXriiSfsZmNKy4gRI6wB58WLF89w4K6Xl5e+/vpr1atXT9euXdOiRYv06aefWnezzw1eXl4aOnSoBgwYoEWLFmnp0qXatWuXzpw5o0uXLikoKEh33HGHGjVqpPbt26tdu3YZzvqWEzw8PLRgwQJdu3ZNGzdu1IYNG7Rjxw4dOXJEERERunz5sry9veXv769y5cqpTp06euihh3Tfffc5rcvWDSNHjlSvXr30xRdfaMWKFQoPD9eFCxcUGBiosmXLqm3bthowYECuTlDgCE9PTz377LMaMGCAli9frpUrV2rz5s06c+aMIiMj5e7ursKFC6t8+fJq0KCB2rdvryZNmmQ4k1H//v310EMPadq0afrpp590+PBhnTt3ToUKFdKdd96pVq1a6YknnlDVqlVzaU9T9+ijj+qhhx7SvHnztGLFCm3ZskVnz57V5cuX5efnp5IlS6pKlSpq0qSJHnjggVRbYG9VUFCQ5s+fr7179+qbb77R2rVrFR4erqioKLm5uSkwMFDly5dX3bp11apVK7Vu3dqhrrDZaezYsWrcuLG+/PJLbd26VadPn3ZoQhVXlR3HxaJFi7Rjxw6tWrVKv//+uw4cOKCTJ0/qypUr8vb2VkhIiGrWrKkHHnhAPXr0sLqOOaJ27doqW7asNfOZI60y0vWxk88884x++eUXbd68Wfv27dOJEyd06dIleXh4qEiRIqpWrZratGmjXr16qWjRounWN2vWLLVv315z587Vzp07FRkZ6dDA/rxw3sE12Yy5TTu6AgAA5BPh4eEqV66cjDHy9fXVv//+m6lkCHBVTAAAAADg4qZPn25NxPDII4+QyCDfoGUGAADAhV27dk1lypSxZgnctGmTde8Z4HZHywwAAIALGzlypJXINGrUiEQG+QoTAAAAALiQn3/+WT///LOuXbum33//3e6GxhMnTnRiZEDuI5kBAABwIb/99luq9/saPny4mjVr5oSIAOchmQEAAHBRvr6+qlGjhp5++mn16dPH2eEAuY4JAAAAAAC4JCYAAAAAAOCSSGYAAAAAuCSSGQAAAAAuiWQGAAAAgEsimQEAAADgkkhmAAAAALgkkhkAAAAALolkBgAAAIBLIpkBAAAA4JJIZgAAAAC4JJIZAAAAAC6JZAYAAACASyKZAQAAAOCSPJwdAABkRlJSkhYtWqQVK1bot99+U0REhM6fPy8fHx8FBwerRo0aatSokbp06aKKFSumWkdCQoL27NmjP/74Q1u2bNEff/yh/fv3KzExUZIUHh6uMmXK5OJeAdkjO86P6OhoLVu2TKtWrdK2bdt09OhRXb16VQEBAapWrZo6dOigAQMGKDAwMHd3DgBSYTPGGGcHAQCOWLZsmYYOHaqDBw86VL5Zs2aaMGGC7rnnHrvlY8aM0ejRo9Ncj2QGrig7zo+ffvpJnTt3VmxsbLrrFitWTN98841atGhxSzEDwK2iZQaAS3j77bc1YsQI3bj+0rhxY3Xs2FF16tRRkSJFFBMTo9OnT+vXX3/V0qVLdejQIa1bt05vvvmmli9fbldX8ms4Pj4+ql27ts6ePau//vorV/cJyC7ZdX5ERUUpNjZWbm5uat26tdq1a6datWopMDBQJ0+e1Ny5c/Xtt9/q9OnT6tChg3799VfVrl3bSXsNALTMAHABs2bNUp8+fSRJwcHBmjt3rtq0aZNmeWOMFi9erBEjRujOO+9MkcysWLFCx44d01133aWaNWvKw8NDffv21cyZMyXRMgPXkp3nx7fffqs1a9bo1VdfVenSpVNd/8MPP9Rzzz0nSbrvvvu0atWqbNwbAMgckhkAedqpU6dUoUIFXbt2TX5+ftqyZYuqVKni0LoxMTFavHixHnnkkQzLkszAFeXW+XGzu+66S1u3bpWbm5vOnDmjIkWKZLoOAMgOzGYGIE/773//q2vXrkmSxo0b5/APNel6F7Ks/FADXIWzzo/mzZtLuj7hQHh4eJbqAIDsQDIDIM8yxmjWrFmSpIIFC6p///5OjgjIO5x5fiSfIMDNjZ8SAJyHbyAAeda+ffsUGRkpSWrSpIkKFSrk5IiAvMOZ58e6deskSR4eHipfvnyubRcAbkYyAyDP2r17t/W8bt26TowEyHucdX4sXbrU2nbbtm3l7++fa9sGgJsxNTOAPOvGVWdJCgkJSbfsvn37lNZ8JmFhYfLz88vW2ABnc8b5ce7cOQ0ePFiS5O7urrFjxzoYLQDkDJIZAHnWpUuXrOcFCxZMt2ytWrWUmJiY6ntr1qyxBiwDt4vcPj8SExP1+OOP6/jx45KkkSNHqk6dOo4HDAA5gG5mAPKs5GMArly54sRIgLwnt8+PZ555xronTfv27fX666/n+DYBICMkMwDyrOT3rjh79my6ZRMSEmSMsR6jRo3K6fAAp8rN82PEiBH6/PPPJUn33nuvvvvuO7m7u2c+aADIZiQzAPKsWrVqWc+3b9/uxEiAvCe3zo+3335bb731lqTrEw0sWbJEBQoUyLHtAUBmkMwAyLOqVatmXX3esGEDXc2AZHLj/Pj444/1yiuvSJKqVKmiFStWKCAgINu3AwBZRTIDIM+y2Wzq3bu3pOuDnb/66ivnBgTkITl9fsyePVtDhgyRJJUtW1a//PKLgoODs3UbAHCrSGYA5Gkvvvii1aXl1Vdf1Z9//unkiIC8I6fOj4ULF6pfv34yxqhUqVJatWqVSpQokS11A0B2IpkBkKeVKlVKH330kSQpOjpaTZo00dq1azNc7/z58zkcGeB8OXF+/Pzzz+revbsSExNVtGhR/fLLLypTpkw2RQwA2Yv7zADI8/r166dTp07pjTfeUEREhFq0aKGmTZvqwQcfVM2aNVWkSBEZY3TmzBnt2rVLP/zwg/744w9r/ZsHK1++fFkLFiywW5b8ivaCBQvsutPUrl1btWvXzpmdA25Rdp4fmzdvVufOnRUXFydPT09NnjxZ8fHx2rt3b5rbL1WqlAIDA3NyFwEgTTaT1i2BASCPWbx4sYYOHaojR444VL5x48Z6++231bhxY7vlx44dU1hYmMPbHTVqlEaPHp2ZUIFclx3nx+jRozVmzJhMbXfGjBnq27dvptYBgOxCNzMALqNjx446cOCAFi5cqIEDB6pGjRoKCQmRh4eHChUqpNDQUD3wwAMaPXq09u3bp40bN6ZIZHKaI3dSz4r33ntPlSpVUtWqVTV58mRJkjFGgwYNUvny5VW/fn399ddfVvk333xT5cuXV7Vq1ayr8GvXrtXDDz+cI/HdipiYGHXp0kXly5dXixYtFBkZmaJMZvd1586datiwoapXr6569eo51PUqL8nKceQK5wcAZDsDAMg2zZo1S/f9hIQEEx0dnak6V65caR544AETFxdnjDHm9OnTxhhjFi1aZLp27WqMMebHH3+0nu/evds0aNDAxMfHmx07dpj69esbY4xZs2aNVSYv+eCDD8zQoUONMcZMmTLFep5cZvf18OHD5s8//zTGGHPgwAETFhaWG7vikAsXLpjExMR0y2R0HAEArqNlBgBywd9//63Ro0erUqVK2rFjR6bW/eyzzzRixAh5enpKkooWLSrpereiXr16Sbp+Vf7XX3+VMUaLFy9W9+7d5eHhodq1aysuLk7//vuvXZ2rV6/WPffco6ioKPXt21dDhgxR8+bNVaFCBW3atEndunVTxYoVNWLEiGzY+/Ql34/evXtr8eLF6ZZxZF8rVKigcuXKSZIqVaqky5cvKzEx0a7OxMRE9ezZU1WrVlWNGjU0Y8YMSdIff/yhJk2aqG7duuratasuX74sSfrtt9/UoEED1apVSy1btszy/m7YsEFVqlTRhAkTFBERkeV6AAB0MwOAHJOYmKglS5aoY8eO6tixowoXLqw//vhDTZs2zVQ9R44c0S+//KK7775bbdq00eHDhyVJ//zzj0qWLClJcnNzU1BQkKKiouyWS9cHaJ86dcp6vWrVKo0cOVKLFy+2brp46dIlrV27VqNHj1bHjh319ttva+/evZo3b16q3b46d+5sTYyQ/LF161ZJ0uDBg1N9f9GiRSnqSh5v4cKFdeHChXTLZGZfJemHH35QvXr15O7ubrd8586dCg8P1/79+7Vnzx516dJFcXFxGjZsmBYtWqTt27erYcOGmjp1qmJjY9WzZ09Nnz5du3bt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g86fP682bdtp+/Ztuq//62rxxGuK+vuI5r/+uCL+3KMxY8bo5Zdf5io2LImJiXrrrbc0evRo3VGhph59c46K3Fleq6eP15ovx6lu3Xr6ecVyFS5c2NmhAoBLIpkBAAcdOXJEFStWVMnKddVn8mL5h5SQJCXEx+mXz0dr/cy31aBhI82dM1thYWFOjhbOdvToUT3es5f++H2zmvV5RS0HjpKHp5ckKfrsP5r5n446dXC7Dh8+rAoVKjg5WgBwTYziAwAHVahQQcuXL1fChX/1frca2rPqe0mSh6eX2g2eoIGfr9PhY6dUo2ZNffXVVzk+MBt5kzFGM2bMUM1atXTkxL8a+Pl6tR083kpk9vyyQO93q6GEC/9qxYoVJDIAcAtomQGATIqKitLAgU9p4cLvVbdDHz047AP5FPSXJMVcjtbiSc9r2+Kv1LlzF33++WcKDg52csTILZGRkXryyYH68ccfVK9jX3Uc+r7dsbHovee0fclMde36sD777FMVKVLEyREDgGsjmQGALDDGaNasWRry7LPyLlREj7w5W2Vq32u9v2fV9/pxwkD5+Xhp1syv1LZtWydGi9ywfPly9enbT1dj49Xp1c9V/b4u1nvhOzZowajeir0UpY+mTlWvXr0YYA8A2YBuZgCQBTabTX369NHuXbtUKayUPh/YTMs/elUJ8XGSpBotu+r5eXtUuGwttWvXTkOGPKurV686OWrkhKtXr2rw4CG6//77VbhcbT03b4+VyCTEx2n51BH6fGAzVQorpT27d6t3794kMgCQTWiZAYBblJiYqHfffVevv/66NWNV0bAqkq634Gya/5F++mA4UzjfhrZt26Yej/dU+LFjeuD599TwkWesROX00f367o2eOv3XXr355psaPnw4M90BQDajZQYAbpG7u7teeeUV/f777/Iz1/Rhz7r67dupMsbIZrPpnseG6Nk523XVVkANGjTQxIkTlZiY6OywcQsSExM1YcIENWzYUFfdfPXsnO1q9Ohg2Ww2GWP067wPNbVXPfkpRr///rteeeUVEhkAyAG0zABANrp27ZpeeullTZ36oSo1aquub3yZ6hTODRvdozmzZzGFswsKDw/X4z176ffNm1KdcnnBmH46vPlnDRnyrN55520VKFDAyREDwO2LZAYAcsCKFSvUp28/Xb4Wq06vfq4aLbta7x3buVHfvdFL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aZK1/po1a4wks2bNGrvthIeHG0lmxowZ1rI+ffoYLy8vU6VKFfP+++/b7feIESNMxYoVzfTp082KFStMhw4djCSzdevWDD/nm/fp4MGDplChQqZcuXJm1qxZZunSpaZ79+5Gknn77bdTxH7nnXeahx56yCxevNjMmTPHlC9f3vj7+5u//vrLKlupUiVTvnx5M3v2bLNu3Trz/fffm6FDh6bY7+QuXbpkPD09zYQJE6xlgwYNMgUKFDB+fn4mLi7OGHP9KrjNZjMff/yx3T6VKVPG3H333Wb+/Plm2bJlpnnz5sbDw8Murn379pmAgABTo0YNM2vWLPPzzz+boUOHGjc3NzN69OgU+1qmTBnz+OOPm6VLl5pvvvnGlC5d2lSoUMEkJCSk+xnfd999JigoyCxfvtzUqlXLuLu7m5CQEPPUU0/ZtcAcPHjQSDIfffRRijqGDRtmbDabuXbtmjHGmFdeecVIMpcvX05RNjg42HTv3j3dmG5umdmzZ4+58847TaNGjczZs2etcjl13PXt29dMnz7drFy50qxcudKMHTvWFChQwIwZM8auXGhoqClZsqQpXbq0+fLLL82yZcvM448/biSZd9991yqXmfNdkilZsqSpWbOm+frrr83q1avNzp07zfLly40k079/f7Np0yazadMm8+eff9qt16xZswz3LTUXLlwwAQEBpnPnzinemzZtmgkICDCnTp2ytnNzy0x8fLypV6+eqVatmvnjjz/MpUuXzLZt20zt2rVN3bp1rfMBgPOQzACwcyOZSf7j3xhjateunaJbRXx8vAkJCTFdunRJs76EhAQTHx9v+vfvb+rUqWP3XkxMjKlTp44JCwsz+/fvN8WKFTPNmjVL8SM1rWTm2WeftSvXqVMnI8n897//TRF73bp1rdeZTWZuTthu7Lcks337dmt5VFSUcXd3Ny+++GKan0da+9StWzfj7e1tTpw4YVfu/vvvN76+vubChQt2sdetW9ckJSVZ5W50iRowYIAx5np3KElmypQpGcZys3vvvdfcd9991uvy5cub4cOHGzc3N7Nu3TpjjDFz5841kszhw4ft9qlYsWImOjraWhYREWHc3NzMxIkTrWVt27Y1pUqVsksojDFmyJAhxsfHx5w7d85uXx944AG7cvPnzzeSzKZNm9Ldj0qVKhkfHx9TqFAhM2HCBLNmzRrzzjvv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "create_base()\n", + "plt.text(1.5, 4, 'Max IOPS for Coherent Filesystem',fontsize=24)\n", + "plt.text(3.3, 3.3, '|<-- 600km == 0.002 sec. -->|', fontsize=7)\n", + "plt.text(2.5, 1.0, f\"maximum iops when {distance} km apart: {int(max_iops)}\", fontsize=12)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6ea5f011", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Pick any 2, Which two?\n", + "\n", + "* CA: Consistency and Availability\n", + "* AP: Availabilty and Partition Tolerance.\n", + "* CP: Consistency and Partition Tolerance" + ] + }, + { + "cell_type": "markdown", + "id": "26bebd15", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing CA:\n", + "* At best, you need a quorum system.\n", + "* quorum means obtaining a consensus among the existing nodes about the current state.\n", + "* usually means voting for primaries (at best, sharded masters) and triggering a pause to vote for a new primary whenever it goes down.\n", + "* The system as a whole, cannot respond to queries while voting is in progress, and so is unavailable.\n", + "* but at least it is up after the vote is done, in spite of the formerly primary node dying.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b843815d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Quorum and Voting:\n", + "* Multiple Algorithms:\n", + " * Paxos: \"Paxos is exceptionally difficult to understand\"\n", + " * \"few people were comfortable with Paxos even among seasoned researchers\"\n", + "\n", + "* papers like \"In Search of an Understandable Consensus Algorithm (Extended Version)\" resulted in RAFT\n", + "\n", + "* Things you want:\n", + " * quickly convergent?\n", + " * a single leader elected?\n", + " * low overhead once leader in place.\n", + " * quick detection of loss of leader.\n", + " * known behaviour when: flapping, partitioning, WAN.\n", + "\n", + "* If it partitions, tradeoffs:\n", + " * AP: both sides will elect new leaders and \"fork\" the file system (writing inconsistently thereafter.) OR:\n", + " * CA: One side will know it has a minority of nodes and shutdown (losing a lot of nodes, but hoping the other side is still up.) OR:\n", + " * CP: both sides refuse to write. (no A for writing.)\n", + " \n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "1a58bd02", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing AP:\n", + "\n", + "* incoming requests are routed to the nodes that remain up. They provide the data they have.\n", + "* you don't need a master. nodes receive updates, and tell the others, \n", + "* changes propagate through the cluster... eventually consistent.\n", + "* no voting, no loss of availability." + ] + }, + { + "cell_type": "markdown", + "id": "9499457e", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Voting Failure Modes\n", + "\n", + "\n", + "| Number of Masters | < 1 | 1 | >1 |\n", + "|-------------------|----------------------------|--------------------------|--------------|\n", + "| All Talk | Down wait | Up OK | Bickering |\n", + "| Partitioned | Unstable LR | Unstable LR | LR |\n", + "\n", + "(LR... partitioned into left and right clusters.)\n" + ] + }, + { + "cell_type": "markdown", + "id": "c2fb9b08", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing CP:\n", + " * as long as the backups can reach the primary, all can answer read requests.\n", + " * when a write is received, the backup passes it to the primary.\n", + " * when a backup loses access to the primary, for coherency, it must stop answering. It no longer knows.\n", + " * A Manual intervention would be required to tell the backup that it is now the primary in order for availability to be restored, because there is no way to tell the difference between a primary failure and a network partition.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a1b6bb55", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bringing Amdahl & CAP Together\n", + "\n", + "* Amdahl's Law is about dealing with large numbers of processors sharing resources.\n", + " \n", + "* CAP is about tradeoffs inherent using different methods of sharing resources." + ] + }, + { + "cell_type": "markdown", + "id": "f335b812", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Almost-Coherent File System or DB Performance\n", + "\n", + "* A bandaid on Traditional single node \n", + "* a synchronized file system is going to have ordered operations, it is going to be a 1:1 connection with shared state.\n", + "* usually a \"journal\" to sync between sides.\n", + "* in the meantime, readers on the wrong side get the _wrong_ data. \n", + "* sometimes you establish an upper bound (if you hit it, you hang so that it doesn't get too out of sync.)\n", + "* synchronization is a struggle against Amdahl's Law.\n", + "* will not scale." + ] + }, + { + "cell_type": "markdown", + "id": "58fa5584", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Universal Write Scaling\n", + "\n", + "* obtain delegation for a subdomain, write locally.\n", + "\n", + " * write behind cache ... the write will get to the other side eventually, but G1 and G2 will be out of synch for a while. \n", + "\n", + " * sharding (dividing domain so every node is writing locally.) \n", + " * route requests to appropriate shard.\n", + "\n", + "* locks over smaller subsets --> more write parallelism.\n", + "* writes must be localized (no solution for scaled distributed writes.)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "3df85e5c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Non-Shared Approaches.\n", + "\n", + "* the only thing that does scale.\n", + "* transfer files or objects, rather than synchronizing file systems. maximally distributed synchronization.\n", + " * explicit sharding.\n", + "* any number of transfers can occur in parallel.\n", + "* the underlying file systems are local stores.\n", + " * are not the same, they just contain the same files, eventually.\n", + " * are not synchronized at any point, they receive a sum of changes that add up to the same stored state.\n", + "* lag can occur, but can run an unlimited number of processes to do transfers, none limited by propagation delay. Transfers occur in parallel to the degree desired.\n", + "* Because the transfers are independent, no fundamental performance limit, beyond the perfomance of hardware on each end, and the pipe between.\n" + ] + }, + { + "cell_type": "markdown", + "id": "4b5061d6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "## Examples of Non-Shared Approaches\n", + "\n", + "### Object Stores\n", + "\n", + "* An object is a self-contained item that can be distributed.\n", + "* Every object is equivalent to a db or a file system. \n", + "* with many objects, state is distributed among subsets of nodes, not global.\n", + "* distribution of objects (with lag) is much easier to achieve than a coherent view.\n", + "* Different Object Stores lean in different directions in CAP.\n", + "* requires application adaptation, perhaps profound.\n" + ] + }, + { + "attachments": { + "CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAIiAtgDASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAUGBAcIAwEC/8QAWhAAAQQBAgQDBAYEBg0HCwUAAQACAwQFBhEHEiExE0FRFCJhcQgVMkKBkSNSYqEWM3JzgrEJJDQ3Q1N0g5KisrPBFzhjdZOj0iU2REVXdpSVtNHwGDVUwtP/xAAbAQEAAgMBAQAAAAAAAAAAAAAAAQMCBAUGB//EADQRAQACAgEDBAAEAwcFAQAAAAABAgMRBBIhMQUTQVEGImGBFDJxFSMzQpGhsVLR4fDxJP/aAAwDAQACEQMRAD8A4yREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREF+4HcLs9xY1kNP4WSKtHFH4925MCY68W4G+w+04k7BvTc+YAJG989gvokcM8k/T2fbndYZattFcdBPI5scn3usb42Ag92guLex6hS/0dJXcPvoX6y19j2Mgy9185htbe8OXlgh/wBGRzyPi4rjVzi5xc4kknck+aDoXivoXgZmeGuT13wl1RPTtYsxOtYK9IS8sfK2P3BJ7+4LwdwXtO23QrnhFeOBWjXa94tad0sYy6vattdb28q7Pfl6+XuNIHxIQdT8J/o08OMnwkwtbVNZ7NbZ3EzZCCU3JWOhaeXlIiDg0iMTQ8wIPVx/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hMhmPB1FkLsslSl7PK4ytcIADzhpYPsO7kdviFo7RlytjtYYXIXH+HWq5CCaZ+xdysbI1zjsOp6A9kHQH9kZAHHDE7DbfTkG/wD8RZXM63n9NXXeleIXFXHZnSGVGToQYWGrJMIZIgJRNO8t2e1pPR7eu23VaMQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQERTeldKal1VadW03gcllpGDeQVKzpBGPVxA2aPidghraFRbgw3AbOH39TaiwOBaDs6Bs5u2Nvg2AOYD8HPaVbMZwl4aY4A3LGpdQStP2jJFQiPzYBK4j5PCqtnx18y3sPpvKzfyUlzn5ourqGE0RjC04zh9p2N7dvftMluFxHmRPI5n5NA+Gyl62YsUz/5Op4jG9Nv7QxVat09P0bG+g/IKmebjjw6WP8Ocq380xDj+OCeRu8cMjx23a0lZowOdI3GFyRHwqv8A/suvn6w1Y/vqfM/hekH9RWMdQ59zi52cyZJ6km2/r+9YTzq/EL6/hnL83hyDYx9+u8ssUrMLmjctfE5pH5hY23w2XZsWrNUxNDY9SZhjW9mi7IB+XMvzb1Lmro2yNwZEbbEXoWWRt8RI126n+Or9MZ/DOf4tDjT8UXWt6ppvIsDMpofSloDrvHi2VHH5ureGT+agMnw34XZMk/UOYwjyOhxuT542n1LJ2vcR8PEHzVleXjlq5PQOZTxET/SXNP4pst25bgRVmHNpnXNGZ2xPs+XqPpvJ8mtewyxn5ucwKhav4Za60rWdczGnbYoN6e31i2zV/wC2iLmA/Anf4K+uStvEuXl4ubDP95WYU5ERZKBERAREQERe1StYuWY6tSvLYnkPKyOJhc9x9AB1JQeS+LZeL4Ka3mAkzkWP0rCRuTm7IglHTf8AucB0/wD3asNDhboShscxqnMZqQfaixdNtWI/KaYud+cIVd81KeZRMxDSaLoitiOHePj5KHD6pZcO02VyFixJ+Ubo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jOzmOHmFfgz2xT+iYnTnNfFubjHoullcZY1/papHW8NzTnsXC3ZtZ7jt7VC0DYQPcQC3/BvO32XN20yu1S8XjcLYnYiIsgREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERB9CeSLbf0d9F1snkJ9aZ+rHYwmFka2GtMzdl+6RvHCR2LGj9I/4BrT9sKLWisblZixWy3ilY7yvfB7R/wDAHTjM7fjaNVZitvC1w9/GU5B/qzStPXzbGduhe4CeXtetWL1ya5bldNPO8ySPd3c4ncleK4ubLOS25fR/T+DTh4YpHn5kREVTfEREBERAREQEREBERAREQEREBEWbgsZYzGVgx1Ysa+V3V7zs2NoG7nuPk1oBJPoE8sb2ilZtbxCU0jia0rJc3lo3OxdNwaYwdjZlPVsQPp03cfJo9SF+8tfs5PIS3bTgZJD2A2a0AbBoHkAAAB5ABZeobtWV8OPxgczF0WmOsD0Mh+9K79p56n0Gw7AKKWtmybnpjw8fy+VPJydU+PgREVDVEREBERAREQfqWvWyNGTF3jywyHmil23MEnk/5eRHmPiAtd5KnYx16albj8OaF3K8f8R6g9wfMLYSw9UY4ZfEmzG3e/Rj36d5YB1I+be4/Z39Arsd/iXP5vH3HXVQURFe5YiIgIiICIiAiIgIiICIiAiIgIiIM7B5Szh8lHeqiN5aHMkilZzxzRuBa+N7T0cxzSWkeYK1jxo0RW07frZ/TzXu0xmC51QFxe6nMOslSQ/rM3BaT9pha7vzAbAUpiDi79C9pfURP1HlmtZNIG8zqkzd/CssH6zCTuB9pjnt+8tri5/btqfEsqzpzMil9YaeyWldTX9PZeNrLlKUxvLDux47tew/eY5pDmnza4HzUQuysEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQSemcLkdR6hoYHEwePevzsggZvsC5x23J8gO5PYAErqyerjMNjKGl8E7nxWHjMMMvLsbUpO8thw9Xu6j0aGN+6tefRwwAw+nMjruzGBcumTF4guHVjOUe0zD091zYgf25PMK8rn8zL/kh678O8HUTyL/ALCIi571giIgIiICIiAiIgIiICIiAiIgIiICuNKI4DSzT9nJZmPmd5GKpv7o+chHN/Ja39ZQ2kMX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ucDuBJKGck9Jx7R2Y9z4bvIO3LHfdceoEQt+totG4cS9LUnVoERFkwEREBERAREQEREBERAREQEREBERAREQFn4PE381fFLHwiSQMdJI5zgyOKNo3dI97iGsY0dS4kAL3xuGY7Fy53NX4cLgIHcsuQsAkPd/i4WD3ppf2G9u7i0dVrviPxKdmMfJpnStWbD6ZLwZWPcDayDmndr7Lx0I36iJvuN6facOY0Zc8U7R5bnG4lss7ntCa4jcQqGJxtrSOg7ZmbZjdBmM41pa640nrBX36sr9OrujpPPZvunTy+r4tC1ptO5dulK0jVRERYsxERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQTGlNSZzSuYZltP5Kahba0sLmbFsjD3Y9p3a9h82uBB8wtw6f1ho7WobDcNTR+oXdw9xGLtu/ZcdzWcfR28f7TBsFodfFnS9qTuFWXDTLGrQ6DzeIyWFueyZSnLVlLQ9ocNw9p7Oa4dHNPk4Eg+RWCqFojibqTTFEYh3s2awXNzHFZNhlgaT3MZBD4XdftRuaT579lsbD5zQOq+UYrLnTORf3x2alHs5d6R2wA3b+dbHt+s7utzHyIt5crNwL0717wxkUjnMFl8JLHHlcfPVEreeF7hvHM39Zjxu17fi0kKOWxExLQms1nUiIilAiIgIiICIiAiIgIiICLOwmHyubueyYjHWb0+25ZDGXco9Tt2HxPRfMxf0RpRp/hFnxlsg3/1VgpGTEH0ls9Yo/6HikeYCwvkrXzK7Hhvkn8sPxicdfy1+OhjKc9y1IfcihYXuP4Dy+K+5/UGjdCgtvS19VagZ2xtObejWdt/6ROw/pCD3jiO3QgyNPRa/wBZcVc/m8fPhcTDBpvAzdJKGOLgbDfLx5SS+b5OPID1DWqgFaeTkTbtXs6mDgVp3v3lP621hn9ZZRt/PXjOYmeHWgjaI4K0flHFG3ZrG/ADqep3JJUAEXxazoeBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREFq0dxA1fpKJ1bB5ueKjId5aEzWz1JT6ugkDo3H4lu/wAVecbxV0pk+WPVGj5MbKdgbun5yB8XOrTFzXH4NkjHwWnUHdZVvaviVd8VL/zRt0JQGkM0R/B/XeHe8jcVstvjph8zJ+g/KUrNvaP1PTq+2SYS5JTJ2Fuuzx4HfKVnMw/gVzcVnYbL5bC3G3MPlL2OsMO7Zqlh0Tx8nNIKvrybR5al/T8c+OzcxBBII2I7goqXU4y8SIf7p1I7LAjY/W9SDIEj52GPKz6/GfKkk5DSGjciTv1fj5K/XfffavJGPX4dfltbHKj5hrT6bb4ssqKBPF7HSAeLw3wDCPOvdut3+fPM792y+f8AK1iv/Z3i/wD5ha/8an+Kow/s7L9wn0UCOLuNYCY+G+Ckcf8AH3rjmj8GytO/4ryscZr+22P0To7Hnyc2pPYPcd/HmkHlt28yn8VVMenZPtY1NY3SepsjXNqngsg+qPtWTA5sLfi6R2zR+JWs7XGjiNIA2pnYcQBvscTjq1Fw/pwRtd+9VDPagz2oLPtOdzeSys/+Mu2nzO/N5JWE8r6hdX03/qs3nfr6Zwu/8I9c4Cm9vevRmORnPwHs/NGD8HyNVfyHE/ROKaW6e0rdztgdrOcn8GH5+zwO5vzmI+C0yiptnvb5bWPh4qfG1v1jxJ1lqmp9X5LLuhxYO7cbRiZVqA+RMMQa1x6/acC74qnr6iq22YiI8PiIihIiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg/9k=" + } + }, + "cell_type": "markdown", + "id": "5c040b89", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "![CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg](attachment:CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg)\n", + "\n", + "* Source: Fotis Nikolaidis from \"Tromos : a software development kit for virtual storage\n", + "systems\" (PhD thesis 2020/01/17)" + ] + }, + { + "cell_type": "markdown", + "id": "2f097463", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Cloud Solves This!\n", + "\n", + "* cloud vendors talk about \"availability zones\" there is no HA or synch across availabilty zones, they talk about times like 15 minutes, to do periodic synching. Object syncing takes time (and money.)\n", + "* 2006, HP DC Consolidation project established 200 miles as the longest distance between Data Centres. So that would give them 500 IOPS or so on a synchronous store.\n", + "* Note, putting a file system or a db inside a single object store means no scaling. \n", + "* Individual objects don't scale, using many objects does." + ] + }, + { + "cell_type": "markdown", + "id": "d4c68303", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "from: https://aws.amazon.com/rds/features/multi-az/\n", + "\n", + "* *Failover time depends on length of replica lag*\n", + "* *Any data updates that occurred after the latest restorable time (typically within the last 5 minutes) will not be available*\n", + "\n", + "From Google: https://cloud.google.com/compute/docs/disks#repds\n", + "* *Regional persistent disks have storage qualities that are similar to zonal persistent disks. However, regional persistent disks provide durable storage and replication of data between two zones in the same region.*\n", + "* *Zonal SSD PD multi-writer mode Throughput per GB (MB/s): 0.48*\n", + "\n", + "\n", + "the google performance numbers are within a zone (multi-DC but not too far apart.) not multi-zone.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2f016ef3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "### Sarracenia: File Systems Flying in Formation\n", + "\n", + "* Another distributed approach\n", + "* transfers files between file systems without synchronizing.\n", + "* Every file is equivalent to an object, a db or a file system.\n", + "* no locking, architecturally unlimited scaling.\n", + "* generalized buffering and parallelism of transfer at scale.\n", + "* file systems at all nodes are totally independent, limited only by local hardware on each node. \n", + "* transfers limited by bandwidth, not latency.\n", + "* works with legacy code un-changed, or new (agnostic.)\n", + "* In terms of CAP--> heavily AP.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7ee8541d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Thanks!" + ] + }, + { + "cell_type": "markdown", + "id": "23f98a0c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Sources:\n", + "\n", + "* https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/ (An Illustrated Proof of the CAP Theorem, referring to Gilbert & Lynch's paper which is a dead link. )\n", + "* https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing\n", + "* https://www.youtube.com/watch?v=eWMgsk7mpFc&ab_channel=IBMTechnology\n", + "* https://eax360.com/distributed-coordination-algorithms/ - Distributed Coordination Algorithms\n", + "* https://raft.github.io/raft.pdf In search of an Understandable Consensus Algorithm (Extended Version) Diego Ongaro and John Ousterhout.\n", + "* https://www.cs.ubc.ca/~bestchai/teaching/cs416_2020w2/lectures/lecture-mar23.pdf - fun practical talk about practical CAP.\n", + "* https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf - Impossibility of Distributed Consensus with One Faulty\n", + "Process - MICHAEL J. FISCHER, NANCY A. LYNCH, MICHAEL S. PATERSON\n", + "\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/PDS_Algorithm.html b/Contribution/Philosophy/PDS_Algorithm.html new file mode 100644 index 000000000..333cbc6ff --- /dev/null +++ b/Contribution/Philosophy/PDS_Algorithm.html @@ -0,0 +1,553 @@ + + + + + + + PDS Algorithmic Design — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

PDS Algorithmic Design

+

The PDS (Product Distribution System) is an obsolete data transfer engine gradually being replaced, but still deployed in a few Storm Prediction Centres (offices of Environment and Climate Change Canada.) other systems place files in input directories, and PDS has an array of configurations identifying which other destinations files need to be sent to.

+

It is a sort of inspiration for Sundew. The lack of source code meant that it’s functionality was deduced and reverse engineered to to produce later products. It is a design of it’s time, the 1980’s for use on large UNIX servers.

+
    +
  • pdsReceiver, scans a directory feeds things to dispatcher.

  • +
  • pdsDispatcher, scans sender configurations, and queues files for senders.

  • +
  • pdsSender… does the sending.

  • +
+

The initial deployments were on HP-UX HP-9000 UNIX servers, but it was ported to Linux in the early 2000’s, without substantial changes.

+
+
[105]:
+
+
+
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow
+
+def directory_polygon(x,y,box_bg,arrow1):
+   return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),
+               Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),
+               Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),
+            FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2),
+            Circle((x+2.5, y+0.6), 0.5, fc=box_bg)
+          ]
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(15, 15), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 9)
+    ax.set_ylim(0, 6)
+
+    x=0
+    y=3.6
+    patches = []
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    y=0
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    y=1.8
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+
+    patches.extend( [ FancyArrow(3.1,2.4,0.35, 0, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                      Circle((4.2, 2.4), 0.5, fc=box_bg),
+                     FancyArrow(3.0,1.2,0.35, 0.4, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                     FancyArrow(3.0,3.8,0.35, -0.4, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                     FancyArrow(5.1,2.4,0.35, 0, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                     FancyArrow(5.1,1.8,0.35, -0.4, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                     FancyArrow(5.1,3.1,0.35, 0.4, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2 ),
+                     Circle((6.2, 3.9), 0.5, fc=box_bg),
+                     Circle((6.2, 2.4), 0.5, fc=box_bg),
+                     Circle((6.2, 1.1), 0.5, fc=box_bg)
+                    ])
+    for p in patches:
+        ax.add_patch(p)
+    plt.text(3.8,2.35, 'Dispatcher', fontsize=18)
+    plt.text(2.2,0.55, 'Receiver', fontsize=18)
+    plt.text(2.2,2.35, 'Receiver', fontsize=18)
+    plt.text(2.2,4.15, 'Receiver', fontsize=18)
+    plt.text(5.9,1.05, 'Sender', fontsize=18)
+    plt.text(5.9,2.35, 'Sender', fontsize=18)
+    plt.text(5.9,3.85, 'Sender', fontsize=18)
+create_base()
+plt.text(3.0, 5, 'PDS Component Design',fontsize=36)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_PDS_Algorithm_1_0.png +
+
+
+

Receiver

+

Scans an input directory, uses IPC to tell the dispatcher about it. Log activities.

+
+
+

Dispatcher

+

Processes the files arriving as notified by the dispatcher using IPC.

+

For each file:

+
    +
  • Examine the accept/reject (imask/emask) rules for each sending process. The decision to put files in a sender directory was based on evaluating sequential regular expression patterns for each sender. Assume 10 regular expressions per sender.

  • +
  • As a single dispatcher is running, it can assign ensure file names are unique then they are placed in sender directories.

  • +
+

place a file in the directory each sender uses. Log activities.

+
+
+

Sender

+

Scan the sending directory, send files, remove them. Log activities.

+
+
+

Design Decisions/Assumptions

+
    +
  • Developers were asked to provide a single log for the entire system.

  • +
  • on the order of 10 receivers.

  • +
  • on the order of 100 senders.

  • +
  • Assume 10 regex’s per sender.

  • +
  • it was the eighties… so 1 or two cpus. Servers were very expensive, clustering for HA was a thing, but scaling was accomplished by bigger, more expensive servers.

  • +
+
+
+

Routing 1 Product

+
    +
  • PDS dispatcher routing for all receivers at once.)

  • +
  • represents a scale out vs. previous deployments.

  • +
  • units of as the number of regular expressions evaluated. (not MIPS.)

  • +
+
+
[115]:
+
+
+
+S=100  # number of senders to route to.
+re=100 # number of regex's per sender to evaluate.
+R=10   # number of receivers.
+Rp=S*re/2
+RE=100
+s=10 # number of senders selected by pre-routing phase.
+
+print( f"PDS pre-routing cost {int(Rp)} regular exppression evaluations")
+
+print( f"Assuming an average regex costs about the same as 500 instructions")
+print( f"then that means the cost to route 1 product is, on average, about {int(Rp*RE)} instructions.")
+
+Rpc= s* RE*re/2
+
+print( f"Then add client-side second evaluation of RE's by sender: {Rpc}")
+
+
+
+
+
+
+
+
+PDS pre-routing cost 5000 regular exppression evaluations
+Assuming an average regex costs about the same as 500 instructions
+then that means the cost to route 1 product is, on average, about 500000 instructions.
+Then add client-side second evaluation of RE's by sender: 50000.0
+
+
+
+
+

Observations:

+
    +
  • modern systems have many more cores (16 is common on single servers… ) but also clustering is common. Clustering is scaling with multiple servers, rather than more powerful individual servers.

  • +
  • In order to ensure log entries were not corrupted by different processes writing to the log at once, a locking mechanism was used to mediate access.

  • +
  • The dispatcher is a single process doing all of the routing.

  • +
+

2004, there was a project to use PDS to replace the existing Tandem Apps application. It didn’t work, PDS was too slow, so that motivated some analysis of how PDS worked. The work can be used as motivation for a discussion of application design.

+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/PDS_Algorithm.ipynb b/Contribution/Philosophy/PDS_Algorithm.ipynb new file mode 100644 index 000000000..7e58ed10c --- /dev/null +++ b/Contribution/Philosophy/PDS_Algorithm.ipynb @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0cc09567", + "metadata": {}, + "source": [ + "PDS Algorithmic Design\n", + "====================\n", + "\n", + "The PDS (Product Distribution System) is an obsolete data transfer engine gradually being replaced, but still deployed in a few Storm Prediction Centres (offices of Environment and Climate Change Canada.) other systems place files in input directories, and PDS has an array of configurations identifying which other destinations files need to be sent to.\n", + "\n", + "It is a sort of inspiration for Sundew. The lack of source code meant that it's functionality was deduced and reverse engineered to to produce later products.\n", + "It is a design of it's time, the 1980's for use on large UNIX servers.\n", + "\n", + "\n", + "* pdsReceiver, scans a directory feeds things to dispatcher.\n", + "* pdsDispatcher, scans sender configurations, and queues files for senders.\n", + "* pdsSender... does the sending.\n", + "\n", + "The initial deployments were on HP-UX HP-9000 UNIX servers, but it was ported to Linux in the early 2000's, without substantial changes." + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "id": "b43b0c60", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU5f7+8XvTQ4CEBAKhWpCmKCBNDlIEQYoewQLqUfGIHewNC4JIs4EH7HpEj2JBRAELoPQmVVCqlNDSE9KzKZv5/cGX/Ai7CbvJJjNJ3q/ryqU8uzPz2c2k3fPM57EZhmEIAAAAAAAAAABYio/ZBQAAAAAAAAAAAGcE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAABAEyZMkM1mK/YBAADM5Wd2AQAAAFVFWlqatm3bpmPHjiktLU3p6eny9/dXSEiI6tWrpxYtWui8885TkyZNzC7VLYcPH9b+/ft19OhRpaWlyW63F72WevXqqU2bNmrVqhUBDgAAAACYhAAfAIAaLDo6Wueff77bzw8KClJoaKhCQ0PVqlUrXX755bryyivVt29f+fh4dmOfu6Gwr6+vAgMDFRgYqPDwcEVGRqp58+Zq3bq12rdvr3/84x+Kiory6NieOH78uD755BPNnTtX+/btk2EY59wmPDxcl19+ubp27aoBAwaoR48e8vMz/9eu/Px8/fDDD/r222+1fPlyJSYmnnOb0NBQde7cWf/85z81cuRINWjQoBIqBVCT9enTR6tWrTrn82w2W9HPh9DQUEVGRqpx48Zq1aqV2rVrpyuuuEJt2rSphIoBAAAqjs1w569QAABQLXka4JfkvPPO05gxY/TII4+4HVR7c1Z3mzZtdOONN+r2229Xq1atvLLP7OxsjR8/XjNnzpTD4SjXvurWratFixapV69eXqnNU/n5+Zo9e7Zef/11xcTElHk/fn5+GjJkiCZMmKAOHTp4r0AApho1apQ+/fTTon+3aNFC0dHRptXjboDvjsjISA0bNky33nqrad+Dq5IJEyZo4sSJxcaIDAAAMBc98AEAQLlFR0frySefVPfu3fX3339X+vH37t2rV155RW3atNE///lP/fXXX+Xa39GjR9WhQwe98cYb5Q7vJSk9PV0pKSnl3k9ZbN26Ve3bt9fjjz9ervBekgoKCvTDDz+oU6dOuv3225WcnOylKgGgYiQkJOj9999X79691alTJy1atMjskgAAADxi/r3cAADAUkJCQtSyZUuXj2VnZyspKUknT550+fjWrVvVv39/rVu3Tk2bNvXouP7+/mrXrp3Lx7KyspSWlqa0tDTl5eWVuA/DMLRw4UL99NNPevLJJzVp0iSPW9fExMSoT58+Onz4sNNjPj4+uuKKK9SlSxe1atVKoaGh8vf3V0pKipKSkrRz505t2bJFhw4d8uiYFeV///ufRo8eXeJ7Vrt2bfXu3VsdOnRQgwYN1KBBA/n6+io9PV2HDx/Wjh07tHbtWqWnpxfbzjAMff7557r77rvVp0+fSnglAGq6tm3bKiAgwGk8Ly9PJ0+eVFpamnJyckrdx/bt23Xddddp6NCh+uSTT1S/fv2KKhcAAMBrCPABAEAxnTt31sqVK0t9zsGDB/XVV19p5syZSkpKKvbY0aNHddNNN2nDhg0eHbdx48b6448/zvm8kydPavPmzdq0aZNWrlyp5cuXO93eX1BQoGnTpun333/XwoULVbt2bbfrePDBB53Ce5vNpvvuu08vvPCCWwvUHjlyRN99953mz5+vdevWuX1sb/rggw90//33u2x90L17d7300ku66qqrXAZiZ8rPz9eSJUv07rvv6ueff6aVAgBT/PTTTzrvvPNKfY7dbtcff/yhTZs2af369frhhx9kt9udnrd48WJ16tRJy5YtU+vWrSuo4qppwoQJmjBhgtllAACAM9BCBwAAeOzCCy/U888/rz///FNdu3Z1enzjxo2aN29ehRy7Xr16GjBggF544QX9+uuv2rNnj8aMGSN/f3+n565YsUJDhgwpddb+mVauXKkffvih2JiPj4++/PJLvfvuu26F99Kp/tGPPfaY1q5dq507d+ree+9VSEiIW9t6wy+//KIHH3zQKWyvU6eOvvnmG23YsEHXXHPNOcN76dSdEUOHDtWPP/6oLVu2qGfPnhVVNgCUS1BQkLp3766HH35YX331lY4fP65p06a5XHz72LFjJd5tBQAAYCUE+AAAoMwaNWqkxYsXq1GjRk6Pvf/++5VSQ+vWrTVr1iytXr1aLVq0cHp89erVevLJJ93a1xdffOE0NmbMGI0YMaLM9bVv317vv/++rr766jLvwxMJCQm64447nHr3N2rUSKtWrdJNN91U5n136tRJq1ev1muvvebyggkAWElERISeeeYZ7dixQ3379nV6PC4uTjfddJPbF3kBAADMQIAPAADKpUGDBnr66aedxteuXavs7OxKq6N79+7avn27WrVq5fTY7NmztXHjxnPu4+eff3YaGzt2rFfqqyxPPvmkEhMTi40FBATol19+UceOHcu9f5vNpieffFK//PKLQkNDy70/AKhoUVFR+vXXX11ejN26datmzJhhQlUAAADuoQc+AAAot+HDh+vxxx8vNpabm6tdu3apS5culVZHvXr1tGDBAnXr1k2ZmZlF44Zh6Omnn9bq1atL3LagoEAxMTHFxurWrVvigr5W9Ndff7m8i2DSpEm67LLLvHqsq666qszb5uXladOmTTp+/LgSEhKUlZWliIgIRUZG6uKLL9ZFF13kxUpL9vfff2vbtm06fvy47Ha76tatq7Zt2+qKK65wu+WRYRjasWOHduzYoYSEBDkcDjVs2FDt2rVT165dZbPZKqz+goICbdmyRbt27VJSUpJ8fHwUFRWl888/X927d5evr6/Xj+lwOLRt2zYdPnxYiYmJSktLU3h4uBo0aKBWrVqpffv2Xj+mK4mJidq4caMOHTqkzMxMhYaGKjIyUt26dXN5J443FBYWavv27YqOjlZiYqJSUlJUt25dNWjQQC1btlTHjh3l41Ox86Py8/O1adMm7d69W0lJSfL39y9677t27Vohn/PqwsfHRx9//LF27dqlv/76q9hjkydP1v3331/ui5JJSUnasmWLEhISlJiYKIfDofr166thw4bq3r27IiIiyrX/MzkcDv3999/6888/lZiYqPT0dDkcDtWqVUt169ZVs2bNdP755+vCCy+s8POyNDk5OVq3bp2OHz+uuLg4+fr6qlGjRmrfvr0uu+yyCv0e6crff/+trVu36sSJE8rNzVVERIQaN26snj17ql69epVaCwAAbjMAAECNdfjwYUNSsY/evXuXaV8hISFO+/rxxx9LfP7Zz23RokXZXoQLb7zxhtP+JRlbtmwpcZuYmBin50dFRXmtpsowevRop9dw0UUXGQ6Hw+zSDMMwjGXLlhnXXXedUbt2bZefn9MfF1xwgfH4448bsbGxZTpOixYtiu3vzjvvLHqsoKDAeO+994w2bdqUePzatWsbY8eONZKTk0s8Rnp6ujFx4kSjSZMmJe6nYcOGxuuvv27k5+d7VL+rr8tPPvmk6PG4uDjj0UcfNcLCwko8dmRkpDFmzBgjISHB07fPpc2bNxsjR440wsPDS/3cNW7c2LjnnnuMAwcOlOk4vXv3LvX70YoVK4yrr77a8PHxKbGGdu3aGZ9//rlRWFjohVduGKtXrzZGjBhxztceHh5u/Otf/zJ27drl8TE++eQTp/0dPny46PHY2FjjkUceMUJDQ0s8flhYmPHoo48aSUlJbh3z7K+TsnysWLHC49fqjrPPg7Pfj/LYunWry9fyxhtvlGl/aWlpxuTJk43OnTsbNputxPfKZrMZnTp1Mt5++20jNze3zPVv2bLFuPvuu0s9F878qFu3rtG/f39jxowZxrFjx865/5deeslpH2Xx119/GTfddJPL3w3O/H7x8ssvG5mZmeU+/tnbvPTSS0WPORwO47///a9xySWXlFiLr6+v0a9fP2PDhg1ler0AAFQkAnwAAGowbwb4jRs3dtrXF198UeLzz36uNwP89PR0l+HGY489VuI2KSkpTs/38/MzsrKyvFZXRcrJyXEZjJc1lPKmgwcPGgMGDPA4HAwJCTFefvlljy9AlBTgnzhxwujWrZvbx2/atKmxY8cOp/2vW7fOaN68udv76datm3Hy5Em36y8twP/xxx/PGSSf+REREWF8+eWXHr1/Z0pMTDRGjBhRajDp6sPf3994+OGHDbvd7tHxSgrw7Xa7ywtUpX0MGDCgWDDoqX379hmDBg3y+Lz18fEx7r77biMnJ8ftY5UW4H/77belXqxx9Tl3J4SsqQG+YRhG3759nfbfsWNHj/ZRWFhozJw504iIiPD4fWvRooXx22+/eXQ8u91u3HvvvaVevHLne9G5lDfAdzgcxnPPPWf4+/t79H6cvsDu7QD/2LFjxhVXXOHR+/Tcc8959JoBAKhotNApg61bt5pdAizm8ssvN7sEADBdWlqa01hYWFjlFyKpTp06uuuuuzRz5sxi44sXL9abb77pcpuwsDAFBgYqNze3aKygoEALFizQbbfdVpHlesXy5cuLtQ2STvW+HzVqlDkF/Z+tW7dq8ODBSkhI8HjbrKwsjR8/Xjt27NDnn3+uoKCgMtcRExOjf/zjH4qOjnZ7m+PHj2vAgAHavHmzmjVrJklaunSprr/+euXk5Li9n99//13XXHON1q5dKz+/sv/6vWjRIg0fPlwFBQVub5OcnKxbb71V6enpuvfeez063qFDh3TNNdfo77//9rRU5efn6z//+Y+2b9+uH374oVytKex2uwYPHqwVK1Z4tN3SpUs1ePBgLV++3OPWMr/99ptuvPFGpaamerSddKrVzscff6w///xTCxcuVMOGDT3ex2nvvfeeHnzwQRmG4fY2ycnJ6t+/v9auXasOHTqU+djV2SOPPOJ0Pm3fvl0nTpxQkyZNzrm93W7XnXfeqW+++aZMxz9y5IgGDhyod955R/fcc885n5+Xl6chQ4bot99+K9PxKkthYaFGjRql//3vfx5td+TIEfXu3VsrV670aj2HDh1Sr169dOLECY+2mzJlimw2m1555RWv1gMAQFkR4AMAgHI7cuSIsrKynMYbNGhgQjWn9OvXzynA//vvv5WYmOiyLpvNpiuuuMIpQHjyySfVtWvXSuvLXlauws0OHTooPDzchGpO2bNnj/r06eN0YUGSLrroIg0bNkwtW7ZU3bp1FRcXp02bNmnhwoVOz58/f75yc3O1cOHCMvVLLigo0PXXX18U3ttsNl155ZW6+uqr1axZMwUGBur48eP65ZdfnAKy+Ph43X///frxxx+1Z88e3XjjjUXhfXBwsAYMGKBevXqpUaNGcjgcOnjwoL799lvt2rWr2H5+//13zZgxQ0899ZTH9UvS4cOHNXbs2KLw3mazqUePHho8eLCaNm0qm82mY8eO6eeff9a6deuKBb6GYej+++9XRESEbrjhBreOl5CQoJ49eyo2NtbpsaZNm2r48OFq27atwsPDlZCQoB07dmjBggVKTk4u9tw1a9aof//+Wr9+vQIDA8v02v/9738XO79bt26tQYMGqU2bNgoPD1daWpq2b9+u+fPnKz4+vti2q1ev1owZM/Tkk0+6fbxFixbphhtuUH5+frHxgIAAXXXVVerWrZuaNWum0NBQZWZmKjo6WsuXL9eaNWuKPX/Tpk26/vrrtXr1avn7+3v8un/++WeNGTOm6HMZGhqqAQMGqEePHoqMjFRhYaGio6O1ePFi/f7778W2zcrK0l133aXNmzeXeNGoXbt2RRdZjx49qpMnTxY95u/vr3bt2p2zxtq1a3v8uqygb9++8vHxUWFhYbHxDRs26MYbbyx12/z8fA0YMMDp8y1JF154ofr27atLL71U4eHh8vPzU1JSkjZv3qyffvqp2ALjBQUFuu+++9SoUSNde+21pR5z6tSpLsP7Zs2aacCAAWrXrp0aNmyooKAgZWdnKz09XQcOHNBff/2lDRs2uPz5XBEef/xxl+F9SEiIBg8erB49eqhRo0bKycnRkSNH9OOPP2rLli2STp2z119/vW666Sav1JKRkaFBgwYVhfenv2f2799fzZs3V+3atZWYmKh169ZpwYIFstvtxbafOnWqrr32WnXr1s0r9QAAUB42w5PpHJDEDHw4YwY+gKoqOjpa559/frGxssyCmzFjhtMitgEBAUpJSSlxMdCzg9gWLVp4NDv6XJKSklwG9UuWLNGAAQNcbvP2229rzJgxTuMhISF64okndN9996lx48Zeq9Gb+vXrp+XLlxcbGzNmjGbNmmVKPbm5uerWrZt27NhRbDw8PFxvvfWW/vWvf7nc7uTJk3r88cc1Z84cp8dmzpypRx555JzHPu+883TkyJGif595Z0XHjh314Ycflvize8mSJbrhhhucAq/Vq1frkUce0fbt2yVJI0eO1JtvvqmoqCinfRQWFmrq1Kl64YUXio2HhoYqNjZWwcHBpdbv6usyKCioKGBq06aN5syZU2KwtGnTJo0aNUp79uwpNt6gQQPt3r1b9evXL/X4hmFoyJAh+vnnn4uNBwcHa/LkyXr44YddzmjPycnRSy+9pDfeeMMpGH300Uc1Y8aMUo8rSX369NGqVauK/n3m627UqJFmzZpVYsCamZmphx56SJ999lmx8bCwMMXExJzzfZdOXSjp1KlTsZn3fn5+euyxx/TUU0+VelHyjz/+0OjRo53+Vnj88cf1xhtvlHrcOXPm6K677io2dvq122w2Pf7443rhhRdKvKtp3rx5uuOOO5xCyC+//FIjR44s9diSNGrUKH366adF//b292NPnX0eSKc+N+edd57XjtG+fXunxWzHjRunKVOmlLrdI488ov/85z/Fxtq1a6cZM2bo6quvLvEiY05Ojt566y2NHz++2MWhsLAw7dixQ82bN3e5nd1uV4MGDYpd2KxVq5Zmz56tO++885wL1Obm5mr16tX65JNPdOLECaf39WwTJkzQxIkTi425ExmsWrVKffv2dXrubbfdppkzZ5b4fWflypUaPXq0Dh48KOnU95mz73By5/hnv+9nfu/o1q2b3nnnHXXq1MnlttHR0brhhhu0bdu2YuMDBw7UL7/8cs5jAwBQ0cxbjh4AAFQLSUlJevXVV53Ge/bsWWJ4Xxnq16/vFIJKp26pL8no0aOLWqWcKSsrSy+//LKaNm2qbt266dlnn9X333+v48ePe7Xm8ti3b5/TWOfOnU2o5JQ33njDZXi/YsWKEsN7SapXr54++eQTPffcc06PPfPMMx63QpBUFN736tVLq1evLvXC+8CBA/Xhhx86jd94441F4f1TTz2lL7/80mV4L0k+Pj56/vnnNXr06GLjaWlpWrBggcf1SyoKoi6++GKtXbu21FmhXbt21Zo1a3TxxRcXG09MTNQzzzxzzmN9+eWXTuF9UFCQFi1apMcee6zEdjTBwcF69dVX9e677zo99tZbbxXNtPXE6dd9wQUXaOPGjaXOjq5du7bmzJmjgQMHFhtPTU3V/Pnz3TrebbfdViy8r1WrlpYsWaJXX331nHcUdejQQevXr9fVV19dbHzWrFk6duyYW8c/0+nw/tNPP9Xrr79eakuym266SR9//LHT+EcffeTxcWuKLl26OI2V9vNBOnWB7+zw/p///Ke2b9+uAQMGlHqHUHBwsJ599ln9+OOPxe7ISE1N1dSpU0vc7rfffnO6K+mdd97RXXfddc7wXjp1AfPqq6/W3Llznb6uvcUwDD3wwANOQfvjjz+uzz//vNSLhn369NHatWvVqlUrSfKoPVlpTn/vGDp0qFauXFlieC+duui7bNkyp3ZXy5Yt09GjR71SDwAA5UGADwAAyiwhIUHXXXed4uLinB5zp69vRYuMjHQaKy10DwwM1Lx580qcqWsYhjZt2qTp06dr2LBhatasmRo1aqQhQ4Zo8uTJWrNmTbEe+pXF4XC4/Bw0atSo0muRTrWYmD17ttP4nDlzdOmll7q1j8mTJ+uaa64pNpabm6u33367TDVFREToq6++cqvlxy233OIUfp/u4d+7d29NmzbNrWNOnDjRKWArT4AWEBCg7777ThEREed8bkREhL777jsFBAQUG//iiy+UlJRU6rau1ol49dVX1a9fP7fqvPfee3X//fcXGzMMo8T1J87F399f33zzjVq0aHHO59psNpfHWbJkyTm3XbZsmTZs2FBs7L///a+uuuoqt2sNCAjQvHnzigWW+fn5ZX7tjz32mG6//Xa3nnvrrbeqa9euxcZWrVrlNCsfp3j680GSXn755WL/vvTSSzVv3jynr7PSXH311XrppZeKjX3yySdO7Z9OO/uiQnBwcJnXZalVq1aZtjuX5cuXO93x0717d73++utubd+oUSN9/fXXHq9VcS7nnXee2+unhIeHO31eCgsLtWzZMq/WBABAWRDgAwAAjx06dEjTpk1T+/btnQIv6dTMxhEjRphQWXGuZqymp6eXuk23bt20ZMkSt1vlxMfH66efftILL7ygXr16KTIyUnff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680336ywY7pydhuKzMxM/fLLL2Xa17x58865/5pgz549ZWq/tH//fu3atavYWJcuXeTj4/rPAFfv7bfffuvxcaWq9bm7+uqrnca+/vprFRQUmFBN5Tv7wmJZL7hZXWZmpkaNGuU0HhYW5nL8TP379y/277y8PH3zzTderM77WrdurbvvvrvYWHZ2tg4ePOi1Y4SEhKh9+/bFxhYvXlymixs16WsOAABPEOADAIBqIzk5WcOHD1d2dnaxcR8fH7366quVWkt2drZ+//13p/ELL7ywwo7Zvn173XbbbU7jL7zwQrlaGriyfPlybd++3eVjAwcOdBp77733PD7G4cOHnYL/+vXrV8pCxFb00UcfebzNhx9+6DQ2aNCgEp/v6nP3wQcfeHwXR1pamubOnVtszN/fX3379vVoP5WlSZMmTncHHD58uNJbUJnl7Fnaru5gquocDofuuusul22hxo8ff84Flv/5z386jU2ePFl5eXleq7EitGnTxmmsPGtquDJixIhi/87Oztbbb7/t0T7y8/NdtjYCAAAE+AAAoJrYsGGDOnXqpAMHDjg99thjj7nVuuP999+X3W73Sj1vvfWWsrKyio1FRka6bNXhTW+88YYaNGhQbCw3N1fXXHONV3oLG4ah1157Tddcc02JIdCwYcPUuHHjYmOLFy/WTz/95NGxxo4d6zQb87777pO/v79nRVcTs2bNcnl+l+TAgQOaNWtWsbHAwMBSZxp37drVae2GnTt3enwB5vnnn1dKSkqxsZtvvtll732reP75553GnnzySa8v+mlFZy9EmpqaWukLYFekmJgY9evXz+XdJN27d9eYMWPOuY8hQ4aoQ4cOxcYOHz6sRx55xFtlVojY2FinsbN/RpTX3XffrYCAgGJjkyZN8uiuocmTJ2v37t1erQsAgOqCAB8AAFRp+/bt05gxY9SrVy+nBTOlU4u7Tps2za19PfLII7rgggv01ltvlWsG6vz58zVhwgSn8ZtvvrnE1iXeEhkZqU8//dSpJ3FMTIx69eql7777rsz73rp1q3r27Kmnn35a+fn5JT7P39/fZSB25513uh3QjB8/Xj/++GOxsaCgID344IOeFV2N5Obmavjw4W4FqydPntTw4cOdel3feuut51xI9vHHH3cae/LJJ7Vq1Sq36vzvf/+rd955p9iYzWbTY4895tb2Zhk2bJjTxYu0tDQNGjTIqQ2RuzIyMvTaa6/p888/90aJFebsFiiSPL7gZkXJycmaPn26OnTo4PL8bdq0qb755hu3LgrabDZNmjTJafy9997TuHHjyrzWyIYNG3TrrbeW+PiMGTO0bNmyMu07PT3d6S6S0NBQNW/evEz7K0mDBg10//33FxvLzs5W//793eppP2PGDJet7wAAwCkE+AAAoEpJTU3Vr7/+qsmTJ6tfv35q27at3n77bZd9cwcMGKCFCxd6tHBsbGysHn30UTVs2FC33Xabfv75Z7dn5R8+fFijR4/WzTff7NRWISIiwmWoXxEGDRqkd955x6kfflpamm644Qb94x//0JIlS0oN4U/Lz8/Xjz/+qKFDh6pLly5av369WzU88cQTTrNVk5KS1KdPH3311VclbpeamqrRo0e7DMqmT5/uNLO/pggKCpJ0ao2Hnj17atOmTSU+d/Pmzbryyiud2iY1aNBA06dPP+exbrnlFg0ePLjYWE5OjoYMGaLZs2eXGFTa7XaNGzdO99xzjwzDKPbYo48+atkFbM/05ZdfKjw8vNjYoUOH1K1bN02ePNmt1iOFhYVasWKF7r//fjVv3lxPP/204uLiKqpkr+jevbvTxcUnnnhCP/zwg1vfJ6zCbrfr999/16xZszRy5Eg1bdpUzz77rBITE52ee/7552vFihVq1qyZ2/sfOnSoxo4d6zQ+bdo09e3bV6tXr3ZrP7GxsZo1a5Z69OihHj16aOHChSU+d9WqVRowYIAuueQSTZkyxe1FXnft2qX+/fvryJEjxcZHjBjhNFveG6ZMmaILLrig2Njx48fVoUMHjRs3zqnuvLw8/fzzz+rXr1+xi4bdu3f3em0AAFR17v81CwAAUIFiYmKcAt/TcnJylJqaqrS0NKcZxa74+/vr2Wef1UsvveQ0E91d2dnZmjt3rubOnSt/f39ddtll6tatm5o3b66IiAiFhYXJbrfr5MmT2rt3rzZt2qTNmzeXWM/HH3+siIiIMtVSFvfee6+CgoJ0zz33OF1MWL9+va655hrVrl1bffv2VYcOHVS/fn01aNBAfn5+Sk9P1+HDh/XHH39o7dq1ZeqXHBAQoLlz56pr167F7mZITEzULbfcookTJ+r6669Xy5YtVadOHcXHx+v333/XwoULXS5+OGTIEJfBWU3x9NNP680331RmZqZ2796t7t27q2fPnho0aFBRAHns2DH98ssvWrNmjVOAbrPZ9O6777rdOuOTTz5Rhw4dirXfyMrK0tixY/Xaa69p+PDhatu2rcLCwpSUlKQ//vhDCxYsUFJSktO+OnXqpKlTp5bj1Veeli1b6ptvvtHgwYOLfd1kZWXphRde0NSpU9WzZ0/94x//UFRUlMLCwpSdna3U1FQdO3ZM27Zt07Zt25SammreiyiDqKgoXXPNNcVm3cfHx+v6669XQECAmjVrppCQEKeLgh999JHTXQsVZfDgwS6D57y8PKWlpSktLc2pbVlJhg0bpo8//tipdZA7ZsyYof3792vJkiXFxlevXq3evXurdevW6tOnjy6++GKFh4fL399fqampSklJ0a5du7R161bt27fP4xn7u3bt0vPPP6/nn39e5513njp27KjLLrtMDRs2VFhYWNH37gMHDmjNmjVat26d0/eBiIiICpvpHhISoq+//lr9+/cv9jMjNzdX06ZN07Rp0xQWFqZGjRopJydHcXFxTj/PH3zwQTVo0EAbN24sGqvou9YAAKgKCPABAIAl5Ofna8eOHeXah4+Pj6677jpNmTJFbdu29VJlp2rbsmWLtmzZ4vG2tWrV0pdffqnrrrvOa/W464477lC7du30r3/9y2Uv4szMTC1atEiLFi3yaL++vr66++67S7zgclrbtm21YsUKDRkyRAkJCcUe27t3r9utjYYPH64vvvjCKTysSc4//3x98cUXGj58uBwOhwzD0Jo1a7RmzZpzbmuz2fTee+/phhtucPt4kZGRWrt2ra655hr9/fffxR47evSoZs6c6dZ+evbsqYULFyowMNDtY5utX79+WrNmjW688UYdO3as2GNZWVlasmSJU3hbHbz22mtatWqVUwiel5engwcPutymMhe7dbX4rKcuv/xyTZo0qdSFnM/F19dXixYt0uOPP67Zs2c7Pb5v3z6Per+XRXR0tKKjo7VgwQK3twkNDdV3332nRo0aVVhdnTt31rJlyzRw4ECXrb5SU1NLvLg1cuRIvfXWW053qtWtW7cCKgUAoGrhcjYAAKjy2rVrpxdffFH79u3TggULyhzeT58+XVdeeaXXZvwNHz5ce/bsMSW8P61z587auXOnXn/9dUVFRZVrX4GBgRo5cqT++usvvf/++woLC3Pr+Bs2bFD//v09Pl5ISIgmTpyoefPmFbWQqcmuu+46ff/9926976eFh4friy++0L333uvx8S644AKtW7dON998s8cXT/z9/TV27FgtW7asTLOczda1a1dt27ZNd911V7kWTbbZbOrTp4+uvPJKL1ZXMdq1a6dly5apZcuWZpfiVY0aNdL999+v1atXa8uWLeUK70/z9/fXrFmzNG/ePLVq1apc+4qMjCx1bQ9vBO49e/bU+vXr1atXr3Lv61y6dOmiXbt2aeTIkW49v27dupoxY4bmzp0rPz8/p+A/NDS0IsoEAKBKYQY+AACwNB8fHwUEBCgoKEjh4eGKjIxU8+bN1bp1a1166aXq2bOn12YUPvLII3rkkUeUmJioZcuWac2aNVq7dq12797tVrsDX19ftWnTRsOHD9dtt92m1q1be6Wu8goICNATTzyhsWPH6ocfftC3336r5cuXu2x3crZ69eqpS5cuGjZsmEaMGFGmMPaCCy7QsmXLtGzZMs2aNUvLly8vtdXFBRdcoH/+8596+umnK3S2aFU0dOhQ7d69W5MnT9b//vc/paenu3xegwYNNGLECI0fP97ttjkl7efrr7/WU089pddff11Lly4tdRHdqKgoDR06VM8884wuvPDCMh/XCurXr6///ve/mjBhgmbNmqWff/5Zu3fvdmpLcrY6deqoV69e6t+/v4YNG6YWLVpUUsXld8UVV2jv3r1aunSpFi9erJ07d+rQoUNKT09XdnZ2mRdqrSg2m03+/v4KDAxUaGioIiMj1aRJE7Vq1UoXX3yxevToUaHfh2+88UYNHz5c3377rf73v/9pzZo1brUca9u2rfr3769rrrlGAwYMKHWdltOL5P74449asWKFNmzYoBMnTpzzGMHBwRoyZIjuuOMOXXvttR69rvKKiorSl19+qRdeeEHz5s3T0qVLdfz4ccXHx8vX11cNGzZU+/btNWjQIN16663FQvqz14s4e00KAABqIptxrt9A4WTr1q1mlwCLqQqLsgEAyi43N1cHDx7UgQMHFB8fr4yMDGVnZysoKEh169ZV3bp11bJlS1188cUKDg42u1y3HTx4UPv379fRo0eVnp4uu92ukJAQ1atXT+Hh4WrXrp1atmzp9dY1eXl5+v3333Xs2DElJiYqKytLERERatCggS655JJyz2it6qKjo3X++ecXG/vkk080atSoYmP5+fnavHmzdu3apeTkZPn4+Cgq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LwEAsBrDMBQfH292GbA4wzCUkJBQ7O5OAAC8gR74AHAOBzYv1/6NSzVt6pQSe8CfraoFpZ07d9bgwYNlGIZeffXVohD9zMD+wIEDHu/39CKvMTExbm8TGBio2rVrl/mY7jgd0B89elR//vmnJGnZsmUqKChQvXr1dMUVVzhtc/q9qKiaKkNVOy8BALCCtLQ0t+6+AxITE4tNgAEAwBsI8AGgFIZhaOnb49Slazddf/31bm/n6+tbcUVVkHvuuUe+vr46fPiwFi9eLEm68MILFRISIklaunSpx/u89NJLJUm///67R3/4XnbZZZKkX3/9tUL+CGrWrFlRbafb5pz+78CBA11eqDld019//VVif3urc/cCFAAA+P8SEhLMLgFVhMPhKHG9KQAAyooAHwBKsWvFAh39a5NenT7No9nLVTEobdq0adHCrR9//LEKCgrk5+en6667TpK0ePHiooVcS3J6wdvTrr32Wvn6+iotLU3vv/++27UMGzZM0qkZ8p999lmpz83JyVF+fr7b+z5t8ODBkk5dmDh06FDRTHxX7XNOPz8wMFAOh0PTp08vtQ9uYWGhMjIyPK6polXFC0sAAJgpOzvbkj/TYV3x8fEyDMPsMgAA1QgBPgCUwFFQoGXvPKcBAwaqT58+Hm1bVYPSUaNGyWazKSYmRt9//70kafTo0WratKkcDofGjh2rzz//vNiCtpmZmVq/fr1eeukl3XPPPcX216xZM91+++2SpM8++0yTJk3S0aNHix4/efKkli5dqieffLLYdn369FHfvn0lSbNnz9bUqVN15MiRosfz8/P1119/6T//+Y+GDh2qlJQUj1/rgAED5O/vr7S0NE2YMEGSdP7556tt27Yun1+/fn2NHTtWkrR27Vo99NBD+uOPP4qCfMMwFB0drc8//1wjRozQmjVrPK6polXV8xIAALPQ+x6eys3NdZrUAgBAeVS9KaIAUEm2Lp6j+Oh9mvbdlx5vWxVn4EtSy5Yt1atXL61atUqffPKJrrvuOoWGhurtt9/WU089pf3792vmzJmaOXOm6tSpo8LCQmVlZRVt36xZM6d9PvDAA8rKytK8efP0ww8/6IcfflCtWrVUWFgou90uSUU97880adIkvfzyy1q6dKnmz5+v+fPnKzg4WP7+/srMzCzWWqcsvd3r1q2rnj17asWKFdq9e7ekkmffnzZy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bDZFRkaaXQYAAFWWlYLSo0ePas2aNZKkBx54QKNGjSr2uJ+fny666CJddNFFuvPOO2W3202oEpXBSuclAMB6CPABwIIaNmxIgA8nERER8vf3N7sMAADgBfv37y/6/969e5/z+UFBQS7Hjx8/rrlz52rTpk2Kj49XYWGhoqKidMUVV+i2225To0aNnLZZtGiRJk6cqKioKC1atEh79uzRp59+qu3btys9PV2RkZHq3bu3Ro8erbp165ZY059//qk5c+bojz/+kN1uV8OGDdWvXz/dddddbrwDUmZmpr7++mutXr1aR48eld1uV3h4uC677DLdcsstat++vdM2MTExuu666yRJCxcuVGFhoT799FP9/vvvSkxMVP369bVo0SK3jg8AQFVAgA8AFhQaGsosfBRjs9kUFRVldhkAAKACxMfH6/zzz/d4uwULFmj69OkqKCiQJAUEBMhmsyk6OlrR0dFauHChpk+fru7du5e4j19++UUTJkxQQUGBateuLYfDoRMnTmju3LnauHGj5syZo1q1ajlt98MPP2jy5MkqLCyUJNWuXVuxsbH65JNPtGLFCg0bNqzU2v/66y898cQTSk5OlnSq939QUJDi4+O1dOlSLVu2TA8++GCpFwN27typKVOmKDs7W0FBQfLzI+IAAFQ/NNEFAItq0qSJ2SXAQho2bKiAgACzywAAAF7Srl072Ww2SdLMmTN15MgRj7ZfuXKlJk+eLEkaNWqUFi1apHXr1mnt2rX69ttv1b9/f2VlZemZZ55RXFycy32cPHlSL7/8soYOHarFixdr5cqVWr16tZ5++mn5+fnp0KFD+uyzz5y227t3r6ZMmaLCwkJdfvnl+vbbb7Vy5UqtWbNGkydPVnJysj766KMSa4+JidHYsWOVnJysfv366fPPP9e6deu0atUqLV26VKNHj5aPj4/efvttrVy5ssT9TJkyRRdccIE+++wzrV27VmvWrNHs2bM9eh8BALA6AnwAsKiQkBDVq1fP7DJgAX5+fi5vfwcAAJ45HZhbQePGjXX99ddLkg4cOKAbb7xRt912m6ZPn64ffvhBBw4cKLE3en5+vl599VVJ0rhx4zRmzBhFRUXJZrPJZrPpvPPO07Rp09SrVy9lZWXpiy++cLkfu92uAQMG6IUXXij6XSMoKEg333yzRowYIUkuF9B955135HA41Lx5c731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+uiCCy6ocrv169frgw8+0H//+1/l5uYqJCREycnJ6tOnj6699toq5+yvak7p7OxsXXHFFXI6nXrqqafUs2fPk2Z99tln9c9//lPNmjWrcvHibdu26c0339S3336rgwcPys/PT82aNdOf/vQn3XzzzYqKijphm4qpSc477zw9//zzWrdund555x1t375dubm5uuuuu3TPPfec8u/wTHnjflkf2Ww2XT7scb1476Vavny5rr/++pM+ti5/pna7XcnJyUpOTtbAgQM1depUffzxx1q1apWSkpJ0xx131FkWADjWRTcO01dvzNVDDz+sN15//ZSPtdJ74fbt2yt/f6rzkgrBwcFVfn3fvn1asmSJNmzYoMzMTJWXlyshIUEXXnihBg0apPj4+BO2+eCDDzRlyhQlJCTogw8+0M8//6xFixbpu+++0+HDhxUbG6uePXvqrrvuOuXUij/++KNefvllff/99youLlZcXJwuvfTSar8nFBQU6M0339Tnn3+u1NRUFRcXKyYmRuecc45uuukmderU6YRt9u/fr6uvvlqS9P7776u8vFyLFi3SN998owMHDqhRo0b64IMPqvX6VmGl/RIAYD0U+ABQA5fePUnff/iKnnjiiWovluYtjv3gUF5eftz3UlNTNXLkSKWmpko6WjCGhYXp0KFD+uyzz/TZZ59p4MCBevDBB094XqfTqZSUFL3xxhuVXwsJCZHT6dRvv/2m3377TZ9++qnWr19/3HZFRUWaOHFi5cg0SQoLC1NBQYG+++47fffdd/rwww81Z86cas3ZHxMTowsvvFD//ve/9eGHH570g7LL5dKqVUfnHr/yyitP+P5zzz2nF198sfLvKzg4WGVlZdqxY4d27Nih999/X3PmzDnhAsixUlJStHjxYtlsNkVERFSOtAMqJHf7c+Xi2VdffbXlFpcODg7WlClTtGfPHv3yyy96+eWXdf3111de2DzdIra7d+/W4sWLtXnzZmVmZkqSoqKi1LhxY3Xr1k39+vVTy5YtKx//++f76aef9PLLL+uHH35Qfn6+YmNjdckll+jOO++scoqb8vJybdmyRV988YU2b96srKwsZWdnKywsTElJSbr88survCA4ZMiQ4+alriiMKlRckDtWaWmpVq5cqXXr1umXX35Rfn6+GjRooCZNmuiiiy7SlVdeqaZNm57073bt2rV66623tHPnTpWUlKhFixa66qqr9Je//OWUx4qDBw/q9ddf13/+8x/t379fpaWlaty4sbp27apBgwapdevWJ2zz+7/Xbdu26bXXXtO3336rQ4cO6Zxzzjnhz1ddlFGoS7VZBNxqMjMz1apVqxpvt3z5cs2cOVNlZWWSpMDAQNlsNu3evVu7d+/W+++/r5kzZ550oIQkrVq1SpMnT1ZZWZnCw8PldDqVlpamJUuW6Ouvv9bLL7+s0NDQE7Z77733NH369MrzxvDwcKWnp+ull17Sp59+quuuu+6U2bdu3ar7779fhw4dknT0QnFwcLAyMzP18ccfa82aNfr73/9+yosBW7Zs0WOPPaaioiIFBwdb7v0SAAB34N0NAGrIV0uJr7/+WtLRcr5JkyaVX8/Pz9ewYcO0f/9+devWTUOGDFGHDh0UGBiogoICvffee3ruuef09ttvq0WLFrrpppuOe96nn366sry/+uqrddttt1XOjZ2dna0tW7ZUOa/rpEmT9MUXX6h58+a655579Kc//UlhYWEqKSnR119/rdmzZ2vLli2aOnWqnnzyyWr9Gfv166d///vf+uKLL5Sfn19l0ffDDz8oLS1N0okF/pIlS/TCCy8oLCxMd9xxh/r3769GjRrJ6XRq+/btmjdvnjZu3Kj7779fS5curfLD7rZt2/Ttt99q8ODBuvXWWxUdHS2Hw1H54RU41u/nlreSgIAA3XHHHXrwwQdVWFio9evX65prrjntdl9//bVGjx4th8Mh6egUESEhIcrMzFRmZqa2bt0qf3//k96Rsn79eo0fP16lpaUKCwuTy+XSvn379Oqrr2rt2rV67rnnjjuGSUfX+rjrrrsq/7+iJMrLy9O3336rb7/9VqtXr9b8+fOPG+EaGRmpqKgo5ebmSjp6kcFutx/3/WOlpaVp9OjR2rVrl6SjP7/w8HDl5ubq0KFD+vHHH3X48GHdf//9Vf7ZZs6cqaVLl8rPz6/yeLd9+3Y99dRT2rZtm6ZMmVLldl988YUmTpxYOf2Zv7+/AgIClJaWprS0NH344YeaOHGi+vfvX+X2krRu3TpNnDhRZWVlCgsLowSD17LycfP3zj77bNlsNrlcLs2ZM0czZ86s0foh69ev1/Tp0+Xv76/bb79dAwYMqBxtv2fPHi1cuFBr167VuHHj9Oabb1Y5Ej8nJ0dTp05V//79dddddyk+Pl7FxcV6//33NXv2bP3666965ZVXKi/2Vdi2bZsee+wxlZeXq0uXLho/frxatmypsrIyrVu3To8//rhefPHFk2bfv3+/hg8frvz8/MoR+8nJyfL391d2drbeeustvfTSS3r66afVqlUr9erVq8rneeyxx9S6dWuNHTtWZ599duWfHQAAX8KZOQDUwLoXpiosNERjx441HcVtKhax3bhxoyTpT3/603FTwPzrX/+qLO/nz59/XKkTHh6uQYMGqUmTJhozZoz++c9/6oYbbqh8zJ49e/Taa69JkgYPHqz77rvvuNeOiYlRr169TvhQ9u9//1vr169Xw4YN9dxzzyk2Nrbye0FBQerZs6fatWungQMHav369frll1/Utm3b0/5Ze/ToofDwcBUUFGjNmjVVTk2ycuVKSdK5556rZs2aVX49NzdXzzzzjGw2m2bNmlW5ZoB0tAxs37695s+frzvuuEM///yz3n33Xd18880nPH9RUZEGDRp03N9FYGCgEhISTpsf9cfOjZ9o+9cfa9myZZYuUi+66CLZ7XY5nU59++231SrwZ86cKYfDoQsuuEAjR45UcnKypKPTdO3du1effPJJlSVThcmTJ6tz58568MEH1apVK5WVlenTTz/VjBkzlJ6ergcffFAvvfTScUW73W5Xz5491bdvX5177rlq2LCh/Pz8VFRUpHXr1umZZ57Rd999p2eeeUajR4+u3G7WrFnHTdXwyiuvnHBxoEJBQYGGDx+u1NRUNWjQQMOHD9dll12m8PBwlZWVKS0tTV988cVJy8XPP/9cR44c0ahRo3TNNddUFv8LFizQu+++q5UrV6p///7q1q3bcdtt3bpVY8eOVWlpqa6//nrdfPPNat68uex2uzIyMrRo0SItXbpUjz76qFq3bl1ZcP3elClTdP7552vUqFGVdz9U3HVVG95UosL7lRYf0SfPT9Zf/vpX/eEPfzAdp9qaNGmia6+9VsuXL9fOnTs1cOBAnXXWWercubPatWunDh06KCkpqcp/T6WlpXriiSckSePHjz/h+NuyZUs9/vjjGj16tD7//HMtXry4youHxcXF6t+/vx566KHKrwUHB+vGG29UWlqaFi9erNWrV59Q4D/zzDNyOp1KTEzU3LlzKy9++vv7q0+fPpXHwZOZO3eu8vPzdeWVV2rq1KnHfS8mJkZDhw5VRESEUlJS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tm1b5Xbff//9SS8yHjs9w8lG58fHxysuLk6S9Pnnn580n7tVFHilpaWVFymsguMNPO3jZx9WfHyC/vGPf9RoOyudo6WlpWnPnj2nfVzFwApJlYtk/+lPf6q8ePjkk0+edvqv35+jnYmIiAhdcMEFkqTXXnutyuPnN998U+UCttLREfw9e/aUJL366qun/TtwZ3ar4pgJADgVCnwAOAlnWZnWPDNBl1/ep8bFlLeehN9+++2y2Wzav3+/3n33XUnSXXfdpWbNmsnpdGr48OF67bXXjptfuqCgQP/5z3/0yCOP6O677z7u+Zo3b65bb71VkvTKK6/o0UcfVWpqauX3c3Jy9PHHH+uBBx44brtevXpVLuC5YMECzZgx47gPd6Wlpdq6davmzZun/v37H7eYWnVdfvnlCggIUF5eniZPnixJatWq1XHF37EaNWqk4cOHS5L+/e9/6x//+Ie+//77yiLf5XJp9+7deu211/SXv/xFX3zxRY0zeZq37pf1QU0XYaxg6mdaXFysyZMn65dffpF09Njx+4Wfq3K6i21BQUGVvz/ZvMYnK4s2bdqkH374QZKOm3orLCys8gLsjh07TtiurKxMzzzzzEkzHTs3/akuzFVc7Hzvvfe0bdu2kz7Onc4+++zKixLPPPPMaRcbr8sSjOMNPGnfz5u1Zc2bmjplskJCQmq0rZX2zV9//VU33HCDRowYoRUrVhw35WBZWZm2bdumKVOmaPHixZKkDh06VC44HRQUpAcffFA2m03btm3TnXfeqa+++uq442xaWpqWLVumwYMHa+nSpW7NPnToUNntdu3evVsjR46snNqnrKxMa9as0fjx40/5vjBy5EhFRkaqsLBQd911l9577z0VFBRUfj83N1effPKJxowZo4kTJ7o1uxVZab8EAFiPdYYfAIDF1HQxyWNZaXRXTSQnJ6tHjx767LPP9NJLL+nqq69WZGSknn76aY0ZM0bbt2/XnDlzNGfOHEVERKi8vFyFhYWV21c1h+m9996rwsJCLV26VO+9957ee+89hYaGHrdYZHh4+AnbPfroo5o6dao+/vhjLVu2TMuWLVNISIgCAgJUUFBw3NQ6tZk3tEGDBvrjH/+oTz/9VD/99JOkk4++r/DXv/5VDodDTz/9tDZt2qS77rpLAQEBCg0NVWFh4XELTFpxLlNv3S993eGD6TVehLFCXX7gLy8v16+//qqvv/5aS5curZz7/sorr9Rtt91Wref44Ycf9OSTT+qqq67SRRddpBYtWsjPz08ul0tbtmzR448/LkmKi4s76Z0IBw8e1MiRIzVu3Di1bNlSZWVlWr9+vWbMmCHp6OjUiguA0tGpJ8455xx9//33SklJUVRUlLp06SI/Pz/t3LlTKSkp+vnnnxUSElLlNDQRERGKjY1VVlaWPvjgAyUnJ1f5b+mWW27R6tWrlZqaqr///e8aPny4LrvsMoWHh6usrExpaWn6+OOPFRwcXHlh80zZbDaNHz9eQ4YMUUZGhm6//XYNHz5cf/zjHxUcHCzp6PQamzZt0ooVK9SkSRM99NBDbnnt0+WijIInffz0BJ3Vtl21jz3HstK+6e/vr/Lycn355ZeVi7lWnFccPnz4uLt+2rVrpyeffPK4i5u9evXS1KlTNX36dG3fvl3Dhw+X3W5XeHi4jhw5IofDUfnYihHv7nL22Wdr3LhxmjFjhjZu3KiBAwcqPDxcDodDDodDLVu21HXXXaeUlJQqt2/WrJmefvppjR07Vvv379ejjz6qadOmKSIiQmVlZSoqKqp8bPfu3d2a3Yo4RwMAnArvEgBQhdosJnksK304rKk777xTn332mTIzM/XOO+/or3/9q5o2bapXXnlFq1ev1tq1a/Xzzz8rNzdXdrtdTZs21VlnnaU//elP6tGjxwnPZ7fbNW7cOPXp00fLli3Td999p+zsbAUFBalJkybq1KmT+vTpc8J2wcHBeuyxx3T99dfr/fff1w8//KCDBw+qqKhI0dHRat26tS688EJdcsklio2NrdWftV+/fpXTTvj5+emKK6447TaDBw/WJZdcoqVLl2rjxo3av3+/CgoKFBYWpmbNmqlr167q1auXOnXqVKtMnuTN+6UvW/fCVIWGBGvMmDG12t7f3/+4i0fucuy/S4fDocLCwuMunEVFRenee+/VgAEDavS8FaV5SkqK/P39FRYWpoKCgsq7WcLCwjRt2rST7q+TJ0/Wgw8+eEJZJB2dymbmzJknFCH333+/hgwZoqysLN17770KDAxUQECACgsLZbfbNWnSJC1cuPCk88hff/31Wrhwod58800tX75c0dHR8vPzU8eOHSsvHISFhWnevHkaPXq0fv31V02fPl0zZsxQeHi4CgsLK/98N910U43+vk6nY8eOmj17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Ho88Hs+B37tcLsN/KwAAgJYrLCxUjx49Wvy4V199VXPnzlVDQ4Mkye12y+FwaNu2bdq2bZv++te/au7cuTrjjDOOeIx33nlHU6dOVUNDg5KSkuT3+7Vr1y4tX75cn332mZ599lklJCT85HGvv/66Zs6cqcbGRklSUlKS8vPz9cwzz+iDDz7QZZdddtTsX331le666y7t27dP0o9r/3s8HhUWFuq9997TqlWr9F//9V9HvRiwYcMGzZo1S9XV1fJ4PIqJYY4iACDy8OoGAAiJ2tpalZWVqby8/EBp3xL7PxxKUkNDg6qrq3/yPTExMfJ4PEpKSlJycrISExPlcDjanB0AACDQTjjhBDkcDlmWpYULF2ru3Lnq1q1bsx//4YcfaubMmYqJidGNN96oK6644sBs++3bt+vxxx/X6tWrNWHCBL3wwgtNzsQvKSnR9OnTNXToUN1yyy3KysqSz+fTX//6V82fP19bt27Vc889p+HDhx/yuE2bNmnWrFlqbGzUaaedpokTJ6p79+5qaGjQmjVrNGfOHD399NNHzL57926NHDlSFRUVB2bs5+TkKCYmRsXFxXrxxRf1zDPP6NFHH1WPHj10zjnnNHmcWbNmqWfPnho/frxOOOGEA393AAAiidN0AABAZLIsS5WVldq1a5e+/vprffXVV9qxY4fKyspaXN43V0NDgyorK1VQUKDvvvtOGzZs0LZt21RSUiK/3x+UcwIAgPBhpwv7HTt21KWXXipJ2rJli6688koNGzZMc+fO1euvv64tW7YccW30+vp6Pfjgg5KkiRMnasSIEcrOzj5wt2L37t01Z84cnX322aqqqtKyZcuaPI7P59MFF1yge++990DB7/F4dPXVV+u3v/2tJDW5ge5///d/y+/3q2vXrlq0aJG6d+8u6cfJFIMHD9asWbNUUVFxxL/7okWLVFFRoYsuukhz585Vnz59DsyeT0tL0/Dhw3XnnXdKkp588skjHic5OVn//d//faC8l9SiiyB2YadxCQCwH2bgAwACxrIslZWVqbS0VGVlZQdu5zaloaFB+/bt0759++RwOOT1epWcnKy0tDRusQYAAMZNmDBB6enpWrZsmWpqavTdd9/pu+++O/D1tLQ0DRkyRDfccIPS09MP/Pknn3yioqIipaenH1gPvikXX3yxPv74Y3366adH/J7//M//bPLPBw4cqGXLlmnHjh3y+XwH1uCvqKjQZ599Jkm6/vrrm1yb/8wzz9TJJ5+sDRs2/ORrZWVl+uCDDyTpJxv3Hp59wYIF+v7777Vv375D/v77XX311U0u7xNuKPABAEdDewEAaDO/36+9e/eqqKhIdXV1puM0ybIslZeXq7y8XDt37lR6eroyMzOPuCEcAACIPPuXrLGLmJgYDR8+XNddd50+/vhjffnll/rmm2/0ww8/qL6+XsXFxVq+fLneeustLVy4UCeddJIk6V//+pckqby8XEOGDDni8evr6yVJ+fn5TX49OTlZXbp0afJrHTp0OPD78vLyA++ZNm3adGBpw379+h3x3P369WuywN+4ceOBx99+++1HfPzB8vPzmyzwf/7znzfr8QAAhDMKfABAq9XX16uoqEh79uwJqyVqLMvS3r17tXfvXqWkpCgzM1NJSUmmYwEAgCCz60znpKQkXXTRRbrooosk/bh30D//+U89//zz+tvf/qbS0lJNmDBBr7zyiuLi4rRnzx5JP74X278J7NEcafnCo81ed7lcB35/8F2VxcXFB36fkZFxxMcf6Wv7s0tqVnbpx6V+mpKWltasx9udXcclAMAeKPABAC1WU1OjwsJCFRcX22oWW2uUlpaqtLRUiYmJysrKUnJyMh+iAACIUOHyGh8XF6fTTz9dp59+uqZOnaqVK1eqsLBQn376qc4555wDEyfOOussLV682HDaltmfPS4uTp988kmbjuV0Rsa2fuEyLgEAZkTGqx0AICTq6uqUm5urb775Rvv27Qv78v5gVVVVB/5uZWVlpuMAAIAgOHhWebi47LLLDvx+27ZtkqT27dtL+nHz21A7eNZ7UVHREb/v4Jn2B9ufvba2Vjt27AhsuDDF3kwAgKOhwAcAHJNlWSosLNTXX3+t0tJS03GCyufzacuWLdq6deuBdWMBAEBkCMei9OBlbtxut6T/W/u9qKhI//znP0Oap0+fPgdmvq9fv/6I37du3bom//zkk08+MOP83XffDXzAMBSOF5YAAKFDgQ8AOKqqqip9++232rlz54ENx6JBSUmJvv76axUVFUXUnQYAAEQzOxWlu3bt0vbt24/5fStXrjzw+z59+kiS/t//+38HZrI/9NBDR1wjfr9A3l3o9Xp1xhlnSJL+/Oc/N7m+/ueff97kBrbSjzP4Bw4cKEn605/+dMz/BtFw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//Ef5/jjj09HR0eGh4dzzz335K/+6q9y//3356KLLspf/uVfTug1nnzyybnrrrty55135rnnnktXV9cBz/nBD36QJ598MsmBBf6XvvSlXHnlleno6Mgf/MEf5JRTTsm8efOyb9++PPzww/nsZz+bLVu25M/+7M9y3XXXpb29/YD9P/TQQ/ne976Xs88+O2eddVb6+vqyd+/e/Md//MeEXgMAwFQql8tpb28/6P/HAADTz5I/gEPwjSsvSkd7W84777yio0yan/3sZ/n4xz+eLVu2JEmOP/74/UbA/PVf//V4ef+5z30uRx99dFpbW5MknZ2dOfPMM/PRj340SXLVVVdlZGRkfNvHHnss1157bZLk7LPPzoUXXjhe3icvropfuXJlLrnkkv0y3XXXXfnWt76VuXPn5oorrshv//Zvp6OjI0lSqVTym7/5m/niF7+Ytra2fOtb38qPfvSjCb3WE044IZ2dndm7d29uu+22gz7na1/7WpLkzW9+cxYuXDj+9e3bt+fzn/98SqVSPv3pT+dd73pX5s2bl+TFX3SXL1+ez33uc1m+fHm2bdt20NE7yYvvLDjzzDPzp3/6p+nr60uStLa2ZnBwcEKvAQAAAJg9FPgAE/TM449kyw1X5oMXXPCK8++T1PXbhFevXj3+8ba3vS2nnHJKvvrVryZJlixZst8YnFqtlptuuilJcuaZZ77sjc5WrlyZjo6ObN++PQ899ND412+++eaMjo6mp6fnkOa7j5XfJ510Uvr7+w/6nPnz52fFihVJkn/8x3+c0H4rlUp+67d+K0lyyy23HPD43r17c/vtt48f+6W+/vWvZ8+ePVm+fPn4DX9/WXNzc1avXp3kv97R8MuampqyZs2aCeWdbPV8XgIAAAAHMkIHYII2feFDGRgYzLp164qO8pq83KiWk08+ORdccMF+NxV79NFHs2PHjiTJRz/60Vec1b579+4kyU9/+tP86q/+apLk/vvvT5K89a1vPaSblX3/+99Pknz1q189aNE+ZufOnUlefBfBRJ188sm54YYbxkflDA0NjT82NlqntbU1b3/72w+a6V//9V/HS/qD2bNnT5IXvw8Hs3DhwsyZM2fCeSeTAh8AAAAaiwIfYAKefOh7uf+2r+Sqq65KtVp91efXc1H63e9+N8mLq+vHbkJ72WWX5Wtf+1qWLl2as88+e/y5Tz/99PjnP//5zye0/7ECO/mviwWHMh5mZGQk27dvT/JiQT9W0k/0mK/mzW9+c4aGhvLkk0/m61//etauXTv+2NjFghNOOOGA+fhj34vh4eEMDw8fdqaiynsAAACg8SjwASbg1svOzxveeNR+5fYrqecCf0ypVMq8efPyzne+M4sXL8573vOe8Rnuxx57bJJkdHR0/Pm33npr5s6de9jHmqh9+/aNf/6JT3wiJ5544mEd85WyvOMd78iGDRtyyy23jBf427dvz3e+850kL67S/2Vj34t3vvOdOf/88w/7+K/0Loap1gjnJQAAAPBfzMAHeBU/3rI5D9+zKZde8omXnQH/yxqtKF2xYkVOOumk1Gq1fOpTnxov0V9a2P/4xz8+5P2O3eR169atE96mUqmks7PzsI85EWMF/eOPP55//ud/TpLcdtttGRkZSV9fX4477rgDthn7XkxVpunQaOclAAAAzHYKfIBXUKvVsuny83PsW96a3/3d353wduVyeepCTZFzzjkn5XI5//Zv/5abb745SbJ06dJ0dHQkSTZt2nTI+/y1X/u1JMk//dM/TWjszJhf//VfT5Lcfvvt+70LYLIsWrRoPNvY2JyxP1evXn3QCzVjmR544IGXnW9f7yZ6AQoAAACoDwp8gFfww29+NY8/8P/yqU9eekirlxuxKF24cOH4jVuvuuqqjIyMpLm5OaeeemqS5Oabbx6/kevLGbvh7Zjf+Z3fSblczo4dO3LFFVdMOMtpp52W5MUV8tdcc80rPnf37t154YUXJrzvMSeddFKSFy9MPProo+Mr8Q82Pmfs+ZVKJfv27csnP/nJ/Ub9/LLR0dE899xzh5xpqjXihSUAAACYzRT4AC9j38hIbvv8BTnxxNVZuXLlIW3bqEXpu971rpRKpWzdujU33HBDkmTt2rVZuHBh9u3bl/e+97259tpr97uh7c6dO3P33Xfnwx/+cM4555z99rdo0aKcddZZSZJrrrkmF198cR5//PHxx3/+859n06ZN+cAHPrDfditXrsyqVauSJJdddlkuueSSPPbYY+OPv/DCC3nggQfy2c9+NqecckqeffbZQ36tJ554YlpaWrJjx4585CMfSZK87nWvy/Llyw/6/Hnz5uW9731vkuSuu+7KunXr8v3vf3+8yK/VavnJT36Sa6+9NqeffnruvPPOQ8401Rr1vAQAAIDZ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2),\n", + " Circle((x+2.5, y+0.6), 0.5, fc=box_bg)\n", + " ]\n", + " \n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=0\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=1.8\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " \n", + " patches.extend( [ FancyArrow(3.1,2.4,0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " Circle((4.2, 2.4), 0.5, fc=box_bg),\n", + " FancyArrow(3.0,1.2,0.35, 0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(3.0,3.8,0.35, -0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,2.4,0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,1.8,0.35, -0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,3.1,0.35, 0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " Circle((6.2, 3.9), 0.5, fc=box_bg),\n", + " Circle((6.2, 2.4), 0.5, fc=box_bg),\n", + " Circle((6.2, 1.1), 0.5, fc=box_bg)\n", + " ])\n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(3.8,2.35, 'Dispatcher', fontsize=18)\n", + " plt.text(2.2,0.55, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(5.9,1.05, 'Sender', fontsize=18)\n", + " plt.text(5.9,2.35, 'Sender', fontsize=18)\n", + " plt.text(5.9,3.85, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(3.0, 5, 'PDS Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f1d19ebf", + "metadata": {}, + "source": [ + "Receiver\n", + "------------\n", + "\n", + "Scans an input directory, uses IPC to tell the dispatcher about it.\n", + "Log activities.\n", + "\n", + "Dispatcher\n", + "---------------\n", + "\n", + "Processes the files arriving as notified by the dispatcher using IPC.\n", + "\n", + "For each file:\n", + "\n", + "* Examine the accept/reject (imask/emask) rules for each sending process.\n", + " The decision to put files in a sender directory was based on evaluating\n", + " sequential regular expression patterns for each sender. Assume 10 regular\n", + " expressions per sender.\n", + "* As a single dispatcher is running, it can assign ensure file names are unique then they are placed in sender directories.\n", + "\n", + "place a file in the directory each sender uses. Log activities.\n", + "\n", + "\n", + "Sender\n", + "----------\n", + "\n", + "Scan the sending directory, send files, remove them.\n", + "Log activities.\n", + "\n", + "\n", + "Design Decisions/Assumptions\n", + "--------------------------------------------\n", + "\n", + "* Developers were asked to provide a single log for the entire system. \n", + "* on the order of 10 receivers.\n", + "* on the order of 100 senders.\n", + "* Assume 10 regex's per sender.\n", + "* it was the eighties... so 1 or two cpus. Servers were very expensive, clustering for HA was a thing, but scaling was accomplished by bigger, more expensive servers.\n" + ] + }, + { + "cell_type": "markdown", + "id": "09e41c5f", + "metadata": {}, + "source": [ + "Routing 1 Product\n", + "--------------------------\n", + "\n", + "* PDS dispatcher routing for all receivers at once.) \n", + "* represents a scale out vs. previous deployments.\n", + "* units of as the number of regular expressions evaluated. (not MIPS.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "id": "c2a69a1e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PDS pre-routing cost 5000 regular exppression evaluations\n", + "Assuming an average regex costs about the same as 500 instructions\n", + "then that means the cost to route 1 product is, on average, about 500000 instructions.\n", + "Then add client-side second evaluation of RE's by sender: 50000.0\n" + ] + } + ], + "source": [ + "S=100 # number of senders to route to.\n", + "re=100 # number of regex's per sender to evaluate. \n", + "R=10 # number of receivers.\n", + "Rp=S*re/2\n", + "RE=100\n", + "s=10 # number of senders selected by pre-routing phase.\n", + "\n", + "print( f\"PDS pre-routing cost {int(Rp)} regular exppression evaluations\")\n", + "\n", + "print( f\"Assuming an average regex costs about the same as 500 instructions\")\n", + "print( f\"then that means the cost to route 1 product is, on average, about {int(Rp*RE)} instructions.\")\n", + "\n", + "Rpc= s* RE*re/2\n", + "\n", + "print( f\"Then add client-side second evaluation of RE's by sender: {Rpc}\")" + ] + }, + { + "cell_type": "markdown", + "id": "20ad3b93", + "metadata": {}, + "source": [ + "Observations:\n", + "--------------------\n", + "\n", + "\n", + "* modern systems have many more cores (16 is common on single servers... ) but also clustering is common. Clustering is scaling with multiple servers, rather than more powerful individual servers.\n", + "* In order to ensure log entries were not corrupted by different processes writing to the log at once, a locking mechanism was used to mediate access.\n", + "* The dispatcher is a single process doing all of the routing.\n", + "\n", + "2004, there was a project to use PDS to replace the existing Tandem Apps application. It didn't work, PDS was too slow, so that motivated some analysis of how PDS worked. The work can be used as motivation for a discussion of application design." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9fba884", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/README.html b/Contribution/Philosophy/README.html new file mode 100644 index 000000000..e9d6b3ebe --- /dev/null +++ b/Contribution/Philosophy/README.html @@ -0,0 +1,140 @@ + + + + + + + <no title> — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Some Development Background slideshows.

+

To make a slide show, use normal notebook stuff:

+
    +
  • To start up the Editor: jupyter notebook –ip=0.0.0.0 –port=8040

  • +
  • to make a slide show: jupyter nbconvert –to slides xx.ipynb

  • +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.html b/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.html new file mode 100644 index 000000000..8cdf68d37 --- /dev/null +++ b/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.html @@ -0,0 +1,1001 @@ + + + + + + + Sarracenia Algorithmic Design — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sarracenia Algorithmic Design

+

What were the issues with Sundew that led to Sarracenia?

+
    +
  • Problems/Weaknesses of Sundew

  • +
  • Key Decisions for Sarracenia

    +
      +
    • Use Folders/Directories

    • +
    • Add a robust message queueing system.

    • +
    +
  • +
+
+

Problems/Weaknesses of Sundew:

+
    +
  • push only, required someone else to send to us for us to receive. Senders always must configure who they are sending to.

  • +
  • limited parallelism: each receiver or sender is a single process, only a single sender for a single destination.

  • +
  • without dictionaries, the routing algorithm falls back on relatively expensive PDS routing. Message routing (the cheap kind) was disappearing.

  • +
  • internal design details, make the path names highly constrained (had to have colons in them, no folders, etc…) Nobody without very specific directory structures could use it.

  • +
+
+
+

Use Folders/Directories

+
    +
  • Sundew used only the name of the file to make routing decisions.

  • +
  • Sarracenia uses the entire path.

    +
      +
    • It allows for any directory tree to be transferred, and for the

    • +
    • all the directories in the tree to contribute to routing decisions.

    • +
    • The entire path becomes file metadata.

    • +
    • more compatible/adoptable for different file flows.

    • +
    +
  • +
+
+
+

Add RabbitMQ

+

Introduced rabbitmq/amqp broker. After years of study of performance (it was used as a pure dissemination tool, outside of Sundew for six years prior to start of work on Sarracenia, which made a message broker central to data routine.) We needed to ensured that, as a componentm it would scale reasonably well for our loads.

+

Using RabbitMQ, we introduced topic/based routing, as an analog to dictionary routing, but much more applicable to non-message traffic (aka files.) should improve routing decisions for files, while not hurting routing for messages too much. Product mix is much more file oriented than message oriented in recent times.

+
    +
  • topics provide a pre-filtering function that substantially reduces the amount of routing work done in the application. each sender now only has to check its own regexes.

  • +
  • the topic matching is done by the broker, and the broker has it’s own parallelization methods.

  • +
  • by sharing of queues, can parallelize all transfers (except initial one), both reception and transmission.

  • +
  • can obtain new products by subscribing to other data pumps. One is not restricted to only receiving data sent by others.

  • +
+

Introduced an architectural constraint or weakness in the form of a message broker. Introducing such components is a rare, exceptional event.

+
    +
  • Use of message brokers do not introduce synchronization to an app. (messages are not locks.) they are queues so every process operates at it’s own speed.

  • +
  • It needs to be well done. RabbitMQ is well done. message brokers are hard.

  • +
  • scaling of rabbitmq is, in principle, possible, we have done so in some deployments.

  • +
+
+
+

Add RabbitMQ

+
    +
  • topic based pre-routing.

  • +
  • robust, high performance implementation with good internal design for multi-core.

  • +
  • able to scale beyond one node.

  • +
+
+
[18]:
+
+
+
+# make a fig here...
+# should have multiple readers for queue...
+# might show multiple subscribers for reading from different pumps.
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow
+
+def triple_circle(x,y,box_bg):
+    return [
+       Circle((x+0.4, y+0.9), 0.5, fc=box_bg),
+       Circle((x+0.2, y+0.7), 0.5, fc=box_bg),
+       Circle((x, y+0.5), 0.5, fc=box_bg)
+    ]
+
+def directory_polygon(x,y,box_bg,arrow1):
+   return [
+       Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),
+       Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),
+       Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),
+       FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)
+       ]
+
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(15, 15), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 9)
+    ax.set_ylim(0, 6)
+
+    x=0
+    y=3.6
+    patches = []
+
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=0.2
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=1.8
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    patches.extend(triple_circle(4.5, 1.8, box_bg))
+    len=0.5
+    patches.extend(
+        [
+          FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )
+
+        ])
+    patches.extend(triple_circle(6.5, 3.6, box_bg))
+    patches.extend(triple_circle(6.5, 1.8, box_bg))
+    patches.extend(triple_circle(6.5, 0.2, box_bg))
+
+    for p in patches:
+        ax.add_patch(p)
+    plt.text(4.25,2.45, 'Message', fontsize=18)
+    plt.text(4.25,2.25, 'Broker', fontsize=18)
+    plt.text(2.2,0.75, 'Subscriber', fontsize=18)
+    plt.text(2.2,2.35, 'Subscriber', fontsize=18)
+    plt.text(2.2,4.15, 'Subscriber', fontsize=18)
+    plt.text(6.5,1.05, 'Sender', fontsize=18)
+    plt.text(6.5,2.35, 'Sender', fontsize=18)
+    plt.text(6.5,4.1, 'Sender', fontsize=18)
+create_base()
+plt.text(2, 5.2, 'Sarracenia Component Design',fontsize=36)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_5_0.png +
+
+
+
[20]:
+
+
+
+"""
+
+Estimating the cost to route 1 product, using topic based routing along with the traditional regular expressions.
+
+
+"""
+from math import log
+
+K=10000   # number of topics/folders in the hierarchy
+B=8       # branching level at each level in the hierarchy
+n=10      # number of characters in a topic name
+b=2       # number of bindings per sender
+S=100     # number of senders
+
+
+# routing cost on the broker, in integer ops.
+# log(K,2)*n - lookup each topic in a dictionary to map it to an integer
+# log(K,B)   - the depth of the higherarchy is a function of the total, and the number of choices at each level.
+# log(B,2)   - the time to search each level in the hierarchy, comparing integers.
+#
+Rpb = ( log(K,B)*(log(K,2)*n + log(B,2))*b*S )
+
+# AMQP exchanges function as an additional topic in the higherarchy at the top of the tree.
+# so they can be ignored.
+
+print( f" the cost on the broker to route a single product: {int(Rpb)} in integer ops"  )
+
+# this is pretty iffy, used this conversion throughout, but if RE's are much cheaper, then
+# ... the numbers change alot.
+# convert integer ops to regular expressions.  This is difficult to estimate
+# https://swtch.com/~rsc/regexp/regexp1.html "Regular Expression Matching Can Be Simple And Fast (but is slow in Java, Perl, PHP, Python, Ruby, ...)" -- Russ Cox, rsc@swtch.com
+# it lists O(m,l)
+#
+m=30 # the length of the regex being matched to.
+l=50 # the length of the string being matched.
+RE=l*m
+
+print( f"\nregex's are O(m,l) = {RE} hmm... sounds like too much.")
+# Seems overly pessimistic, overriding.
+
+RE=100
+
+print( f"overriding source, lowering estimate to {RE} more investigation needed.")
+
+# on the client side, in # of regex evals.
+
+s=10   #number of senders pre-routed to by the broker.
+re=10  #number of regular expressions per sender to evaluate.
+
+Rpc = s*RE*re/2
+
+print( f"Assuming {s} senders selected by broker, each one needs to look at the {re}'s in it's configuration")
+
+print( f"Client-side routing cost is {round(Rpc)}")
+
+Rp=Rpb+Rpc
+
+print( f"\nt total cost={round(Rp)}")
+
+
+
+
+
+
+
+
+
+
+
+ the cost on the broker to route a single product: 120366 in integer ops
+
+regex's are O(m,l) = 1500 hmm... sounds like too much.
+overriding source, lowering estimate to 100 more investigation needed.
+Assuming 10 senders selected by broker, each one needs to look at the 10's in it's configuration
+Client-side routing cost is 5000
+
+t total cost=125366
+
+
+
+
+

The Algorithmic Cost to Route 1 File

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Application

Pr

Client

Dominant Term/ compute cost scales as a function of

Pre-Routing Done By

PDS

500

50

product of the number of receivers, senders, products

One Dispatcher process

Sundew File

500

50

product of the number of senders, products

All (10ish) receivers

Sundew Msg

<1

5

log of the number of messages

All (10ish) receivers

Sarracenia

120

5

log of the number of topics/folders

Message broker

+
    +
  • Units: Kilo Instructions per second. 500 = 0.5 MIPS.

  • +
  • broker is a service, but is multiprocessing/scaled itself. not a single process.

  • +
  • sender processes do the client-side routing in all cases.

  • +
  • For Sundew, this number is optimistic (bulletin) case, file case is more like PDS, but multiple routers.

  • +
  • for Sarracenia, this number applies to all routing.

  • +
  • give scaling factors the large the configuration, the bigger the perf. advantage for topc routing.

  • +
+
+
[ ]:
+
+
+
+
+s=10 # number of senders / product shipped.
+ServerPeak=1000
+
+#for PDS
+clientSide=1000/0.050 = 20000
+Dispatcher=1000/0.5= 2000
+PDSPeak=2000
+# limited to slowest element, which is the dispatcher, so 2000 is upper bound.
+
+#For Sundew
+SundewClientSide=1000/0.005= 200000
+SundewReceivers=1000/0.182 = 5494
+SundewPeak=SundewReceivers*s=54940
+# limited minimum of client and receivers, so 54.9 is it.
+
+#For Sarra
+SarraClientSide=1000/0.005 = 200000
+SarraBroker= 1000/0.125 = 8000.0
+# how many cpus can a broker use to route? um... it isn't limited.. or is at least unknown...
+# hmm... defined by the broker implementation.lets just use s.
+SarraPeak= SarraBroker*s= 80000
+
+
+
+
+
+
+

Overall Server Algorithmic Ceiling

+

Assume a 1 GIPS machine within infinite cpus, and take IP cost per route, so the number of products that can be routed by a server is going to be inverely related to cost to route each one: route/second = 1000 MIPS / cost to route from previous cell. The other contributor is the number of processes doing the routing.

+ + + + + + + + + + + + + + + + + + + + + + + + + +

Application

Route/s

Routers

Total

PDS

2000

1+10

2000

Sundew

5494

10+10

54940

Sarracenia

8000

10+10

80000

+
    +
  • These numbers are algorithmic maxima, never to be seen in real life.

  • +
  • The Sarracenia one requires a guess at parallelization of Rabbitmq, is conservative.

  • +
+
+
+
+

Duplicate Suppression

+
    +
  • All message passing networks implement “Duplicate Suppression”

  • +
  • typical:

    +
      +
    • checksum the message, store the checksums.

    • +
    • when you get a new message compare it’s checksum to what is in the store.

    • +
    • if there is a hit, then it’s a duplicate, so discard the message.

    • +
    +
  • +
  • Does not need to be perfect…

    +
      +
    • a few duplicates is ok, but need to prevent “storms”

    • +
    +
  • +
  • Simple if one process is handling an entire flow.

  • +
  • Not simple if > 1 process share the flow.

  • +
+
+
[16]:
+
+
+
+# make a fig with a global cache.
+# make a fig here...
+# should have multiple readers for queue...
+# might show multiple subscribers for reading from different pumps.
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch
+
+def triple_circle(x,y,box_bg):
+    return [
+       Circle((x+0.4, y+0.9), 0.5, fc=box_bg),
+       Circle((x+0.2, y+0.7), 0.5, fc=box_bg),
+       Circle((x, y+0.5), 0.5, fc=box_bg)
+    ]
+
+def directory_polygon(x,y,box_bg,arrow1):
+   return [
+       Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),
+       Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),
+       Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),
+       FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)
+       ]
+
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(15, 15), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 9)
+    ax.set_ylim(0, 6)
+
+    x=0
+    y=3.6
+    patches = []
+
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=0.2
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=1.8
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    patches.extend(triple_circle(4.5, 1.8, box_bg))
+    patches.extend(triple_circle(4.5, 3.5, box_bg))
+
+    len=0.5
+    patches.extend(
+        [
+          FancyArrowPatch( (3.4,4.4), (3.4,4.5), connectionstyle="arc3,rad=10.5" ),
+          FancyArrowPatch( (3.3,4.2), (3.3,4.3), connectionstyle="arc3,rad=10.5" ),
+          FancyArrowPatch( (3.2,4.0), (3.2,4.1), connectionstyle="arc3,rad=10.5" ),
+
+          FancyArrowPatch( (6.2,4.4), (6.2,4.5), connectionstyle="arc3,rad=-10.5" ),
+          FancyArrowPatch( (6.3,4.2), (6.3,4.3), connectionstyle="arc3,rad=-10.5" ),
+          FancyArrowPatch( (6.2,4.0), (6.2,4.1), connectionstyle="arc3,rad=-10.5" ),
+
+          FancyArrowPatch( (6.4,3.3), (6.5,3.4), connectionstyle="arc3,rad=-16.5" ),
+          FancyArrowPatch( (6.3,3.1), (6.4,3.2), connectionstyle="arc3,rad=-16.5" ),
+          FancyArrowPatch( (6.2,2.9), (6.3,3.0), connectionstyle="arc3,rad=-16.5" ),
+
+          FancyArrowPatch( (3.3,3.3), (3.2,3.4), connectionstyle="arc3,rad=12.5" ),
+          FancyArrowPatch( (3.3,3.1), (3.2,3.2), connectionstyle="arc3,rad=12.5" ),
+          FancyArrowPatch( (3.3,2.9), (3.2,3.0), connectionstyle="arc3,rad=12.5" ),
+
+          FancyArrowPatch( (3.1,1.6), (2.9,1.65), connectionstyle="arc3,rad=28" ),
+          FancyArrowPatch( (3.3,1.5), (3.1,1.55), connectionstyle="arc3,rad=28" ),
+          FancyArrowPatch( (3.5,1.4), (3.3,1.45), connectionstyle="arc3,rad=28" ),
+
+          FancyArrowPatch( (5.9,1.6), (6.1,1.65), connectionstyle="arc3,rad=-28" ),
+          FancyArrowPatch( (6.1,1.5), (6.3,1.55), connectionstyle="arc3,rad=-28" ),
+          FancyArrowPatch( (6.3,1.4), (6.5,1.45), connectionstyle="arc3,rad=-28" ),
+
+
+
+          FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )
+
+        ])
+    patches.extend(triple_circle(6.5, 3.6, box_bg))
+    patches.extend(triple_circle(6.5, 1.8, box_bg))
+    patches.extend(triple_circle(6.5, 0.2, box_bg))
+
+    for p in patches:
+        ax.add_patch(p)
+    plt.text(4.25,2.45, 'Message', fontsize=18)
+    plt.text(4.25,2.25, 'Broker', fontsize=18)
+    plt.text(4.24,4.3, 'Duplicate', fontsize=18)
+    plt.text(4.24,4.1, 'Suppression', fontsize=18)
+    plt.text(2.2,0.75, 'Receiver', fontsize=18)
+    plt.text(2.2,2.35, 'Receiver', fontsize=18)
+    plt.text(2.2,4.15, 'Receiver', fontsize=18)
+    plt.text(6.5,1.05, 'Sender', fontsize=18)
+    plt.text(6.5,2.35, 'Sender', fontsize=18)
+    plt.text(6.5,4.1, 'Sender', fontsize=18)
+create_base()
+plt.text(2, 5.2, 'With a Global Cache',fontsize=36)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_11_0.png +
+
+
+

Global Duplication Suppression

+
    +
  • create a service for all the processes participating in a flow to contact.

  • +
  • ask the service if a partipant has already seen a message with this checksum.

  • +
  • if the answer is yet, discard the message.

  • +
  • totally global is the worst case.

  • +
+
+
+

Distributed Duplicate Suppression

+
    +
  • hash the checksum to get a modulo N

  • +
  • publish the message to an array of destinations modulo the checksum

  • +
  • a single subscriber on each destination will get 1/N messages.

  • +
  • Every occurrence of the same message will be sent to the same subscriber.

  • +
+
+
[17]:
+
+
+
+# make a fig here...
+# should have multiple readers for queue...
+# might show multiple subscribers for reading from different pumps.
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow
+
+def triple_circle(x,y,box_bg):
+    return [
+       Circle((x+0.4, y+0.9), 0.5, fc=box_bg),
+       Circle((x+0.2, y+0.7), 0.5, fc=box_bg),
+       Circle((x, y+0.5), 0.5, fc=box_bg),
+
+       Circle((x+0.16, y+0.52), 0.2, fc="white"),
+       Circle((x+0.36, y+0.72), 0.2, fc="white"),
+       Circle((x+0.56, y+0.92), 0.2, fc="white")
+    ]
+
+def directory_polygon(x,y,box_bg,arrow1):
+   return [
+       Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),
+       Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),
+       Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),
+       FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)
+       ]
+
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(15, 15), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 9)
+    ax.set_ylim(0, 6)
+
+    x=0
+    y=3.6
+    patches = []
+
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=0.2
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    y=1.8
+    #patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    patches.extend(triple_circle(x+2.4,y,box_bg))
+    patches.extend(triple_circle(4.5, 1.8, box_bg))
+    len=0.5
+    patches.extend(
+        [
+          FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )
+
+        ])
+    patches.extend(triple_circle(6.5, 3.6, box_bg))
+    patches.extend(triple_circle(6.5, 1.8, box_bg))
+    patches.extend(triple_circle(6.5, 0.2, box_bg))
+
+    for p in patches:
+        ax.add_patch(p)
+    plt.text(4.25,2.45, 'Message', fontsize=18)
+    plt.text(4.25,2.25, 'Broker', fontsize=18)
+    plt.text(2.2,0.75, 'Subscriber', fontsize=18)
+    plt.text(2.2,2.35, 'Subscriber', fontsize=18)
+    plt.text(2.2,4.15, 'Subscriber', fontsize=18)
+    plt.text(6.5,1.05, 'Sender', fontsize=18)
+    plt.text(6.5,2.35, 'Sender', fontsize=18)
+    plt.text(6.5,4.1, 'Sender', fontsize=18)
+create_base()
+plt.text(2, 5.2, 'Distributed Duplicate Suppression',fontsize=36)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_14_0.png +
+
+

Each process has it’s own duplicate suppression (the white component within each instance.) As The number of instances grow, the duplicate suppression performance grows naturally. This meets the “embarassingly parallel” goal. but it does cost complexity for analysts to set up.

+
+
+

Observations

+
    +
  • If you are going to use a global cache, it better be really good.

    +
      +
    • need to have a scaling architecture for the cache itself.

    • +
    +
  • +
  • If you are using a distributed cache, a fair one is fine.

    +
      +
    • scales with the data transmission naturally.

    • +
    • overall performance is far less sensitive to it’s performance.

    • +
    +
  • +
+
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb b/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb new file mode 100644 index 000000000..7f1108470 --- /dev/null +++ b/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e9c590b8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Sarracenia Algorithmic Design\n", + "=========================\n", + "\n", + "What were the issues with Sundew that led to Sarracenia?\n", + "\n", + "\n", + "* Problems/Weaknesses of Sundew\n", + "\n", + "* Key Decisions for Sarracenia\n", + "\n", + " * Use Folders/Directories\n", + " * Add a robust message queueing system.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "6d06c484", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Problems/Weaknesses of Sundew:\n", + "-------------------------------------------------\n", + "\n", + "* push only, required someone else to send to us for us to receive.\n", + " Senders always must configure who they are sending to.\n", + " \n", + "* limited parallelism: each receiver or sender is a single process, only a single sender for a single destination.\n", + "\n", + "* without dictionaries, the routing algorithm falls back on relatively expensive PDS routing. Message routing (the cheap kind) was disappearing.\n", + "\n", + "* internal design details, make the path names highly constrained (had to have colons in them, no folders, etc...)\n", + " Nobody without very specific directory structures could use it.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "1a17273d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Use Folders/Directories\n", + "----------------------------------\n", + "\n", + "* Sundew used only the name of the file to make routing decisions. \n", + "* Sarracenia uses the entire path.\n", + " * It allows for any directory tree to be transferred, and for the \n", + " * all the directories in the tree to contribute to routing decisions. \n", + " * The entire path becomes file metadata. \n", + " * more compatible/adoptable for different file flows.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "51384c53", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Add RabbitMQ\n", + "--------------------\n", + "\n", + "Introduced rabbitmq/amqp broker. After years of study of performance (it was used as a pure dissemination tool, outside of Sundew for six years prior to start of work on Sarracenia, which made a message broker central to data routine.) We needed to ensured that, as a componentm it would scale reasonably well for our loads.\n", + "\n", + "Using RabbitMQ, we introduced topic/based routing, as an analog to dictionary routing, but much more applicable to non-message traffic (aka files.) should improve routing decisions for files, while not hurting routing for messages too much. Product mix is much more file oriented than message oriented in recent times.\n", + "\n", + "* topics provide a pre-filtering function that substantially reduces the amount of routing work done in the application. each sender now only has to check its own regexes.\n", + "\n", + "* the topic matching is done by the broker, and the broker has it's own parallelization methods.\n", + "\n", + "* by sharing of queues, can parallelize all transfers (except initial one), both reception and transmission.\n", + " \n", + "* can obtain new products by subscribing to other data pumps. One is not restricted to only receiving data sent by others.\n", + "\n", + "Introduced an architectural constraint or weakness in the form of a message broker. Introducing such components is a rare, exceptional event.\n", + "\n", + " * Use of message brokers do not introduce synchronization to an app. (messages are not locks.) they are queues so every process operates at it's own speed. \n", + " * It needs to be well done. RabbitMQ is well done. message brokers are hard.\n", + " * scaling of rabbitmq is, in principle, possible, we have done so in some deployments.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "867e5442", + "metadata": {}, + "source": [ + "Add RabbitMQ\n", + "---------------------\n", + "\n", + "* topic based pre-routing.\n", + "* robust, high performance implementation with good internal design for multi-core.\n", + "* able to scale beyond one node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6db39240", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU9f7+/3vTSQgJIYHQi1JERVGaiAiCYMHejw2PHEUR9ViOvaIiej7qUbAX9CAWFESQgwURaYoIiKL0JEBCeu/J7vz+4Ee+JLtJdje7mcnm+bguLpLZnZnXbjabmXvf83rbDMMwBAAAAAAAAAAALCXI7AIAAAAAAAAAAIAzAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCC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/PDDxsCBAxt8HiMiIowJEyYYCxcu9Go/7733ntM2k5KSam7fvn27cc011xgRERH11nDSSScZX331VYP7+fHHH42zzjrL5XN++LU7ceJE448//vD4MVx//fW1ttWzZ89aty9atMg45ZRT6q0/KCjIOO2004zPP//c4327Ulpaajz33HPGySefbNhstgbfL0477TTjnXfeMaqrqz3eT2N/NwoKCoxHH320wd/HqKgoY/Lkyca+fft88tgLCgqMp59+2hgyZEiDj91msxknnXSSMWfOHKOiosLj/fTs2bPW9q6//vpaty9cuNAYMWJEg/sfNmxYo6/bw1z9nnj6r+7faV9x9Tqo+3w0xXnnnee0/YSEBKOsrMyr7f3444/GFVdcYcTFxTX4fMXFxRnXXHONsW3bNq9rLysrM15++WVj6NChbv2MbDabMWDAAOOmm24yvvnmG6OqqqrRfTT0d9tdVVVVxgsvvNDgMUZwcLBx9tlnG2vWrGny/hs7Rty9e7cxefJkIzIyst56OnXqZDzxxBNGcXGxx48XAAB/IsAHAD87//zznU4Q3nzzzWat4YcffjC6d+/u9Ql6SEiI8fDDD3scSDYUxLz99tsNnkS5CgbqOzmrrKw0pk2b1mCwUt8JoFnPjavnavDgwV7XMX36dLf3VVFRYTzyyCNGVFSUx/s57rjjjN9++82jx1bfz2H//v0NBpCu/j344IMe7bspAf5VV13l1oc59f075phjvApum2LGjBlOdcTHxxvl5eXNWocrL7/8cr0f1jX075RTTjG2bt3q0b4aCvDnzJlT74d8rv7dcccdhsPhqLX9iooK45ZbbvHofcLTD1HqC/BLSkqMiy++2KPn8IILLjCysrI82v+RPv74Y6Nr164e/+yOPfZYjz9gbOjvxqpVqzyqo02bNsbixYu9ftwOh8N46aWXjA4dOnj82Hv27GmsWLHCo/3VF+Dn5eW5DJwb+nfdddc1GtS25gB/1apVLh+Pp6+XHTt2GGeffbbHz1tQUJBx4403evyBwcqVK43evXs36Wf2v//9r9H91F3H0wB/8+bNDX4wX/efzWYzbr/99poP/bzZf0MB/uzZsxv8wLbuv169ehk7d+706DEDAOBP9MAHAD9zNVnkp59+2qw1JCUlaf/+/V6vX11draeeekoXXHCBU2sObzz//POaMmWKSktLm7wtu92uSy+9VHPmzJFhGB6vb4Xn5qWXXtL48eO1efNmr+twt3dvbm6uJkyYoBkzZqikpMTj/fzxxx869dRTtWTJEo/XPdLevXs1YsQIrV+/3qP1nnnmGT388MNN2re71q1b16TX+19//aURI0bou+++82FVDfvyyy+dlk2ePNnrCbN9wW636+abb9btt9+uvLw8j9dfv369Ro0ape+//77JtTzzzDOaNm2aKioq3F7nP//5jx566KGa7ysrK3XhhRd61JKourpakydPdvnz8URFRYXOPvtsLVy40KP1Fi9erHHjxiknJ8fjfc6YMUNXXnmlUlNTPV5327ZtOvPMMzV//nyP161r6dKlGj9+vEd1lJWV6ZJLLtHy5cs93l95ebmuvPJK3XnnnV49bykpKZo4caLeeustj9c9Uk5Ojk477TSP33M/+OADTZ48uUn7DmSjR4/WiSee6LR86dKlbm9jxYoVGj58uMu5jhrjcDj0zjvv6PTTT1dGRoZb6yxbtkxnnXWWkpKSPN5fc/rll180duxY/fHHH26vYxiGXn75ZV1zzTVeHcs15MEHH9Rtt92m8vJyt9dJTk7WqFGjvHrfAwDAH+iBDwB+NnLkSL355pu1ln333Xd65ZVXNH36dFNqSkxM1Mknn6xjjjlGvXr1Urt27RQZGamSkhJlZmbq999/1/Lly5WZmVlrvaVLl+rBBx/Uc8895/W+v/nmGz377LM134eHh2vs2LEaM2aMEhMTFRISogMHDujnn39WcXFxo9t79NFHa4VicXFxOvvsszV06FB17NhRZWVlOnDggP73v/+51de0uZ+bBx98UDNnznR529FHH60JEyaof//+SkhIkGEYysvL086dO7Vx40Zt2LDBo4A5Pz9fp556qrZv3+5023HHHafTTz9dxx57bE2v3MzMTK1fv17Lli1TUVFRzX2Li4t12WWXae3atV7N51BUVKSzzz675sTYZrNp5MiRGj9+vHr06KG2bdsqKytLa9eu1aJFi5xOumfOnKnzzjtPw4cP93jf3goODtZJJ52kY489VgMGDFCHDh3Url07GYahwsJC7dq1Sz/99JPWrl0rh8NRs15xcbGuvPJKbd68Wd27d/drjUVFRfr111+dlp9xxhl+3W9jbrrpJr377rtOyyMiIjRx4kSNHj1anTt3VllZmZKTk7V48WJt3bq11n0LCwt19tln6/vvv9epp57qVR1ffPFFrSC+U6dOmjRpkk466STFx8erqKhIv/32mz799FOnQO3ZZ5/VhRdeqGHDhmnatGm1ArsBAwZo0qRJ6tu3r2JjY5Wbm6s1a9bos88+q/VBgcPh0NSpUzVmzBive8Tffffd+vHHH2u+j4+P18UXX6xBgwYpPj5e2dnZ+v3337Vw4UJlZWXVWnfr1q2aOHGi1q9fr9DQULf2N2PGDD366KNOy0NCQjR27FiNHz9eXbt2VXV1tfbv369ly5bpp59+qhXAVVZW6pprrlFISIguv/xyrx73li1b9MADD6iqqkqS1KZNG40bN06jR4+u+buxf/9+ffPNN1qxYkWtdaurqzVlyhRt27ZNMTExbu2vqqpKEyZM0OrVq51uO+qoozR27FgNGjRIcXFxCgkJUXZ2tn755RctW7as1vNeXV2tm2++WYmJiTrvvPM8ftzV1dW66KKLagWhgwcP1sSJE9WnT5+a19vPP/+shQsXqqCgoNb6H374oS688EJdeumlLrcfFxenE044QdKhn9Nff/1V6/bu3bs32p/86KOP9vhxWcW4ceO0ZcuWWsvWrVvn1rpLlizRJZdcUvOaPCwsLExnnHGGhg8fru7duysmJkbFxcVKTk7W999/7/Sa2rBhgy688EL9+OOPDf5e5uTk6Prrr3f68DEkJESjR4/WyJEj1atXL0VHR0s69J6ZmZmpbdu2adOmTS7/7vtDcnKyzjzzTKfXoiQde+yxOv/889WnTx9FR0crIyNDv/76q5YsWVLz4e7HH3/s03mi3njjjVrHWAkJCTXHiAkJCSovL9fu3bu1aNEibdu2rda6mZmZuvnmmz36UAcAAL8xc/g/ALQGmZmZRps2bVxeonveeecZq1ev9nsN7733ntGnTx/jqaeecrv9SXV1tTFv3jwjMTHR6TLnDRs2uLUNV5fAH9kr+pJLLmmwR3HdS8tdXR59eHvBwcHGI4880mDfUleXqpv13BiGYSxYsMDl6+LEE090a56E7Oxs46233jIGDRrkVmuBCy+80GlfI0eONH766acG18vLyzPuuusupxZFvXr1MgoLCxvdb919HnkZ+/Dhw41ff/213nWTkpKMk046yWkbEydObHS/h9evu667LXT69u1rXHzxxcbChQuN/Px8t9ZJTk42rrrqKqd9nnvuuW6t3xQ//vijy9dTdna23/ddn48//thlTRdccIGRmppa73pLlixx2SqlV69ebv0sXLUGOdw2Jzg42HjyySfrbV1RUFDgskXNhAkTjM8//7zm+4SEBOPTTz+tt4Zdu3YZ/fr1c9rOM8880/gTZzi30AkPD6/5HQwKCjLuu+++eh9DWVmZcf/99xtBQUFO+3/88cfd2v+aNWtc9vYfNWqUsWPHjnrXW7dunTFgwACn9WJiYozk5ORG9+vq78aR7xnXXnutkZaWVu/6P/zwg8s+5DNnznTrcRuGYdx+++1O6w8cOND4+uuvndopHam0tNSYOXOmERoaWmvd2NhYt+ZLqdtC58jH3a9fvwZb8mRmZhoTJ050qnvAgAFuPeamzhfia/5uoWMYhvHZZ5+5PKYoLS1tcL29e/casbGxtdYLCQkx7r33XiMzM7PBdTdv3mycfPLJTvu96667GlzPVXu0M8880+15eJKSkowXXnjB6Nu3r99a6DgcDmPcuHFO6yYmJjY4F0dRUZFxxx131Nzf1TGzty10Dv8OhYaGGjNnzqz3PdPhcBgvv/yyy/dMb+bfAQDA1wjwAaAZ3HvvvS5DrMP/unbtatxwww3Gm2++aWzZssWtCcY8UVhY2GDo0JCUlBSjR48eteq96qqr3FrX1Qn44X/Tp0/3uCZXJ2eHw6yGgrSGmPXcZGZmGtHR0U6P5bLLLvOqX3ljwdgbb7zhtK9bb73Vo8f+7rvvOm3j2WefbXS9+l4DkyZNcqv/b05OjtOElUFBQW4FF00JpdwN7V15/PHHa+3TZrMZf/31l9fbc8dbb73l9Fh79erl1302pKioyGXP+3/84x9uve727t3rMsS/7bbbGl23vt7eQUFBbk2MW1lZ6dS/2WazGfHx8YYko3Pnzm71R969e7dTv/2+ffs2up5hOAf4R/577bXX3NrGa6+95rRuaGiosXv37gbXczgcRv/+/Z3WPeecc9yanDUnJ8c4/vjjXf7ON6ahvxszZsxw63GvWbPG6QPHo48+2q11ly9f7rTfCy64wKNJab/55hunEH/q1KmNrlc3wD/8b+jQoUZOTk6j65eXl7vsO+7OQIHWGOCnpKS4fL4be6+uO3dLZGSkR/MdVFRUGGeeeabT72VDAxqGDBlS6/4DBgzw6ljB4XA0+gGFYXgX4Lv6wDYxMbHBD/yO9H//93/1/u57G+BLhz78/O6779yq4emnn3Za/8Ybb3RrXQAA/IkAHwCaQVlZmUeTdUZERBjDhg0zpk+fbixYsMBIT083tf6lS5fWqi8kJMStcLO+IGbYsGE1E5V5or6Ts7vvvtubh+UT3j43DzzwgNPjOO2007x6XhpTVVXlFAydddZZXm1rypQptbbTqVOnRkMEVz8zd0dSH/bqq686bePtt99udD2zQimHw2EMHTq01n7vvfdev+7z0UcfdXqsI0aM8Os+G/LKK6+4DCI9mfB53bp1TkFsVFSUkZub2+B69QX4nkyC/Omnn9b7Hu1uGGQYhjF16lSn9RsL0A2j/gB/8uTJbu/bMAxj8uTJHr9nLlmyxGmdHj16NHiFU1179+51Gklrs9mM7du3N7hefX83Lr74Yrf3bRiGcfnll3v1vI8cObLWOoMGDTIqKys92rdhGMZTTz3lFCI29rfcVYDv7uj9w5YtW+a0jYcffrjR9VpjgF9WVubytfbtt9/Wu84333zjdP+PP/7Y433n5+fXfCB4+N+dd95Z7/3rXlXy9NNPe7xPT9R9jO4E6KNGjXJab9myZR7tt74JupsS4L/88stu77+ystLpg+Nu3bp59BgAAPAHJrEFgGYQERGhZcuWud0Dt7y8XBs2bNArr7yiyy67TJ07d9aYMWP07rvvejQJl6+cffbZio+Pr/m+urpaP//8s9fbe/755xUcHOyL0hQdHa3HH3/cJ9vyhjfPTXFxsebMmVNrWVhYmP773//67Hk50scff6yUlJSa7202m1555RWvtvXoo4/WmksgIyPD44loJemxxx5zux+1JF155ZVOz42rfu9WYbPZdO2119ZatmbNGr/u09UEsYfnMzDD7NmznZbNmTNHQUHuH36ecsopuv7662stKykp0XvvvedxPbGxsXrwwQfdvv+kSZNcTv575plnaty4cW5vx1X/8U2bNrm9/pHatGmjWbNmebTOrFmz1KZNm1rL3nvvvQYn8nX1s/v3v/+tqKgot/fbu3dv3XfffbWWGYbh9N7njqCgII/nF7nmmmucljX2nrFmzRqnHugvvvii23MGHOmuu+6q6UcuHZqA2JvJTu+44w716NHD7ftPmDBBCQkJtZZZ+b3STBERES5/xxuaFL7u799pp52mK664wuN9x8TE6I477qi1bNGiRfXe/8h5aCSpQ4cOHu/Tn/766y+nv3ETJ07U2Wef7dF2XnzxRZ8eB/Xp00fTpk1z+/6hoaFOP88DBw44zXsEAEBzI8AHgGYSGxurxYsX64MPPvB40jfDMLRq1SrdeOON6t+/vz788EM/VelaUFCQjjrqqFrLfvrpJ6+21bdvX40ePdoXZUmSrrjiCrVt29Zn2/OUN8/NqlWrnAKCK664Qj179vR5fZL02Wef1fp+zJgxXk882L17dx1//PG1lq1atcqjbURFRelvf/ubR+u0b99effv2rbVsx44dHm2judWtd9OmTU4THvpSWVmZ0zJPPiTxpf379zv9fIYMGaKhQ4d6vK1bb73Vadm3337r8XauuOIKjwLoNm3aqH///k7Lb7zxRo/2O3jwYKdl3r52L7jgAnXs2NGjdTp27KgLL7yw1rLc3Fxt2LDB5f0rKyudfqcTExN10UUXebRfSbr55psVEhJSa5k3P7szzjjD6X22Ma4muW7sea/7Xtm7d2+vJ4Fu06aNxo4dW2uZp++VkvSPf/zDo/sHBwc7TQJq9fdKM7Vv395pmav3UunQ7833339fa9mUKVO83ve5555b6/uUlJRaH7YfqW5g7+8PhD1V93mRPH+vlKQePXpo/PjxvihJkvT3v//dow+NJWnYsGFOy/gdAgCYLaTxuwAAfOXwqNyrrrpKy5cv1/z58/XVV181ONqrrn379umaa67Rd999p9dff93l6DF3/PHHH/rll1+0detWpaSkqLCwUEVFRfWOyty9e7dTHd4YM2aMV+vVp25A4gv+fm5++OEHp2V1R2v7imEYWr16da1lI0eObNI2e/fura1bt9Z8v3nzZo/WHzFihMLCwjze71FHHaXt27fXfF9QUODxNpqiuLhYP/74o7Zu3ao///xTOTk5KiwsVElJiRwOh8v7H6miokIZGRnq1q2bX+ozDMNp2ZFXSzSntWvXOi1zNRLdHUOHDlXv3r2VlJRUs2z9+vUyDMOjx+fNB4c9e/as9VqXDo249URcXJyio6NrjaDNz8/3uBZJTkG8uy6++GJ99NFHtZb99NNPLh/Lpk2bnK70uvDCC52CeHckJiZq1KhRtd7zduzYoZycHI9GEJ9++uke77tjx46KiopSSUlJzbLG3jPqBuy+eK88kqfvlUcddZS6du3q8X7rftjR3O+VLYmr9+763ldWr17t9D7blNdI3deHdOg14urD/OHDh2vx4sU133/44Yc65ZRTdMstt5j2Pn+kugMXbDabzjrrLK+2NWnSJH399de+KMur9w5XHxbyOwQAMBsBPgCYICQkRJMmTdKkSZNkt9u1ZcsWrVmzRhs3btSmTZu0Y8cO2e32Brcxd+5clZSU6NNPP3V7vxUVFXrllVf03nvv6c8//2zSY/A2gDrppJOatF9/ba85n5u6LWeCgoI0YsSIJu2zPn/99Zdyc3NrLXv//fe1dOlSr7dZ9wOK7Oxsj9avOzLdXXVHkzfXCfWvv/6q559/Xl9++WW9IzPdlZ+f77cAv26blMP7M4OrFjFDhgzxentDhgypFeAXFBRoz549Hl1J4s1VJ0e2QJEOPcddunTxajtHBvjevnbrjqx2l6v3yfraqvj6Zzd06NBaAb5hGNq8ebNHo2yb8p7hboBfVFTk9GHNt99+qxNPPNGrfUtSenp6re8D/b2yJXL1HunqvVRy/cHkJZdc4tMAvb7XyA033FArwDcMQ9OmTdOrr76qG264Qeeff77Xrxdf+P3332t937dvX6f3T3e5umrJW948J66uXON3CABgNgJ8ADDZ4cvdjwxmSktL9fPPP2vlypVasGBBrVHHR1qwYIFeeeUVTZ8+vdH9rF27Vtdff7327Nnjk7q9PZnxtP1Dc2yvuZ+bjIyMWt/36tXL6xPdxhw4cMDlMlfLvZWTk+PR/ePi4rzaT90+1P5sR3N4+//85z/12muvuRyl6Q1/hgCunlezAnxXIdQxxxzj9fYGDhzoch+ehPKuWmU0pu5rzpttuNqON6/dkJAQj9vIHNanTx+FhYWpsrKyZll9PZ2b62fnieZ4zzh48KDT73lmZqZPe1+b9V5ZXV3t1XYCXWlpaa3ficPatWvn8v6u/m7W/dCnqep7jVxwwQW68MIL9cUXX9Ravm3bNt1zzz2655571L17d40aNUpDhw7VyJEjdfLJJ3t15Yw36tbdq1cvr7fl6soEb3nzO+Rqzgt/H28AANAYeuADgAVFRkZq7NixevLJJ/XXX39p+fLlOvbYY13e96mnnlJpaWmD21u5cqUmTJjgs4Ba8v5kpr4TY281dXtmPDd1R8R7Gwq6w9PAyBuejkr3ZkLI5lZVVaXLLrtMc+bM8Vl4f3i7/uJqZH/dD4uai68n1HX1O1L396gxvnjdmfnajY6ObtJI37rvlfV9uBOoP7vGNMd7paeT0LeE98qWrL73x+7du7tcbvbf0w8//LDB+WP279+vjz76SHfddZdGjBih9u3b6+KLL9ann37a4KTVvlD3faMp86/4cu4WfocAAIGCAB8AWoCJEyfql19+0dlnn+10W2Zmpr788st6183Pz9cVV1zhFPIHBQVpwoQJmjlzppYvX65t27YpOztbxcXFstvtMgyj1j9v+oi64uvRYE3ZnlnPTd05D/w5Ca+rMA6NmzVrVq12BYd17dpVt956q+bNm6f169dr//79ys/PV3l5udPrYuXKlc1as6sJV5OSkjwOS33hyHYx0qF+yJGRkV5vz9Xks3X3Eeg8mYDXnfXre/5cLW/KvlvKz473ytbH1UTOwcHB6tGjh8v7m/0aiYyM1IcffqhvvvlGY8aMafQDveLiYi1atEhXXHGFjjrqKL3xxhsu50rxhbofEHgzz81h3s7tBABAIKOFDgC0EG3atNHHH3+so446yqn9wIoVK3TllVe6XO/pp59WVlZWrWVDhgzR/PnzPeoN2tTe31Zk1nPTrl27WqFq3clOfclVL98vvvhCF1xwgd/22dJlZmZq5syZtZaFhITo+eef12233eb2h0bN/TszePBg2Ww2p4Bmw4YNXk8m6K26LaEMw1BpaanXIf6Rfczr20egc/UcNGX9+p4/V8ubsu+W8rNz9V750ksv6Y477jChGjSHuvPRSIdaPkVERLi8f93XSGxsrCmh/plnnqkzzzxTKSkpWrp0qVatWqW1a9cqLS2t3nVSU1M1depUffXVV/rss8+aFLC7EhMTU+sKhaZ8SFd3kAMAAGAEPgC0KO3atdPkyZOdlu/YsaPedT7++ONa33fv3l3fffedxxN7mTGK19/Mem46dOhQ63t/BgDx8fFOy46cDBTOvvzyS6erMmbNmqU777zToys+mvt3Jjo62uUkp99//32z1iG5bpvSlH78rtb1tj94S1VUVNSk0bN1Q7H62uK01p8d75Wtz4oVK5yWnXrqqfXev+5rJD8/37R5RiSpZ8+emjZtmj799FOlpqYqJSVF8+bN00033VTvZOlLlizRtGnTfF5L3feNprQbao5WRQAAtDQE+ADQwgwbNsxpWX0TAv71119Ok67dfvvtHvcXraqq8umkp1Zg5nOTmJhY6/vk5GS/tZTo1KmT0zJfT7oXaL799tta37dv31633Xabx9vZu3evr0py2/nnn++0bO7cuS4navSnhIQEp2V//fWX19v7888/nZa5ClwDWXV1tdevqaSkJKfXQH0TgLfWnx3vla3LihUr9McffzgtP++88+pdx+qvkR49eujqq6/WG2+8of3799fM8VPXO++8o23btvl033XnDfjjjz+8/sDRSs8pAABWQYAPAC2Mq4C5vlHB+/fvd1p22mmnebzPzZs3ezz5ntWZ+dyccsoptb53OBwuL+X3hUGDBjm1A1i+fLlf9hUo6r42hg8f7lW7AX/9TBty/fXXKyio9uFdVlaWFixY0Kx1nHTSSU7LNm7c6PX2fvnll1rfx8bG6qijjvJ6ey3Vr7/+6rP1XF2tIfn/Z2ez2Vzuw2wdO3ZUr169ai1bu3atJfv1o+n+7//+z2lZYmKixo8fX+86rgZQ/O9///NpXb40ZswYff3117rppptqLTcMQ4sWLfLpvuo+NwUFBdq+fbtX2/rpp598URIAAAGFAB8AWpiMjAynZa5GhUmuR+Z707rgk08+8XgdqzPzuRkzZozTsv/+978eb8cdERERGjVqVK1lBw8edNk6AIfUfW1487rIzs5u9klspUMjMC+++GKn5ffff3+ztnpw1Ybis88+82pbv/76q1MrkxEjRjQ6gWMg+uKLL7xab+HChU7LRowY4fK+J510ktOHfl988YXsdrvH+83IyNDq1atrLevfv78lW+hIcgpvKysr9emnn5pUTfNyNRDAm595S/Duu++6DN5vvfXWBj+sPfPMM52WffLJJ6qurvZpfb72zDPPKDg4uNYyX49yrzswQZLmz5/v8XYcDodTe0MAAECADwAtjqt+1vWNRI2KinJaVl+7nfrk5+fr3Xff9WidlsDM5+b000936hf76aefKiUlxeNtucPVhLWPP/64X/YVCOq+Njx9XUjSnDlzTLtq5amnnnIK4w4cOODzvsfl5eV69dVXXd7WrVs3HXPMMbWWbdy40asR5K724aotRGvwxRdfOE283ZisrCwtXry41rK4uDiXo4klKTQ0VGPHjq21LD093asPD958802ncNPKPztX75VPP/10s7egMoOriYX9OcG6WTZs2ODyvbBLly666667Gly3a9euTleuJCUlae7cub4s0ec6dOjg1BqroKDAp/uYMGGC03HNW2+95fF+Pvroo4Br2QgAgC8Q4AOAny1ZssRnE+Ht2bPH5WjASZMmubx/586dnZZ98803Hu3ztttuM3WSNn8x87mJjIzU9OnTay2rrKzUtddeK4fD4fH2GnPjjTc69d1fs2aNZs2a5fN9BYK6r41169appKTE7fW3bdummTNn+rost/Xv318PPvig0/L58+fr9ttv98lrbNeuXRo1alSDo5NdhWS33XabR32RN2zY4BSORUVF6YYbbnB7G4GkrKxM999/v0fr3H///U6TMt9www0KDw+vdx1XP7t77rnHaTsNSUlJ0bPPPltrmc1m88sEmr5y7rnn6sQTT6y1LCkpSXfccYc5BTWj6Ohopw/+zJjHw5/efvttjRkzxunDVZvNptdff93lB/t1PfTQQ07L7rnnHu3cudNndfpaeXm58vLyai1zNddFU7Rp00bXX399rWUZGRm6++673d5Genq6R/cHAKA1IcAHAD/76quv1K9fP91www1e9wOVpLS0NF100UVOAUpCQkK9PVsHDx6stm3b1lr2n//8x+3RTTNmzNCHH37oXcEWZ/Zzc8cddyg2NrbWstWrV+uqq65SRUWFx9traPR+mzZtXIYODz74oGbPnu3xvg5bvny5br31Vq/Xt6q6cyEUFxfriSeecGvd5ORknX/++V79DH3p4Ycf1umnn+60/JVXXtHll1+uzMxMr7ZbXl6uZ555RieeeGKjo+knT57s1Crlp59+cntC4JSUFF166aVOHzjceOONTr87rcm7776rt956y637vvXWW05XCYWGhuqWW25pcL1zzjlHAwYMqLUsOTlZf/vb39xqF5KXl6cLLrjA6e/Veeedp379+rlVuxlsNptmzJjhtPz111/XAw884PWHX+vXr9ff/va3ppbnV0FBQRo4cGCtZV9//bVfPlRubitXrtSkSZP0j3/8Q2VlZU63P/roow1OXnukiy66SEOGDKm1rKCgQGeffbbXE8MWFRXp+eef17x581zevnv3bs2YMcPjq28Oe+ONN5z+Jp1wwglebashd911l9Nx1TvvvKP77ruv0ddRamqqxo0b57JNJAAAIMAHgGZRXV2tuXPn6phjjtGIESM0e/ZsHTx40K11S0tL9frrr2vw4MH6/fffnW5//vnnnfoVHxYaGqoLL7yw1rK8vDyNGzeuwf6naWlp+tvf/qZHH320Zlm7du3cqrelMPu5iYuL0/vvv+/Ux/vTTz/VKaecou+++67RbeTl5endd9/VCSecoMcee6zB+06bNs2pPYTD4dD06dN10UUX6bfffnOr7qSkJM2aNUuDBg3S2WefrR9//NGt9VqSSy65xGki2Oeff16PPPJIg+HlRx99pFNOOaVm1KqZvzOhoaFatGiRjjvuOKfbPv/8cx111FF69NFH3f7AKiUlRU899ZR69+6thx56yK2R2FFRUXrttdeclr/66qu67LLLGgxqli1bplGjRjlNKNyrVy89+eSTbtUcaMLDw2veL6ZOnaqHHnqo3g+KKioq9NBDD2nq1KlOtz300EONTgBss9n0zjvvOPXNXrx4sSZMmKDdu3fXu+7PP/+sUaNGOb2nxMbG6pVXXmlwv1YwadIkpyukJOnZZ5/V2LFj3X7PO3jwoF555RWNHDlSI0eO1JdffunrUn1u5MiRtb7fsWOHpkyZ4rf2bv5y4MABLVy4UPfdd5+OO+44nXHGGfrqq69c3vfhhx/2uKXcRx995PTh5N69ezV8+HA9/fTTbrWNcTgcWrlypaZOnaoePXroX//6l9LT013et7i4WI8++qh69Oiha665RosWLXL5QURdlZWV+ve//61777231vLg4GBdeeWVja7vqe7du+u5555zWv7cc89p2LBhWrhwodPfjuTkZD3zzDM65phj9Oeff0py3U8fAIDWznm2IgCAX/3888/6+eefNX36dPXq1UvDhw/XwIEDFR8frw4dOshms6mwsFApKSnaunWrVqxYUW/7jssvv9zpkuW6HnnkEX3yySeqqqqqWbZz504NHjxYZ511ls444wx169ZN1dXVOnjwoH744Qd99913tYKhv//979qzZ49WrVrlmyfBIsx+bs4//3w99NBDeuqpp2ot37x5s84880z17dtXEyZMUP/+/ZWQkCDDMJSfn6+dO3dq06ZNWr9+fU3tgwcPbnBfNptN8+bNcxmsffHFF/riiy90wgknaMyYMerbt686dOgg6VCf/+zsbG3dulW//vprwLVUcKVfv3665ppr9MEHH9Ra/tRTT2nu3Lm69NJLNWjQILVt21a5ubnasWOHvvzyS+3Zs6fmvpGRkZo1a1ajI539qX379lq1apXOO+88rVu3rtZtxcXFmjFjhmbMmKGBAwdq7Nix6tatmxISEhQbG6vy8nJlZGRox44dWrdunbZt2+ZR65vDLr/8cn399ddOo8A/++wzffXVVzr77LN12mmnKTExUeXl5UpKStKXX36pLVu2OG0rNDRU8+bNU0xMjMd1BILExESde+65evXVV+VwOPTMM8/orbfe0iWXXKJBgwapQ4cOysnJ0datW7Vw4UKXV1mcfPLJLtsruTJy5Eg99thjtT6slA6NZh44cKDGjRunM844Q127dpXdbtf+/fu1bNkyrVu3zum1YrPZ9MYbb6hHjx7ePwHN6MUXX9TOnTv19ddf11r+448/6vTTT1f//v01ZswYHXvssYqLi1NoaKjy8/OVm5urbdu26ddff9WOHTta3Oj1v//973r99ddrLXvvvff03nvvKSEhQQkJCQoNDa11+5AhQ/T22283S31ffvmlU4sjSTIMQ8XFxcrPz1dBQYFbk+926NBBb7zxhi655BKP6zj66KP16aef6pxzzqk1P0JJSYkefvhhzZw5U6NGjdKpp56qzp07KzY2VqWlpcrPz9f+/fu1adMmbdq0yeMWfOXl5frwww/14Ycfqk2bNjrxxBM1ePBg9e3bV7GxsYqOjlZFRYXS09P122+/afny5S7fB+6//351797d48ftjqlTp2rdunVOVxP8+uuvuuSSSxQaGqpOnTopOjpamZmZysnJqXW/2NhYzZ07V/3796+1vO6HiQAAtDYE+ABgouTkZCUnJ3u17vXXX6933nmn0fv169dPs2fP1s0331xrucPh0LJly7Rs2bIG1z/jjDP06quvauLEiV7VaWVWeG5mzJihuLg43XvvvU6hw65du7Rr1y6vt11X27ZttXr1at1www36/PPPnW7/7bff3B6JH+hefvllbdiwwant1YEDB/TSSy81uG5oaKgWLFigyMhIP1bonri4OP3www966KGH9MILL7gMtv7888+akY+e6Nevn9PITlfefPNNBQcHO7V9KSsr08KFC7Vw4cJGt9GuXTstXLhQp556qsd1BpIXXnhBv//+u1avXi3p0AS1dQPX+gwaNEhff/21UwDbkEceeUSGYThd4VNVVaXly5dr+fLljW4jNDRU7733ni6//HK392u24OBgLVmyRHfddZfLNmM7duzQjh07TKjMv4YOHaobbrhB7733ntNtWVlZLlu4NGc7q7y8PKde7p6KiorSP/7xDz3yyCNOo+g9MW7cOK1evVqXXnqp05VCJSUl+vrrr50+APKlsrIyrV+/XuvXr/dovSuuuKLRK/aawmaz6f3331dwcLDef/99p9urqqrqvfIrNjZWS5YsUZ8+fZxuC7SrQAEA8BQtdADAz6699lpdffXVPjvJ7dOnjxYvXqy5c+e6PSLppptu0quvvqqwsDCP9vX3v/9dy5Yta3Cyw5bOCs/NP//5Ty1fvlzHH3+819vo2LGjW/eLjo7WZ599ptdee01du3b1en+S1KNHj4CdTDQmJkbfffedRowY4dF6Xbp00XfffadzzjnHT5V5LjQ0VM8995w2b96sSZMmObVt8lTfvn01Z84cbdu2Teeee26j9w8ODtabb76p//znP2rfvr3H+xsxYoTWrFmjcePGeVNuQAkPD9f//vc/p3ZYjTn//PO1YsWKmitrPPHoo4/qo48+UpcuXTxed+DAgfr222919dVXe7yu2UJDQ/XKK69owYIFTe7b37FjxxYzX8hrr72mO+64w6mNWEsWHBys0aNH6+WXX9b+/fv14osvNim8P2zYsGHatGmTbrjhBo8+GKvLZrNpzJgxTvOvHBYZGano6Givty8d+gB/5syZ+uijj5pUqzuCgoI0d+5czZ8/X4mJiW6tM3bsWG3YsEGjRo1y+SFNa73yCgCAwwLnyAwALOrUU0/VvHnzlJmZqRUrVujRRx/VGWec4TTRV0M6deqkq6++Wl999ZV27Nih888/3+M6brnlFm3atElXXHFFgydvYWFhOu+88/Tjjz/qnXfeCejw/jArPDfjx4/Xb7/9po8//lhnnXWWW6O3+/btq9tvv12bNm1y2Xe2IVOnTtXevXv11ltvafz48W7tLygoSIMHD9a9996rlStXKjk5WXfffbdH+21Junbtqh9//FGzZ892OSLwSD179tSMGTO0fft2jR49upkq9Mzxxx+vJUuWaMeOHXrsscc0ePBgtz4EPDy55a233qp169Zp586duvXWWxUS4tmFnLfffrv27Nmjhx9+2GmC1LoiIiJ05pln6vPPP9f69eub9OFWoImKitIXX3yhBQsWaOjQofXez2azadSoUfr888+1ePFixcfHe73PK6+8Urt379Zzzz2nk046qcEPgUJCQjRq1Ci9/fbb2rp1q8vJlFuSSy+9VH/99Zc++eQTTZo0ye0g8ZhjjtH06dP11VdfKTU11eP3aLOEh4frpZdeUnJysp577jldfPHF6t+/vzp06ODxB93NJTg4WJGRkerQoYP69++v0aNHa/LkyZo5c6a+/vpr5efna9WqVZo+fbpXHyI2JD4+Xu+++652796te+65R8cee6xbH5JGR0fr3HPP1YsvvqikpCStXLlSw4cPd3nffv36KTs7W998843uuusuDR8+3O2fxTHHHKMnnnhCu3bt0v3339/kD3A9cdVVV2nPnj365JNPdMUVV2jgwIFq3769QkJCFBcXpyFDhuiOO+7Q2rVr9f3336tv376S5HIuAF984AIAQEtmM7xpaAoAaDLDMJSamqpdu3Zp3759KiwsVFFRkWw2m9q1a6fo6Gh17txZxx9/vNsjmNxVUlKidevWae/evcrNzZXNZlNcXJz69u2roUOHevThQqCxynNTUVGhX375RQcOHFBWVpYKCgrUpk0bxcTEqE+fPho4cKBPXxeVlZX69ddfdeDAAWVnZysvL08hISGKjo5WfHy8+vXrp379+qlNmzY+22dLs3PnTm3YsEFZWVkqKSlRVFSUunXrpkGDBjn1620pSkpK9PvvvyspKUkZGRk1Ewy2b99e7du3V5cuXTR48OAmj/50JSUlRVu2bFFWVpays7MVFhamjh07qkuXLhoxYoQlWhCZafLkybVaUPTs2dNly7UDBw7ol19+UXJyskpKShQXF6fOnTtr2LBhTb7Kpj4ZGRn65ZdflJmZqaysLAUHByshIUGJiYkaMWJEQI+Wtdvt+u2335SUlKScnBzl5OTIZrMpOjq65m/FgAEDWvXf0dYuKytLv/76q7KyspSTk6Pi4mJFRUUpOjpa3bp104ABA9SzZ88mhekVFRXavXu39uzZo7S0NBUVFamiokKRkZGKiYlRr169dMIJJzTpgzuzvPXWW7rppptqLduzZ0+jH6QDABDICPABAAAAi3E3wAeAQHLhhRdq8eLFNd/Hx8e7nH8BAIDWhBY6AAAAAADAVNu3b9eSJUtqLatvbgAAAFoTAnwAAAAAAGCaiooKXX311XI4HLWW122nAwBAa0SADwAAAAAAmmz27NnasGGDR+ukpaVp3Lhx2rRpU63lffv21cSJE31ZHgAALRIBPgAAAAAAaLKlS5dq+PDhGjx4sJ544gmtW7dORUVFTvcrLy/X6tWrdfvtt6tfv35au3ZtrduDgoL09ttvN2myXwAAAkWI2QUAAAAAAIDAsWXLFm3Z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PUnavn27PvnkE23atEnZ2dkKCgpSt27ddNppp+lvf/uby/Y9b7zxht566y2ddNJJevPNN7VixQotXLhQO3fuVH5+vqZMmdJgvc3FZrPRTxQAgCay2+2qrq42uwwgIHXs2JFWjwAsgwAfAOoRKAH+kTIyMtS7d29T9p2WlqaHHnpIOTk5Cg8PV0hIiHJycrR8+XKtXLlSzz//vEaOHOlU74033qj09HRJUlBQkNq2bav8/Hzl5ORo9+7dSk5Odgrw7Xa7XnzxRX388cc1y9q0aSO73a6kpCQlJSVp5cqV+uGHH1zWumLFCj300EOqrq5WVFSUQkI8/3NZWFio6667TikpKQoNDVVYWJgKCgq0atUqrV69Wg899JAuuOACl+u+8cYbevvtt2taOEVERKi6ulq7du3Srl279OWXX+qll17SgAED6t3/iy++qA8//FA2m03R0dGWGu3eoUMHhYaGml0GAAAtWiAeqwJWEBISQqtHAJZCgA8A9QiUk6KBAwfKZrPJMAy99NJLmjVrlnr27Nnsdbzwwgtq27atZs+ereHDh8tms+mPP/7QU089pd27d+uBBx7Qp59+WqvX5Jtvvqn09HR16dJFDz/8sE4++WQFBwfLbrcrMzNTa9eu1cGDB532NWfOnJrw/vzzz9f1119f85hzc3O1devWBvv9P/HEExo+fLj++c9/1lwZsG/fPo8e71tvvaWgoCA9++yzGjNmjEJCQpSUlKSZM2dq06ZNeuaZZ9S/f3+nEH7+/Pl66623FBUVpRtuuEGTJk1SfHy87Ha7du7cqZdfflm//PKL7r77bi1YsMDlxFrbt2/Xpk2bdN111+naa69V+/btVVlZqZycHI8egz/YbDZ17tzZ7DIAAGjxAuVYFbCaLl26WGrwCwDwjgQA9QiUk6IuXbrowgsvlCTt3r1bl156qa6++mrNmjVLixcv1u7du5tlst6Kigq98sorGjFiRM3lqMcdd5xeffVVxcTEqKSkRHPnzq21ztatWyVJt956q4YNG6bg4GBJhyZA7dy5sy699FJNnz691jopKSmaN2+eJOm6667To48+WusDi7i4OI0ZM0YzZ86st9bevXvrhRdeqNXWp0ePHh493uLiYj377LMaP358zQj+3r176+WXX1aPHj1kt9v12muv1VonPz9fr776qmw2m55//nlNnjxZ8fHxNY/5mGOO0SuvvKJjjjlGGRkZ+uKLL1zuu7S0VFdffbVuv/12tW/fXpIUFhZmieC8U6dOCgsLM7sMAABavEA5VgWsJCIioub4GwCsggAfAOoRSJOC3XfffZoyZYratGkjwzC0Y8cOLViwQDNmzNCVV16piRMn6oUXXvDrCO3x48e7bN8TFxeniy++WJL0zTff1LotOjpa0qHe+e5aunSpHA6HYmJivO73fu2119Z8WOCtE044wWVv/oiICF177bWSpPXr16u4uLjmtv/9738qLy/XMcccU29f/5CQEE2cOFGS9NNPP7m8T1BQkK6//vom1e8PISEhSkxMNLsMAAACQiAdqwJW0bVrV3rfA7AcWugAgAuBNilYSEiIpk6dqmuuuUY//vijNm3apD///FNJSUmqqqpSbm6u5s+fr2XLlumll15qdIJVb9Q3Aa4kDR06VO+9954KCgqUmpqqrl27SpJGjRqlrVu3avbs2UpJSdHYsWM1aNAgtW3btt5tHR61P3z4cIWHh3tV64knnujVekcaOnRoo7c5HA5t37695rnZsmWLJGnPnj01Ib0r5eXlkuSyfZAkdevWTXFxcd6U7VedO3du8gcjAADgEEbgA77Vtm1bxcbGml0GADghwAcAFwL1hKht27Y655xzdM4550g69Di3bNmijz/+WKtXr1Z+fr7uu+8+LVy40Ovwuz4dO3Z067a8vLyaAP+6667Trl279O2332rRokVatGiRbDab+vTpo5EjR+rCCy906ud/+CqCprSLOdx2pikSEhLcui03N7fm66ysLEmHfi7uvAYPB/l1WTG8Dw8Pb/A5AQAAngnU41XALIfPQQDAagjwAcCF1nJCFB4eruHDh2v48OF6/PHHtXTpUmVkZGj9+vUaM2aMT/flzaWoISEhmjlzpm644QatXLlSW7Zs0R9//KE9e/Zoz549mj9/vqZPn65rrrnGJ/s7zBejxBvaf323ORwOSdIll1yiBx54wOt9W3HSrS5dunA5MgAAPmIYBi10AB+KjY1t8CpfADCT9c7wAcACWuMJ0UUXXVTzdXJycs3Xh8Pshp6TI/u41ycjI6Pe2zIzM2u+djX6vV+/frr55pv12muvaeXKlXr11Vd10kknyW636z//+Y927txZc9/Dk06lpaU1WpM/HfmYGrrtyNHyHTp0kHRosuFAEhkZ6ZOrGgAAwCFVVVUyDMPsMtCCLFmyREOGDNF5551ndimWY7PZGH0PwNIYgQ8ALgRS/3t3RUZG1nwdFhZW83V0dLTy8/PrDeBLSkpqBf712bhxoy644IJ6b5OkmJiYRg+eQ0JCNGzYMB133HEaP368KisrtWHDBvXr10+SNGjQIG3cuFE///yzKioqfN4KyF2HH1NDtwUFBal///41y0844QStWrVKf/zxhw4ePNikNkBW0q1bN0bfAwDgQ63xWLW5GIahFStWaPny5dq+fbvy8vIUFBSkuLg4xcfH69hjj9XgwYM1dOhQRmwHiPj4eEVERJhdBgDUixH4AOCC3W43uwSfSU1NVUpKSqP3W7p0ac3XAwYMqPn6cDD+/fffu1xv3rx5bl2xsGLFCpdBf35+vhYuXChJOvPMM2vd1tB2w8LCalrFHNky5rzzzlNwcLAKCgr0xhtvNFqXv2zZssVliF9RUaF58+ZJkkaMGKHo6Oia28455xyFh4fLbrdr1qxZDb4OHQ6HioqKfF+4j8XExNR6jAAAoOkC6VjVSoqKinTzzTfr/vvv1w8//KD09HRVV1crLCxM6enp+u233zR//nzde++9WrlypdnlwgeCgoICZtAMgMBFgA8ALgTSqKa9e/fqsssu0x133KGlS5fWai1TXV2t7du364knntCHH34oSTr22GN14okn1txnwoQJkqT169frjTfeqGmXk5+frzlz5uidd95xK6ANCwvT7bffrp9//rnmku9t27bp1ltvVX5+vqKiojR58uRa65x33nmaPXu2fv/991ph/v79+/Xwww+rvLxcQUFBOuWUU2pu6969u6699lpJ0gcffKAZM2Zo3759Nbfn5eXpm2++0T333OPO0+e1tm3b6r777tN3331X83pKTk7WnXfeqeTkZAUHB2vq1Km11omPj9f06dMlSWvWrNG0adO0ZcuWmpN0wzCUnJysefPm6YorrtDq1av9+hiaymazqXv37maXAQBAwAmkY1UrefTRR7Vp0yYFBwfrmmuu0cKFC7V+/XqtWLFCa9eu1UcffaTp06fXDHBBy9e1a1eFhoaaXQYANIgWOgDgQiCNagoJCZHD4dDatWu1du1aSVJoaKgiIyNVWFhYq3/qgAED9O9//9tpRPvy5cu1ceNGvfXWW3r77bcVHR1dM/r79ttv1+rVq7Vp06YG6/jnP/+pV199VdOmTVNERISCgoJUWloq6VC4//TTTysxMbHWOjk5OZo7d67mzp2roKAgtW3bVhUVFTWTDNtsNt15553q3bt3rfVuueUWlZSUaMGCBVq8eLEWL16syMhIORwOlZeXS5LfL3n+xz/+oYULF+r+++9XWFiYwsLCaj78sNlsuv/++zVw4ECn9a688kpVVlZqzpw52rhxo6ZMmVLz8yopKal1wm71tjRdunQxrYURAACBLJCOVa1i3759NYMjbrnlFqeBJSEhIerbt6/69u2r66+/vuaYEi1XVFSUEhISzC4DABpFgA8ALgTSSdEpp5yiRYsWae3atdqyZYv27NmjzMxMFRUVKSIiQgkJCerfv7/Gjh2r8ePH1wrvpUOT2L700kv673//q6+//lppaWmy2WwaMWKErr32Wg0bNsytkeBdu3bVhx9+qHfeeUdr1qxRdna24uLiNHToUE2ZMsUphJek2bNn69dff9WWLVuUnp6u3NxcSYdG2Z944om6/PLLdcwxxzitFxwcrPvuu08TJ07U559/rs2bNys3N1fh4eHq0qWLjj/+eE2cONHLZ9Q97dq10/vvv6+5c+fq+++/V0ZGhmJiYjRo0CDdcMMNGjRoUL3rXnfddRo7dqwWLFigX375RWlpaSouLlZUVJS6deumIUOGaMyYMTr++OP9+hiaIjIyUp06dTK7DAAAAlIgHataxc6dO2u+Pv300xu9f3090w8cOKD58+drw4YNysjIkMPhUOfOnXXKKafo6quvdhqwIh2aYPaJJ55Q586dtWTJEv311196//33tXnzZhUWFqpjx446/fTTNWXKFLVr167emn7//XfNnTtXW7ZsUXl5uTp16qRx48bphhtucOMZkIqLi/XJJ5/oxx9/1L59+1ReXq64uDidcMIJuuqqq1wee6alpen888+XJH355ZdyOBx6//339fPPPysrK0vx8fFasmSJW/tvTjabTT179rT8gBgAkCSbwdT1AODkjz/+qBnlDcAzNptNAwYMqDUxMgAA8J3U1FSlp6ebXUZA+e6773T//fdLOjSIZMSIER5vY9GiRZo1a1bNFZNhYWGy2Ww15xVRUVGaNWuW07aPDPCnTZumxx9/XNXV1Wrbtq1KS0vlcDgkSX369NHcuXNdHmMtXrxYTz/9dM19D1+5WlVVpV69eumiiy7Siy++WPMhQV1//PGH7r77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A0ADOZ1OsyMAYSEhIUEWi8XsGEFtxowZ6tL1LH311VdmR2mSQK67MTExuvrqq+XxeLRixYo6P2/ZsmWSpBtuuMGoaECDhXvd3bhxozp07KS5c+cGdSO/qKgoqPMDwcJisSghIcHsGADCAKcTAUADHT9+3OwIQFhgR1PTrVu3Tnv37Fb/AQP00oIFuu2228yO1CiBXneHDRumFStWaMWKFfrNb35zSgP0m2++0YEDB5SWlqZevXqd8et99dVXevvtt7VlyxYdO3ZMUVFR6tChg66++mrdeuutiomJqfV1X3zxhd5++21t375dBQUFstlsSkxMVNu2bXXppZdq+PDhcjgcJ73m22+/1cKFC7V161bl5+fLarUqMTFRqampuuSSSzRs2DClpKTUfL7H49HWrVu1du1aff3118rNzVVBQYHi4uLUpUsXDRo0SDfeeONpD0EtLCzUv//9b61Zs0Z5eXlKSEjQhRdeqHHjxqlbt27q3bu3JOm5556r+fXPffrpp1q+fLm2b9+uwsJCxcTEqGvXrrruuuvOuD7qFu5194cfftDBA/v1hz/8QVu3btM///msbDab2bEaLNBrJhAq4uPjZbWyLxaA/3FlCwANUFpaKrfbbXYMICyEeyPJV87qc63ik1I0atQobdv2rR5/fGpQ3WwGQ93t1auX2rZtq0OHDmnz5s2nNOmrd98PGzbstF/H7XbriSee0JIlS2p+LzY2Vi6XSzt27NCOHTu0bNkyPfPMM2rTps1Jr33hhRf0/PPP1/yz3W6X1+tVVlaWsrKytGHDBnXv3v2khviKFSs0derUmp260dHRioiIUHZ2trKzs7Vp0yalpKSclDs7O1t33XVXzT9HRETIbrfL6XRq06ZN2rRpk95//309/fTTstvtp3yP+/fv17333qu8vLyaNV0ulz766COtWbNGM2bMOO2fUWlpqR555BGtXbu25vfi4uJUXFyszZs3a/PmzXrvvfc0d+5cakgj8Gf2k5sf+ZdemXm/vv/hB73z9mIlJyebHalBaOADxqBmAjAKDXwAaABuiABj2Gy2oNz1GIgio+269fGX1brreZo+/SFt375dr7zysuLj482OVi/BUHctFotuuOEGPffcc1q2bNlJDfyysjJ9+OGHslqtuuGGG3To0KE6v05GRoaWLFmiFi1a6O6779a1114rh8Mht9utb775Rk899ZS+//57Pfjgg3r55Zdr3og5cuSIXnjhBUnS6NGjNWbMGLVq1UqSVFxcrF27dun9998/aba5y+XSrFmz5PV6NWTIEN1zzz1q27ZtTeZ9+/Zp9erVat68+UkZIyIidPnll2vw4MG68MIL1aJFC1mtVpWWluqjjz7Ss88+q82bN+vZZ5/VH//4x5Ne63a7NWXKFOXl5SkxMVGPPPKIBg4cqIiICO3bt08zZszQX//619P+WT/66KNau3at2rVrp3vuuUcDBgxQXFycysvLtX79ej355JPaunWrHn/8cc2ePfsMf3M4EXX3v3pdf4dadz1PmZNv1EW9L9aK5ct0wQUXmB2rXsrLy1VeXm52DCAs/PypNgDwl+DZfgUAASAYGklAKGBHk29ZLBZdfuefdMeTy7Rq9Qfq2+8y7du3z+xY9RIsdfeGG26Q1WrVRx99pNLS0prf/+CDD1RaWqqLL75YrVu3rvP1u3bt0sKFC2W32/WPf/xDI0aMqGkMREZGqnfv3vrXv/6llJQU7dy5U2vWrKl57bfffiuPx6P27dvrD3/4Q03zXvrp8f4LL7xQU6ZMUffu3U9ar6SkRDExMXrsscdqmvfST3P9u3fvrgkTJqh///4n5UxJSdGcOXN07bXXqlWrVjVvIsTGxmrYsGGaM2eOJOmdd945pYm4evVq7dq1SxaLRbNmzdKVV16piIgISVLHjh01d+5cJSUl1fln9Nlnn+nTTz9VixYt9Pzzz2vw4ME1b0rYbDZdfvnl+te//qWYmBh9+umn+v777+v8WjgVdfdk7c+7VPe99KUU31J9+/XTO++8Y3akegmWmgkEu6ioqDpH2gGAr9HAB4B68ng8Ki4uNjsGEBbY0eQf3QfcoN/NX6/sY8XqffHFJ40hCUTBVHdbt26tSy65RGVlZfrggw9qfn/58uWSpOHDh5/29UuXLpXX61X//v3VtWvXWj8nLi5Ol19+uaSf5t1Xqz5Ar7S0VGVlZfXKW/2ayspKnx4S3KNHDyUlJamsrEw//PDDSR/78MMPJUk9e/ZUz549T3mtzWbT7bffXufXrh4tNHTo0DpHmqSkpNSMCTrxzwhnRt09VWLrdvrtC2t11mU36Oabb9a0adMC/nBYGviAMXjTE4CRGKEDAPVUVFQU8DdtQCiwWCxBM94lGKV0SdfvFmzQaw+N1NVXX61//vOf+s1vfmN2rFoFW90dNmyY1q9fr2XLlukXv/iFDh48qM2bNyshIUFXXHHFaV+7ZcsWSdLnn3+u6667rs7Pq97df+TIkZrfS09PV2JiovLz83XnnXfqlltuUZ8+fdShQ4dTDtSt1rZtW3Xs2FH79u3T2LFjdcstt6hv377q2rVrza74ulRWVmrp0qX65JNPtHv3bh0/flwVFRWnfF5OTo7OO++8mn+u3hF/0UUX1fm1T/exb775RtJPu/vfe++9Oj+v+k2f7Ozs034f+C/qbt2i7bH65fSFSulyrh599FFt+/ZbLZg/X7GxsWZHO4XX61VRUZHZMYCwQAMfgJFo4ANAPbGjCTBGYmLiGRuIaJq4xJb69TOrtWzWeN11113aunWb5syZrcjIwLo0DLa6e+WVV6pZs2basmWL9u/frxUrVkiSBg8efMbZ4tWHupaWlp40gqcuLper5tcJCQn629/+pr/85S/as2ePZs2aJemn8Tk9e/bUtddeq0GDBp309xsREaHp06frwQcfVFZWlp555hk988wzstvtOv/883XVVVfphhtuOOUg2oKCAt13333atWtXze/ZbLaT/r09duyYPB7PSRmrf1+SWrZsWef3VdfOerfbrcLCQkk/Nejr82TGz9dH3ai7p2exWHT1Xf9PyZ16aNFf79Bl/Qdo+bKlJ42eCgQlJSWqqqoyOwYQ8qxWqxITE82OASCMBNZdGgAEsGBrJAHB6nQzsOE7EZFRuunhf6p11/P0zOzx2rFjh958841TDi01U7DV3ejoaF133XV66623tHTpUr3//vuSftqZfyYej0eSdP/992vs2LENXrtPnz5atmyZPv74Y3355ZfaunWrDhw4oLVr12rt2rVasGCBnnnmmZMa5GeffbYWLVqktWvX6osvvtDWrVu1Z88ebdy4URs3btT8+fOVkZFx0kifJ598Urt27ZLD4dCECRPUr1+/Uxry119/vXJycup8eqKupwJO58Sm5PTp0zVo0KAGfw3UjbpbP+ddfYtatO2izMm/0EW9L9bSJe/o0ksvNTtWjWCrmUCwSkxMrDkDBgCMQMUBgHqoqKhgJx9ggIiICOYwG6zvyPv062dW64uNX+mSPpcGzMGfwVp3q5v1r7/+unJyctSlSxf16NHjjK9r0aKFJGn37t2NXjsmJkbXX3+9/vrXv+rtt9/We++9pwce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SXN1819e0L//829de+2g057t4AvUTMD/YmNjad4DMA0NfAA4Ax5JBvzLYrGoRYsWZsfAaUTbY/XL6Qt17b2P69FHH9Vto0b5tTZSd0PXV199pTlz5mjHjh015xx4vV599913+sMf/qCNGzfKYrHojjvuMDkp0DSX3HiXfvPsR/p667fqffEl2r59u9/WomYC/teyZUuzIwAIY2xrAYDTqKqqCsuDFAEjNW/e3O/jBdB0FotFV9/1/5TSOV1vPXa7Lus/QMuXLVXbtm19ug51N7QVFxfr9ddf1+uvvy7pp0NUy8vLVV5eLumnn7MJEybooosuMjMm4BOdeg7QfS99qVcmDdelffvq9dde0w033ODzddiBD/hXREQEm00AmIod+ABwGuxoAvwvJSXF7AhogHOvuln3vLhOB7LzdVHvi7V+/Xqffn3qbmg799xzde+99+qiiy5S69ataxr3aWlpuuGGG/TSSy9pzBjOv0DoSErtqHv//bk69LpKw4cP14wZM3w+hoy6CfhXq1atZLXSPgNgHnbgA8BpcEME+FdCQoJiY2PNjoEGSj3nQt330pd69U836/IrrtCLL7yg22+/3Sdfm7ob2lq2bKm77rpLd911l9lRAMPYYuM1etbb+uC5R/XQQw9p27ff6sUXXvDJPO2Kigq53W4fpARQG4vFouTkZLNjAAhzvIUIAKfBI8lN17t3b/Xu3VtfffWV2VF85re//a169+6t559//pSPDRs2TL1799by5ctNSBZ82H0fvOKTkvWbZz/S+deN1h133KEHH/yTqqqqmvx1qbsAQpHVatV19/2Pfvm31/XmW4s0YODlOnLkSJO/LjUT9bV8+XL17t1bw4YNMztKUElKSmLUIwDTsQMfAE6jrKzM7Ag+4/V69dFHH2nVqlXauXOnjh07JqvVqqSkJLVs2VLp6enq2bOnLr74YsXHx5sdF2HAZrOpWbNmZsdAE0RG23TL/3tRrbuepyefnKQdO3bo9ddfa9LfayjVXQD4uQuuG6UW7boqc/IvdFHvi7Vs6RL17t270V+Pmul7XDPjROy+BxAI2IEPAHXwer01s3mDXVFRke655x499NBD+vTTT5WdnS23263o6GhlZ2dry5Yteu211/Tggw/qk08+MTtuwGvdurU6dOigxMREs6MEtZSUFFksFrNjoIksFov6/2qixma8p0/XfqY+l/bV7t27G/W1QqnuAkBd2vborfte+lJRSWnqP2CA3njjjUZ/LQ799i2umXEiRj0CCBTswAeAOlRWVvr8kDGzPProo9q0aZMiIiL0y1/+UjfffLPatm0rq9Uqt9utvXv36vPPP9f7779vdtSg8Pjjj5sdIehFRESoRYsWZseAD53d9zr9bv4GvfzHYbr4kku06K23dNVVVzXoa4RS3QWA02nWKlV3P/ep3v7b3Ro1apS2bt2madMeb/BBmbzp6VtcM+NEjHoEECho4ANAHULlhujAgQNau3atJOl3v/udxo4de9LHIyMjddZZZ+mss87SnXfeyU4uGKJVq1YNblIg8LXqeI5+t2CDFj58mwYNGqR58+bpvvvuq/frQ6XuAkB9RNljdOvjryily3n6+98f1vbt25WZ+UqDxrJUVFT4MWF44ZoZJ7Lb7Yx6BBAwaOADQB1CpZH0ww8/1Pz68ssvP+Pn2+32k/65ei7rc889V+eM1t/+9rfatGmT7r77bt1zzz11fu38/Hz9+9//1rp165Sfn6+EhARdfPHFuuuuu9SxY8daX3P8+HG99tprWrt2rQ4dOqTy8nI5HA41b95c559/vq655hpdcskltb7222+/1eLFi7V582bl5+crIiJCycnJOvfcc3Xdddfp0ksvrfncr776Svfee2/Nr3fu3KnMzExt2rRJR48e1QUXXKB//etfDfp+S0pKNH/+fH388cfKycmR3W7XhRdeqHHjxuncc8+t83WStHPnTr3xxhvatGmT8vPzZbVa1bZtWw0YMEC/+tWvah3f8/zzz+uFF15Qr1699K9//UsfffSR3n77bf3www8qLCzUXXfdddq8RrFYLMwTDWGxzZrrzoz39F7GZP3+97/Xtm3fat68jHodABcqdRcA6stiseiKsVOU3LmH3vzLr9S332VavmxpnddFJ6qqqpLb7fZ/yDDR1GvmaocOHdJrr72mjRs3KicnRx6PR23atFHfvn01evRotW7d+pTXLF++XFOnTlWbNm20fPlyfffdd3rppZe0efNmHT9+XMnJybr88st11113nbapvG3bNi1YsEDffPONXC6XUlJSdPXVV2vcuHH1+BOQiouL9cYbb2jNmjU6cOCAXC6XkpKSdMEFF+iXv/ylzjvvvFNec/jwYQ0fPlyStGzZMnk8Hr300kvasGGD8vLy1LJlSy1fvrxe6weS5ORkRj0CCBg08AGgDqHYSMrJyVGnTp1MWfvw4cN65JFHdPToUdlsNkVGRuro0aNatWqVPvnkE82aNUv9+vU7Je9vfvMbZWdnS5KsVqvi4+NVWFioo0ePateuXdq3b98pDfyqqio99dRTWrhwYc3vxcTEqKqqSnv37tXevXv1ySef6NNPP60160cffaRHHnlEbrdbcXFxioxs+H8ujx8/rjvuuEP79+9XVFSUoqOj5XQ69b//+79au3atHnnkEf3iF7+o9bXPP/+8XnzxxZpRIna7XW63Wz/++KN+/PFHLVu2THPnzlW3bt3qXP+pp57Sq6++KovFooSEhIDa7d6iRYt6NXMRvCIiIzVs0lyldDlXL864T4VOp15/7dUzvi4U6y4A1EePgcP0u/nr9cqk4bro4ou164cf1Lx589O+hprpP429Zn7nnXc0Y8aMmjdWoqOjZbFYtG/fPu3bt0/Lli3TjBkzTtpE8nOrVq3SX//6V7ndbsXHx6uqqkpZWVl67bXXtH79ei1YsKDWuexLly7V3/72N3k8HklSfHy8jhw5ovnz5+uTTz7RTTfddNrs3377rSZNmqSjR49K+mncod1uV05OjlavXq0PPvhA991332nfDNi6daumT5+u0tJS2e32Rl1DB4LIyEhGPQIIKMFZTQHAAKFyU9SjRw9ZLBZ5vV7NnTtXM2bMUIcOHQzP8eSTTyo+Pl7PPPOM+vTpI4vFom+//Vb/8z//o127dunhhx/Wm2++edKsyX/961/Kzs5Wamqq/vKXv+iiiy5SRESEqqqqlJubq3Xr1unIkSOnrPWPf/yjpnk/fPhw3XnnnTXfc0FBgbZu3Xra2aVTp05Vnz599Ic//KFmB9yBAwca9P2+8MILslqteuKJJ3TFFVcoMjJSe/fu1d///ndt2rRJ06dP1znnnHNKE/61117TCy+8oLi4OI0bN0433HCDWrZsqaqqKv3www+aN2+evvzyS02aNElvvfVWrTdwO3fu1KZNm3THHXfo9ttvV/PmzVVRUVFzQ2Ymi8WiNm3amB0DASpU6i4ANIbX65XX65VF9dv1S830raZeM3/66af629/+psjISI0dO1a33HJLzW77/fv367nnntOHH36oKVOm6I033qh1J/6xY8f0+OOP64YbbtBdd92l1q1by+VyadmyZXryySe1Z88evfzyyzVPjVbbuXOnpk+fLo/Ho4suukgPP/ywOnbsKLfbrY8++khPPPGEXnzxxTqzHz58WA888ICKiopqdux37dpVkZGRKigo0Jtvvqn58+frH//4hzp16qQrrrii1q8zffp0de7cWX/605/Uo0ePmu892KSmpgbU5hcAoCIBQB1C5aYoNTVVN954oyRp165dGjFihEaPHq0ZM2Zo6dKl2rVrlyGHRpaXl+vpp5/WpZdeWvM46rnnnqtnn31WDodDJSUlWrBgwUmv2bp1qyTpvvvu0yWXXKKIiAhJP+0IatOmjUaMGKEHHnjgpNfs379fmZmZkqQ77rhDjz766Ek3X0lJSbriiiv097//vc6snTp10pNPPnnS4+vt27dv0PdbXFysJ554Qtdcc03N7qNOnTpp3rx5at++vaqqqvTPf/7zpNcUFhbq2WeflcVi0axZszR27Fi1bNmy5nvu3r27nn76aXXv3l05OTlasmRJrWuXlpZq9OjRGj9+fM3uvejo6IBonKekpCg6OtrsGPCzKrdby2ZP0Nv/c7fuvutuvfzSgnq9LlTqLgA01I7/Xabnft1XbZIS9PVXX55x971EzfS1plwzV1ZWaubMmZKkhx9+WPfff7/atGkji8Uii8Wijh076oknntDAgQNVUlKiV1+t/ak0l8ulQYMG6S9/+UtNg99ut+vWW2/VbbfdJkm1bkJ59tlnVVVVpfbt2ysjI6PmGjYyMlLXXXedpk+frqKiojq/94yMDBUVFWno0KGaMWOGunXrVnP9mpSUpHvvvVfjx4+XpJqRkrVxOBx69tlna5r3kkzZONQUdru95vobAAIFDXwAqEMoHQo2ZcoU3XXXXYqJiZHX69X333+vt956S9OmTdOoUaN03XXX6cknn/TrDu1rrrmm1keRk5KSdPPNN0uSVq9efdLHEhISJP00O7++VqxYIY/HI4fD0eh577fffnvNmwWNdcEFF9Q6m99ut+v222+XJH3xxRcqLi6u+djKlSvlcrnUvXv3Ouf6V9+ISdL69etr/Ryr1ao777yzSfn9ITIystbdZggtpceP6aUJQ7XhrX/o2Wef1bPP/qPeI5NCqe4CQH14vV59uuAJvTL5Rl137TX64vN19W54UjN9r7HXzOvWrVNubq5atGhRMw++Ntdff72kn64B6/Kb3/ym1t+vnst/8ODBkw7QLSoqqrkmvOOOO2qdzd+3b1+df/75tX5dp9OpTz75RJJOObi3tuw//PBDnfcMt956a61PhwaTtLQ0Zt8DCDiM0AGAWoTaoWCRkZG69957NWbMGK1Zs0abNm3Sjh07tHfvXlVWVqqgoECvvfaa3nvvPc2dO/eMB6w2Rl0H4ErSxRdfrPnz58vpdCorK0tpaWmSpP79+2vr1q165plntH//fl155ZU6//zzFR8fX+fXqt6136dPH9lstkZlvfDCCxv1uhNdfPHFZ/yYx+PRzp07a/5svvnmG0nS7t27a5r0tam+aattfJAktW3bVklJSY2J7Vdt2rRp8hsjCGy5+3bqlT8Ol7v4qD744ANdeeWV9X5tqNVdADiTSleZ3v7b3dq88lU98shf9PjjUxs0toMd+L7X2GvmLVu2SPrpDKTBgwfX+fUrKysl1X0N53A41K5du1o/1qpVq5pfHz9+vKZRv3Pnzpq596e73u7du3fNdfKJtm3bVvP63/3ud3W+/kRHjhypdUb8BRdcUK/XB6r4+HglJiaaHQMATkEDHwBqEao3RPHx8Ro6dKiGDh0q6afv85tvvtHChQu1du1aFRYWasqUKXr77bcb3fyuS3Jycr0+duzYsZoG/h133KEff/xRH3zwgd555x298847slgs6ty5s/r166cbb7zxlF1q1TuCmjIupj6PrZ/JiTdZp/tYQUFBza/z8vIk/fT3Up+fwRN3X50oEJv3NpvttH8mCH7ff75KbzwySu3bpmnFxxvVpUuXBr0+VOsuANTmeN5hZU6+UTm7t2nhwoU141EagrrpPw29Zq6+hqusrKzXE611/d2dbvf6iZsgTnzD+8Rryfpeb5+oOrukej+NG0zXoA1RfQ8CAIGGBj4A1CJcbohsNpv69OmjPn366K9//atWrFihnJwcffHFF3UeTtVYjXkUNTIyUn//+981btw4ffLJJ/rmm2/07bffavfu3dq9e7dee+01PfDAAxozZoxP1qvmi13ip1u/ro9V73665ZZb9PDDDzd67UA8dCs1NZXHkUOU1+vVZ6/N1cqMyRoyZKhee+1VNWvWrMFfJ1zqLgAc2vGVMif/QjGRFq377DNddNFFDf4aXq+XEToGOtM1c1VVlSSpX79+mjdvnslpG6Y6u81m07p1/GmHpgAArQVJREFU65r0tQLxGrS+EhMTT/uULwCYKXirKwD4UTjeEN100001v963b1/Nr6ub2af7MzlxjntdcnJy6vxYbm5uza9r2/1+9tln65577tE///lPffLJJ3r22WfVq1cvVVVVKSMjQz/88EPN51YfOnX48OEzZvKnE7+n033sxJ1K1Y8i79q1y3/BTBAbG+uTpxoQeNwV5Vr0+G/07lN/1KRJk7V06ZJGNe+l8Ky7AMLPN6te1/N3D9BZHdvp66++bFTzXvppp3ddB6rCv2q7Zq6+/jTjGu7Ea8nTXX+euNP+RNXZy8vLdfDgQd+GCxIWi4Xd9wACGg18AKhFOM5hPvGR3ejo6JpfVx8kW1cDvqSk5KSGf12++uqrM37M4XCc8eI5MjJSl1xyiebOnavo6Gh5vV5t3Lix5uPVB3Rt2LDB1B299fl+rVarzjnnnJrfr54b+u2339Y5GzUYtW3blt33IajoaI5e/N1V2rb6Nb388suaOXNGk55eCce6CyB8eDwevf+PR7TwL7/SbbeO1Jr//bRJ4/6omeap7Zq5+houNze35kwjo3Tr1q1m5/vprj+//PLLWn///PPPr7lOe//9930fMAi0bNmy1sN/ASBQ0MAHgFpUP0oaCrKysrR///4zft6KFStqft2tW7eaX5999tmSpI8//rjW12VmZtZr5+xHH31Ua6O/sLBQb7/9tiTp2muvPeljp/u60dHRNTcrJz6uO2zYMEVERMjpdOr5558/Yy5/+eabb2q9iSovL1dmZqYk6dJLL615g0SShg4dKpvNpqqqKs2YMeO0P4cej0dFRUW+D+5jDofjpO8RoeHw99/o2TsvVmn2Hq353//V7bff3uSvGUp1FwBOVF5SpFcfvFmfLvi7ZsyYoZdfeqnJzUJqpu815Zp5wIABNTvZZ8+eXeeM+GpOp7MJSU+WkJCgSy+9VNJP1+W1bWDZsGFDrQfYSj/t4L/88sslSa+88soZ/wx8mT0QWK3WJr2ZBgBGoIEPALUIpV1Ne/bs0ciRIzVhwgStWLHipNEybrdbO3fu1NSpU/Xqq69KktLT03XhhRfWfM6gQYMkSV988YWef/75mnE5hYWF+sc//qF///vf9WrQRkdHa/z48dqwYUPNI9/bt2/Xfffdp8LCQsXFxWns2LEnvWbYsGF65plntG3btpOa+QcPHtRf/vIXuVwuWa1W9e3bt+Zj7dq1q2kmvvzyy5o2bZoOHDhQ8/Fjx45p9erVmjx5cn3++BotPj5eU6ZM0Ycffljz87Rv3z5NnDhR+/btU0REhO69996TXtOyZUs98MADkqTPPvtMv//97/XNN9/U3KR7vV7t27dPmZmZuu2227R27Vq/fg9NZbFY1K5dO7NjwMe+/fhtPX/XZerQppW+/upL9enTxydfN5TqLgBUKzi8T8/fdZn2b/pYy5Yt05/+9CefPJVGzfS9plwz22w2PfTQQ7JYLNq5c6d+/etf64svvlBlZWXN18jKytLixYt1xx136K233vJp9nvvvVcREREnXWtW5/7ggw/08MMPn/Z6feLEiXI4HCopKdFdd92lpUuXnjQis7CwUB9//LEefPBBPfLIIz7Nbra0tP/P3p2HR1We/x//TNbJRkJ2wg6CiCBYwGoRRGRTCyoFopUCKqiotVYLriiIoILbtyoVEBXFhQAKuPxYZJFFVChFUInsEAaykz2Tbeb3hyUVBSTJzDmzvF/X1asImbk/YLw5557nPE9TBQcHmx0DAM6KQ2wB4DR8aVVTUFCQHA6HNm/eXHswVXBwsMLDw1VUVHTK/qkdOnTQc88996sV7StWrNC2bds0d+5cvf7664qKiqpd/X3vvfdq48aN2r59+1lz/P3vf9esWbN09913y2q1KiAgQGVlZZJ+Gu5PmzZNycnJp7wmLy9Pb731lt566y0FBAQoMjJSFRUVtSuLLBaL7rvvPrVu3fqU140fP16lpaVatGiRli1bpmXLlik8PFwOh6N2RZS7D6kaN26cPvzwQz300EMKCQlRSEhI7Y2QxWLRQw89pI4dO/7qdTfeeKMqKyv16quvatu2bRo7dmztv6/S0tJTbtg9fVualJQUhYaGmh0DLuJ0OrXm9an6fPYTGpGaqjffeOOUbQQaypf6LgBI0oHtG/Teg39SfEwjfbVliy688EKXvTc90/Uaes3cp08fPfnkk5o2bZr27Nmjv/71rwoMDFRkZKTKy8tPWYxycsW7q3Ts2FEPPvignn76aW3dulXDhg1TZGSkKisrVVlZqVatWumGG27Qiy++eNrXN2vWTK+++qomTpyoY8eOaerUqXrqqacUFRWl6urq2mt2Sbrkkktcmt1MERERSkhIMDsGAPwmBvgAcBq+dFN02WWX6aOPPtLmzZu1Y8cO7d+/X9nZ2SouLpbValVCQoLOP/98XXnllerX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6667Xu+887YiIyPNjuXzkpKSFB0drcLCQpWXl5/yayf30P/d736nOXPmaM2aNfrwww+1Z88eFRQUaOzYsadsu5Oenq733ntP27dvV35+vkJDQ9W6dWv169dPw4YNU0hISJ3zHTt2TPfcc4+OHDmi888/X//85z9P2Q4oNzdX77//vr788ksdO3ZMVVVVSkhIUPfu3XXzzTerTZs2v3rPbdu26c4776z9cXp6uhYsWKDt27crLy9PXbp00Zw5c+qc1Z/Rd81xeOcWvTvhBkVHWPXl5s08QQN4kebNmysqKsrsGADgMgzwAQDAObFYLGrTpo2sVqvZUVAH1ZV2pT3+F/3n/72rxx6bpClTJisggIcwjZCdna3CwkJJPx08eiYvvvii3n33XVksFkVFRf3q3897772nF198sXbFbWRkpMrLy7Vz507t3LlTH3/8sV5++WXFx8efc7Y9e/bo3nvvVW5uri655BLNnDlTERERtb++ceNGPfrooyorK5P001YEwcHBstlsstls+uyzz/Too4/qj3/84xlrrFmzRo8++qiqq6sVERFh2iHJ3oy+a45/fzJfH8/8qy655BJ99OESJSYmmh0JwDlKSEjgv1kAPoeraAAA8JtODpEaNWpkdhTU0d6vVyvUatUHH3yg1NRUs+P4hZqaGn3//fe1W+jExsbq2muvPe3Xpqena/v27Ro1apT+8pe/qHHjxqqsrFReXp6knwbpL7zwgiTpiiuu0P3336+mTZuqqqpKq1at0owZM7R3715NnDhRc+fOPadV2tu2bdMDDzyg0tJSDRgwQFOmTFFwcHDtr3/33XeaOHGiqqqqNHToUP35z39W8+bNFRgYqMzMTM2fP1+LFv20H3ibNm3UsWPH09aZMmWKfv/73+vvf/+7WrVqJUk6cuTIOf85+jv6rnk+mn6Hbr31Nv3rX7Pq9XQLAHPEx8erefPmZscAAJdjgA8Av+Ape5sCnuLkECkmJsbsKKijnj176rvvf9CitIXq1q2b2XHOyNv77sCBA2t/7HA4VFRUpJqaGkVEROjqq6/WXXfddcZH+cvKynTzzTfr3nvvrf25kJAQNWnSRJL08ssvS5K6du2qGTNm1A7og4ODde211yoqKkr333+/du7cqXXr1qlfv35nzbp69Wo9/vjjqqqq0k033aT777//V3/+M2bMUFVVlcaOHVu7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dlx4KG//7wCAZ2GADwAejuE9AAAAPJmvDO+Li4v1wAMPaPv27bU/FxgYqMjISGVmZspms+nbb7/Ve++9pyeeeILV7AAAQzDABwAPxvAe+DVvHw4AAOBLfGV4L0mPP/64tm/frsDAQN10000aOnSomjVrpoCAAFVXV+vgwYP68ssvtXLlSrOjwsP5wn8PADwHA3wA8FAM74HT44YIAADP4EvD+yNHjmjjxo2SpPHjx2vMmDGn/HpQUJDatWundu3aafTo0bLb7SakBAD4Iwb4AOChMjIyGN4DAADAIxUWFvrM8F6S9uzZU/vjK6644je/3mq1nvbnjx49qvfee0/ffPONsrKy5HA41KRJE1122WW6+eablZyc/KvXfPzxx5oyZYqaNGmijz/+WLt379b8+fP1n//8R0VFRUpMTNQVV1yhsWPHqlGjRmfMtGvXLr311lvasWOH7Ha7kpKSdNVVV+mWW245hz8BqaSkRAsXLtSGDRt05MgR2e12xcbGqkuXLrrpppvUuXPnX73m2LFjGjJkiCRp+fLlcjgcmj9/vr7++mvl5OQoPj5eH3/88TnV9yW+8t8FAM/AAB8APFB2drZycnLMjgEAAAD8it1u18GDB312SJmVlaXWrVvX+XUfffSRnn32WVVXV0uSQkJCZLFYdOjQIR06dEjLly/Xs88+q0svvfSM77FixQpNnjxZ1dXVioyMVE1NjWw2m9577z199dVXeuuttxQeHv6r1y1btkzTpk2Tw+GQJEVGRur48eN68803tW7dOt1www1nzf7dd9/pgQceUF5enqSf9v63Wq3KysrSqlWrtHr1at11111n/TBg586dmj59usrKymS1WhUUxMgJAFwhwOwAAIBTFRcXKyMjw+wYAAAAwK/U1NRo3759qqmpMTuKS3Xs2FEWi0WS9NJLL+nw4cN1ev369es1bdo0SdKYMWP08ccfa/Pmzdq0aZMWL16sfv36qbS0VA8++KAyMzNP+x4nTpzQk08+qT/+8Y/65JNPtH79em3YsEETJ05UUFCQDhw4oLfffvtXr0tPT9f06dPlcDjUrVs3LV68WOvXr9fGjRs1bdo05eXl6fXXXz9j9mPHjumvf/2r8vLydNVVV2nBggXavHmzvvjiC61atUpjx45VQECAXn31Va1fv/6M7zN9+nS1adNGb7/9tjZt2qSNGzfqlVde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not77703Nm3aFLt27Ypzzz037rzzzjN6vQcOHIivfvWr8dhjj8XExERUKpU477zz4qqrroqzzz571udFRDz33HPx13/917Fp06bYuXNn5HK5GBsbi5/92Z+N3/zN35zx+J477rgj7rrrrvjJn/zJuPPOO+PRRx+NBx98ML73ve/Fnj174uqrrz5l3uWSJInzRAFggSYnJ+PIkSNpx4C21Gg0HPUIZIYCH2AW7VLgv93ExESMj4+n8rlfffXVuOGGG2LXrl1RLpejUCjErl274hvf+EY8/vjj8YUvfCHe+973npT3t37rt2L79u0REZHL5aKnpyf27NkTu3btihdeeCG2bt16UoE/OTkZX/ziF+NrX/va9K91dXXF5ORkvPjii/Hiiy/G448/Hv/yL/8yY9ZHH300brjhhjhy5Eh0d3dHoXDm3y5ff/31uPLKK2Pbtm1RLBajVCrF3r1741//9V/jiSeeiBtuuCF+6Zd+acbn3nHHHXH33XdPH+FUqVTiyJEj8fzzz8fzzz8ff/d3fxe33HJLnHXWWbN+/i9+8Ytx3333RZIkUavVMrXavb+/P4rFYtoxAKClteNYFbKgUCg46hHIFAU+wCzaZVL0rne9K5IkiampqbjlllvipptuijVr1ix7jptvvjl6enri1ltvjQsvvDCSJIlnn302/uiP/iheeOGF+PSnPx1/8zd/c9xZk3feeWds3749ms1mfOYzn4mf+qmfinw+H5OTk7Fjx4548skn44c//OFJn+u2226bLu9/8Rd/MT7ykY9Mv+bdu3fHM888c8rz/j/72c/GhRdeGJ/85Cendwa89NJLZ/R677rrrsjlcvH5z38+Lr744igUCvHiiy/GjTfeGJs2bYo/+ZM/iQ0bNpxUwt9///1x1113RXd3d1x11VXxC7/wCzEwMBCTk5Pxve99L770pS/Fd77znfi93/u9eOCBB2a8WOu5556LTZs2xZVXXhlXXHFFrFy5Mt58883YtWvXGb2GpZAkSYyMjKQdAwBaXruMVSFrms1mpha/APiKBDCLdpkUNZvNeP/73x8RES+88EJ84AMfiA996ENx0003xcMPPxwvvPDCslzWe+jQofjyl78cF1100fR21LPPPjtuv/32qNfrceDAgbjnnnuOe84zzzwTEREf+9jH4oILLoh8Ph8Rb12AOjIyEh/4wAfi2muvPe4527Zti3vvvTciIq688sr4gz/4g+N+YNHX1xcXX3xx3HjjjbNmHR8fj5tvvvm4Y31Wr159Rq93//798fnPfz4uvfTS6RX84+Pj8aUvfSlWr14dk5OT8ed//ufHPWfPnj1x++23R5Ik8YUvfCE++tGPxsDAwPRr3rhxY3z5y1+OjRs3xsTERPzt3/7tjJ/7jTfeiA996ENx3XXXxcqVKyMiolQqZaI4HxoailKplHYMAGh57TJWhSypVCrT42+ArFDgA8yinS4Fu/766+Pqq6+Orq6umJqaiv/93/+NBx54ID73uc/FBz/4wbjsssvi5ptvXtIV2pdeeumMx/f09fXFr/zKr0RExDe/+c3jHqvVahHx1tn5c/XII4/E0aNHo16vz/u89yuuuGL6hwXzde655854Nn+lUokrrrgiIiKeeuqp2L9///Rj//AP/xAHDx6MjRs3znquf6FQiMsuuywiIv793/99xt+Ty+XiIx/5yILyL4VCoRDDw8NpxwCAttBOY1XIitHRUWffA5njCB2AGbTbpWCFQiGuueaa+PCHPxz/9m//Fps2bYr/+Z//iRdffDEOHz4cu3fvjvvvvz/+/u//Pm655ZbTXrA6H7NdgBsRcf7558dXv/rV2Lt3b7zyyisxOjoaERE/8zM/E88880zceuutsW3btrjkkkvinHPOiZ6enln/XcdW7V944YVRLpfnlfW8886b1/Pe7vzzzz/tY0ePHo3nnntu+s9m8+bNERGxZcuW6ZJ+JgcPHoyImPH4oIiIsbGx6Ovrm0/sJTUyMrLgH4wAAG+xAh8WV09PT/T29qYdA+AkCnyAGbTrhKinpycuv/zyuPzyyyPirde5efPm+NrXvhZPPPFE7NmzJ66//vp48MEH511+z6bRaMzpsddee226wL/yyivj+eefj3/6p3+Khx56KB566KFIkiTWrVsX733ve+P973//Sef5H9tFsJDjYo4dO7MQg4ODc3ps9+7d0x//6Ec/ioi3/l7m8h48VuSfKIvlfblcPuWfCQBwZtp1vAppOTYHAcgaBT7ADDplQlQul+PCCy+MCy+8MP7wD/8wHnnkkZiYmIinnnoqLr744kX9XPPZilooFOLGG2+Mq666Kh5//PHYvHlzPPvss7Fly5bYsmVL3H///XHttdfGhz/84UX5fMcsxirxU33+2R47evRoRET86q/+anz605+e9+fO4qVbzWbTdmQAWCRTU1OO0IFF1Nvbe8pdvgBpUuADzKATJ0S//Mu/HI888khERGzdunX61/P5fExOTp7yz+Tt57jPZmJiYtbHduzYMf3xTKvf3/nOd8Y73/nOiIg4cuRIbNq0Ke6+++7YtGlT/Nmf/VlccMEF048PDAzE1q1b49VXXz1tpqX09td0qsfevlq+v78/It66bLidVKvVRdnVAAC85fDhwzE1NZV2DGgLSZJYfQ9kWvaW6AFkQDudfz9X1Wp1+uNSqTT98bGLZGcr4A8cOHBc4T+bp59++rSP1ev10w6eC4VCXHDBBXHLLbdEqVSKqamp+I//+I/px88555yIiPj2t7+d6k6KubzeXC4XGzZsmP71c889NyIinn322VnPt29FY2NjVt8DwCLqxLEqLJWBgYGoVCppxwCYlQIfYAaTk5NpR1g0r7zySmzbtu20v+/Y6vuIiLPOOmv642Mr2x977LEZn3fvvffOacfCo48+OmPRv2fPnnjwwQcjIuLnfu7njnvsVP/eUqk0fVTM24+Med/73hf5fD727t0bd9xxx2lzLZXNmzfPWOIfOnQo7r333oiIuOiii6Z/QBIRcfnll0e5XI7Jycm46aabTvk+PHr0aOzbt2/xgy+yer1+3GsEABauncaqkKZcLregu7MAloMCH2AG7bSq6fvf/3782q/9Wvzu7/5uPPLII8cdLXPkyJF47rnn4rOf/Wzcd999ERHxEz/xE3HeeedN/56f//mfj4iIp556Ku64447p43L27NkTt912W3zlK1+ZU0FbKpXiuuuui29/+9vTW77/+7//Oz72sY/Fnj17oru7Oz760Y8e95z3ve99ceutt8Z//dd/HVfm/+AHP4jPfOYzcfDgwcjlcvGe97xn+rFVq1bFFVdcERERf/mXfxmf+9zn4qWXXpp+/LXXXotvfvOb8fu///tz+eObt56enrj++uvjn//5n6ffT1u3bo1PfOITsXXr1sjn83HNNdcc95yBgYG49tprIyLiW9/6Vnz84x+PzZs3T0/Sp6amYuvWrXHvvffGb/zGb8QTTzyxpK9hoZIkiVWrVqUdAwDaTjuNVSFNo6OjUSwW044BcErOwAeYQTutaioUCnH06NF48skn48knn4yIiGKxGNVqNV5//fXjzk8966yz4k//9E9PWtH+jW98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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(2.2,0.75, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,2.35, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,4.15, 'Subscriber', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'Sarracenia Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "0f8a727c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " the cost on the broker to route a single product: 120366 in integer ops\n", + "\n", + "regex's are O(m,l) = 1500 hmm... sounds like too much.\n", + "overriding source, lowering estimate to 100 more investigation needed.\n", + "Assuming 10 senders selected by broker, each one needs to look at the 10's in it's configuration\n", + "Client-side routing cost is 5000\n", + "\n", + "t total cost=125366\n" + ] + } + ], + "source": [ + "\"\"\"\n", + "\n", + "Estimating the cost to route 1 product, using topic based routing along with the traditional regular expressions.\n", + "\n", + "\n", + "\"\"\"\n", + "from math import log\n", + "\n", + "K=10000 # number of topics/folders in the hierarchy\n", + "B=8 # branching level at each level in the hierarchy\n", + "n=10 # number of characters in a topic name\n", + "b=2 # number of bindings per sender\n", + "S=100 # number of senders\n", + "\n", + "\n", + "# routing cost on the broker, in integer ops.\n", + "# log(K,2)*n - lookup each topic in a dictionary to map it to an integer\n", + "# log(K,B) - the depth of the higherarchy is a function of the total, and the number of choices at each level.\n", + "# log(B,2) - the time to search each level in the hierarchy, comparing integers.\n", + "# \n", + "Rpb = ( log(K,B)*(log(K,2)*n + log(B,2))*b*S )\n", + "\n", + "# AMQP exchanges function as an additional topic in the higherarchy at the top of the tree.\n", + "# so they can be ignored.\n", + "\n", + "print( f\" the cost on the broker to route a single product: {int(Rpb)} in integer ops\" )\n", + "\n", + "# this is pretty iffy, used this conversion throughout, but if RE's are much cheaper, then\n", + "# ... the numbers change alot.\n", + "# convert integer ops to regular expressions. This is difficult to estimate\n", + "# https://swtch.com/~rsc/regexp/regexp1.html \"Regular Expression Matching Can Be Simple And Fast (but is slow in Java, Perl, PHP, Python, Ruby, ...)\" -- Russ Cox, rsc@swtch.com\n", + "# it lists O(m,l)\n", + "# \n", + "m=30 # the length of the regex being matched to.\n", + "l=50 # the length of the string being matched.\n", + "RE=l*m\n", + "\n", + "print( f\"\\nregex's are O(m,l) = {RE} hmm... sounds like too much.\")\n", + "# Seems overly pessimistic, overriding.\n", + "\n", + "RE=100\n", + "\n", + "print( f\"overriding source, lowering estimate to {RE} more investigation needed.\")\n", + " \n", + "# on the client side, in # of regex evals. \n", + "\n", + "s=10 #number of senders pre-routed to by the broker.\n", + "re=10 #number of regular expressions per sender to evaluate.\n", + "\n", + "Rpc = s*RE*re/2\n", + "\n", + "print( f\"Assuming {s} senders selected by broker, each one needs to look at the {re}'s in it's configuration\")\n", + "\n", + "print( f\"Client-side routing cost is {round(Rpc)}\")\n", + "\n", + "Rp=Rpb+Rpc\n", + "\n", + "print( f\"\\nt total cost={round(Rp)}\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2cf1861e", + "metadata": {}, + "source": [ + "The Algorithmic Cost to Route 1 File\n", + "----------------------------------------------------\n", + "\n", + "\n", + "| Application| Pr | Client | Dominant Term/ compute cost scales as a function of | Pre-Routing Done By |\n", + "|------------|-------|--------|----------------------------------------------------------|-----------------------|\n", + "| PDS | 500 | 50 | product of the number of receivers, senders, products | One Dispatcher process|\n", + "| Sundew File| 500 | 50 | product of the number of senders, products | All (10ish) receivers |\n", + "| Sundew Msg | <1 | 5 | log of the number of messages | All (10ish) receivers |\n", + "| Sarracenia | 120 | 5 | log of the number of topics/folders | Message broker |\n", + "\n", + "* Units: Kilo Instructions per second. 500 = 0.5 MIPS.\n", + "* broker is a service, but is multiprocessing/scaled itself. not a single process.\n", + "* sender processes do the client-side routing in all cases.\n", + "* For Sundew, this number is optimistic (bulletin) case, file case is more like PDS, but multiple routers.\n", + "* for Sarracenia, this number applies to all routing.\n", + "* give scaling factors the large the configuration, the bigger the perf. advantage for topc routing.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa0fc94b", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "s=10 # number of senders / product shipped.\n", + "ServerPeak=1000\n", + "\n", + "#for PDS\n", + "clientSide=1000/0.050 = 20000\n", + "Dispatcher=1000/0.5= 2000\n", + "PDSPeak=2000\n", + "# limited to slowest element, which is the dispatcher, so 2000 is upper bound.\n", + "\n", + "#For Sundew\n", + "SundewClientSide=1000/0.005= 200000\n", + "SundewReceivers=1000/0.182 = 5494\n", + "SundewPeak=SundewReceivers*s=54940\n", + "# limited minimum of client and receivers, so 54.9 is it.\n", + "\n", + "#For Sarra\n", + "SarraClientSide=1000/0.005 = 200000\n", + "SarraBroker= 1000/0.125 = 8000.0\n", + "# how many cpus can a broker use to route? um... it isn't limited.. or is at least unknown...\n", + "# hmm... defined by the broker implementation.lets just use s.\n", + "SarraPeak= SarraBroker*s= 80000\n" + ] + }, + { + "cell_type": "markdown", + "id": "53af0101", + "metadata": {}, + "source": [ + "\n", + "Overall Server Algorithmic Ceiling\n", + "-------------------------------------------------\n", + "\n", + "\n", + "Assume a 1 GIPS machine within infinite cpus, and take IP cost per route, so the number of products that can be routed by a server is going to be inverely related to cost to route each one: route/second = 1000 MIPS / cost to route from previous cell. The other contributor is the number of processes doing the routing.\n", + "\n", + "| Application | Route/s | Routers | Total |\n", + "|-------------|----------|---------|--------|\n", + "| PDS | 2000 | 1+10 | 2000 |\n", + "| Sundew | 5494 | 10+10 | 54940 |\n", + "| Sarracenia | 8000 | 10+10 | 80000 |\n", + "\n", + "\n", + "* These numbers are algorithmic maxima, never to be seen in real life.\n", + "* The Sarracenia one requires a guess at parallelization of Rabbitmq, is conservative.\n" + ] + }, + { + "cell_type": "markdown", + "id": "64ff1fe0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Duplicate Suppression\n", + "===================\n", + "\n", + "* All message passing networks implement \"Duplicate Suppression\"\n", + "\n", + "* typical:\n", + " * checksum the message, store the checksums.\n", + " * when you get a new message compare it's checksum to what is in the store.\n", + " * if there is a hit, then it's a duplicate, so discard the message.\n", + " \n", + "* Does not need to be perfect...\n", + " * a few duplicates is ok, but need to prevent \"storms\"\n", + " \n", + "* Simple if one process is handling an entire flow.\n", + "\n", + "* Not simple if > 1 process share the flow.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bfc76ce5", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hb5f3//5dsJ3Y84pnYsZPYGWRvNgUSAoQ9WtYHKFDKar8t3YNSPi0dtKW0pYO2pIzyYZQRKJSmbJJAmAGyyI7jvadseUiWpfP7g59VFHnJlnSO5OfjunyBj3Xu85ajnEivc5/3bTMMwxAAAAAAAAAAALCUOLMLAAAAAAAAAAAAgQjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfADAsRUVFstlsvq8vfOELEa/hoYce8qvBZrOprKws4nXAOqLxNXF4vbfffrvZJUVENP5ZYWRWrVrl9+e8atUqs0sCAAAAolaC2QUAAIDY0tTUpD179qiiokJNTU3q6uqSzWZTenq6MjIylJWVpQULFmj69Olmlwr46enp0a5du1RaWqqamhp1dnbK4/EoIyNDmZmZmjx5spYtW6acnByzSwUAAAAwRhDgA0CYvPfeezr++OP9tp144onavHnziMb7+OOPtWTJkoDt119/ve67774Rjblu3TpdeumlftsuueQSPfXUUyMaD2OTYRh69dVX9c9//lOvvPKKSktLh7VfVlaWli1bpjVr1ujiiy/WrFmzwlwpEKi6ulqPP/64nnvuOX3wwQfq6ekZcp8ZM2boM5/5jC6//HKtWbNGCQm8pQYAAAAQHrTQAYAwOeqoo5SWlua3bcuWLerq6hrReJs2bep3+8aNG0c03kBjnnLKKSMerz9jtV3IWOD1evXAAw9ozpw5OuOMM7R27dphh/eS1NLSog0bNuiWW27R7NmztWLFCj344IPq7e0NY9XAJ8rLy3X11VdrxowZ+u53v6u33357WOG9JJWWlurRRx/VOeeco4KCAt12221qb28Pc8UAAAAAxiICfAAIk4SEBJ144ol+23p6evT222+PaLyBAvxDhw6psrIyZGOGOsBHbNq1a5eOPPJIXX/99SouLg7JmNu2bdN1112nuXPn6oknngjJmMDhDMPQ7373O82fP1+PPPKI3G73qMZraGjQHXfcoVmzZumvf/1riKoEAAAAgE9wvy8AhNEpp5yiF1980W/bpk2bdPrppwc1jmEYevPNNwf8+aZNm3TVVVcFNWZDQ4P27Nnjty0vL0/z5s0LahyMPU8++aS+8IUvyOl09vvzcePG6eijj9bxxx+vyZMnKzs7W+np6XK5XGppaVFpaam2bt2qDz74oN87UkpKSvSb3/xG//M//xPup4Ixxul06oorrtCzzz474GOKiop06qmnqqioSJMmTVJ2drZcLpeampp08OBBvfvuu9q+fbu8Xq/ffk1NTbrzzjv15S9/OdxPAwAAAMAYQoAPAGHU32z2kbS8+fjjj9XU1OT7Pj4+Xh6Px2/MYAP8YGffl5WVBTU+YtODDz6oG264ISC8lKQlS5botttu09lnn62UlJQhx3K5XHr11Vf10EMP6V//+hetcxBWLpdL55xzjjZs2BDws3HjxumGG27Q17/+dc2ZM2fIserr6/XEE0/oj3/8o0pKSsJRLgAAAABIooUOAITVihUrlJ6e7rftww8/VGdnZ1DjHB62X3HFFbLZbAP+fCRjSrTPweBeeukl3XjjjQHhfVpamv7xj39o+/btuuSSS4YV3ktSYmKizj33XD399NMqLi7WF77wBcXF8dYE4XHdddf1G95/5jOf0d69e/XnP/95WOG9JOXm5urrX/+6Dhw4oHvvvVc5OTmhLhcAAAAAJBHgA0BYxcXF6eSTT/bb5na79dZbbwU1zuFh++c+9zktWrTI931paanKy8tHNaZEgI+B1dfX6+qrr/a780OSpk6dqrfeekuXX36530WlYBUWFurvf/+73n77bc2fP3+05QJ+HnroIT322GMB2y+44AK9+uqrmjVr1ojGjY+P10033aSPP/5Ya9asGW2ZAAAAABCAAB8Awqy/UDyYGfOH97+32Ww66aSTtHLlyhGPWV9fr7179/ptmzp1qmbPnj3sMTC2fOtb31JjY6PftgkTJujll1/WkiVLQnac4447Th9++KGuvPLKkI2Jsa2lpUXf/OY3A7afcMIJeuaZZzRhwoRRHyMvL08vvviivvGNb4x6LAAAAAD4NHrgA0CYjbYP/s6dO9Xc3Oz7fvHixcrOztaqVat0zz33+I15zTXXDGvMWJ9973a7tWXLFu3Zs0dNTU0aN26cJk2apDlz5uiYY45RfHy82SX6dHd368CBA9q3b5+amprU3t6uhIQEZWZmKicnR8uWLVNRUZGpNe7YsUOPP/54wPa77rpLCxYsCPnxkpOT+w1cI6Wrq0vvv/++ampq1NjYKKfTqUmTJmny5Mlavny5pk6dGvYaDMPQ9u3btXPnTjU0NMjj8WjKlCmaPn26TjjhBCUmJob0eA0NDdq3b58OHToku92uzs5OpaWlKSsrSwUFBTrmmGOUmpoa0mNGyq9+9SvZ7Xa/bampqXr44YdDei6Ii4sbUYBvhXOAx+PR9u3bVVZWpqamJrW0tCghIUETJ05UUVGR5s+fr+nTp4f8uI2NjXrvvfdUUlKijo4Opaena/LkyTr22GNVWFgY8uNJktfr1bZt21RWVqbGxka1tLRo4sSJmjRpkmbPnq3ly5fTygsAAACWQoAPAGG2dOlSZWVlqaWlxbfto48+ksPhUFpa2pD7Hx629828P7w1TzAz8EcS4BcVFfm16bnmmmv00EMP9Tv2YGP95Cc/0U9+8pNBj1VYWDiiRXPr6ur0q1/9Sg899JDa2tr6fUxGRoa+8IUv6LbbblN2dnbQxxgtwzD0zjvv6Pnnn9emTZu0devWIRdvnTp1qj73uc/p29/+dlhCtKH84Q9/kGEYftsWLFigr3zlKxGvJZzWrVun++67T2+++aZcLteAj1u4cKEuvvhifetb39LEiRNDWoPD4dBvf/tbrV27VnV1df0+Jj09XRdeeKFuv/32EQe7DodDzz//vF555RVt2rRJFRUVgz4+Pj5ey5cv10033aSrr75a48ePH9FxI62rq0t/+9vfArZ/73vfG3HbnNGy0jng+eef14MPPqhNmzYNeM7sM336dK1Zs0ZXXXVVwL8/wdq0aZN+8Ytf6PXXX+93QWzpk3PMrbfeGrDmy0ht3rxZf/7zn/Xqq6/6/Xt8uKysLJ199tn6wQ9+EJYLlAAAAEDQDABA2H32s581JPl9vfDCC8Pa98ILL/Tb75lnnvH9bMGCBX4/KykpGdaY8+bNC6intLR00H0KCwv9Hn/NNdf0+7iNGzcGjB3sV2FhYb9j//3vfx+w7qefftrIyMgY9jGys7ONd999d1i/r1DZtGmTMW3atBH/XhISEozbbrvN8Hg8Eau5q6vLSE5ODqjlnnvuiVgNgxnsNTFcH330kXH00UcH/eeRk5Nj3HvvvUHXfPg4P/7xjw3DMIwtW7YE9fpITk42fv/73wd9/O9+97tGUlLSiF+HU6dONd58882gjxuKP6tgPfLIIwHHHDdunFFbWxvW4w7EKueAjRs3GsuXLx9xHTfffPOg469cudLv8StXrjQMwzCcTqdx/fXXB3WsNWvWGB0dHSN+rvv37zfOOuusoJ9jXFyccd111xnd3d0jPjYAAAAQCtwfCgARMNI++EY//e8/PfNxJH3w6+vrtW/fPr9thYWFprdpGY17771Xl1xySUCbjME0NzfrtNNO0/bt28NW1+FKS0tVWVk54v17e3v185//XBdccMGQM3ZD5fXXX1dXV5fftqSkJF199dUROX64vfjiizr55JP1wQcfBL1vU1OTvvSlL+nrX//6gLOIh+vDDz/UKaecEtTro6urS9/4xjd02223BXWsLVu2yOl0BluiT1VVlU499VQ98sgjIx4jUp5//vmAbRdccIHy8vJMqMYa54Df//73Ou2007Rt27YR19He3h70Pk6nU2eddZbuv//+oPZ75ZVXdPbZZwcsoD0cr7/+uo499li9+OKLQe/r9Xr1wAMPaOXKlaqvrw96fwAAACBUaKEDABEw0j74O3bs8LvVf+HChcrJyfF9v3LlSv31r3/1G/Paa68ddMz+jrt69eohaxmu1NRULV261Pf9jh07/H6em5s7ZHiWn58/7OO9+OKL+upXv+pr8ZKenq41a9bohBNO0OTJk+X1elVWVqb169fr/fff99u3s7NT1157rT744AMlJET+n8S8vDwdeeSRmj9/voqKijRx4kQlJyers7NTDQ0N+vjjj/XSSy+poaHBb7/169fr1ltv1a9//euw19jf62XFihXDav9kdRs3btT555/fbxC6dOlSnX/++SoqKtKECRNUW1urN954Qy+//HJAe50//vGP8ng8fmtSBMNut+vCCy9UZ2enb9vy5ct1/vnnq7CwUImJiaqurtbrr7+u119/PaDeO+64Q9nZ2SNaN8Bms2nx4sVavHix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5betra1NZ5111ohbZTkcDt1111169NFHQ1EiAAAAEBQCfAAwSX+z4A9vLzOShWYPD+cPH3PFihUB7STC6fB+4G1tbbrsssuisvXKSI0bN04XXnih37bW1ladeuqpg876rqmp0RVXXKEf/ehHvm0TJ04MV5lDOuecc/SXv/wloB++w+HQ5ZdfruXLl2vdunVBXaDZs2ePvv/97+vYY48NdbmD+uUvf+m36LMkFRcX6zOf+Yxee+21Aferrq7WBRdcoP/7v/8L+Nnf/va3gJn9w9G3T1VVlU466SS9/PLLAz72wIEDOvXUUwN65iclJQ25JsGZZ54Z0NLqqaee0vXXXz/on9krr7yi4447zrfYr5mvwdF48MEHtXr16oDtb775pubNm6ebb75ZBw8eHNZY9fX1+sMf/qA5c+boS1/6kpqbmwd9vNnngKysLP3f//1fwN/dp556Sscff/ygr/lP1/vggw9q6dKl+vGPfxx0DZH0+OOPB6wxUFJSomOPPVZ33HGHX6u6gXi9Xm3cuFFf+tKXNH36dH3ve99TXV1duEoGAAAABpRgdgEAMFYN1cZm5syZmjZtWtDjrly5ctCFDiPZPkeSrr76at12221+iwj+5z//0X/+8x9lZmYqNzdXiYmJfvvk5+frhRdeiGid4fa///u/evLJJ+V2u33bDhw4oOXLl+vMM8/U6tWrNXXqVPX29qq2tlabNm3Sa6+95jc79otf/KIOHTqkN954w4ynIEm68cYblZqaqi9+8YsBM3d37NihSy+9VOPGjdMxxxyj4447Trm5ucrOzlZ6erqcTqc6OztVU1Ojffv26YMPPhi0pdL06dPD9jyysrL08MMPa82aNX6vzdLSUp1++ulasWKFzjvvPBUVFSkpKUm1tbV688039dJLL8npdAaM95WvfEUXXHDBiGq56aabtG7dOtXU1KiqqkpnnnmmjjzySN/xx48fr+rqam3YsEGvvfaa32uozy9+8QvNmzdv0ONkZmbqm9/8pn7605/6bX/ggQf03HPP6ZJLLtGKFSuUmZkpu92ukpISrV+/Xh9//LHvsfHx8frDH/6ga6+9dkTP1UyJiYn6z3/+o8svvzxgId6enh7dc889uueeezRjxgyddtppKioqUk5OjrKystTT06OmpiYdPHhQ7733nrZt2xb0gtxmnwPOP/98/fCHP9TPf/5z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qqyslNPpVHZ2tgoLC7Vq1SqdeeaZSk1N9T2+paVFmzdv1ltvvaXS0lI1Njaqt7dXkyZN0pFHHqkrrrhCs2bNGvD5S9L69eu1fv16v8fce++9Ouqoo/y2VVVV6R//+Ie2bNmi+vp6eb1eTZkyRccff7yuvPJKy7QGiiaEUtGtubmZVo9AmPFeFUA4EeADwCg4nU51dXWZXQYQ82ihg9Goq6tTd3d3xI87btw4XXvttbrlllvU2dmpTZs26YILLgj5cf74xz/q4Ycfls1mU1pamnp6erRv3z7t27dPb731ln7729+GdA2J9957T7feeqva29slfRLuJicnq7a2VrW1tXrvvfeUk5Pj16/+j3/8o1/4npKSot7eXlVVVamqqkovvviifvazn+nUU0/1PWbcuHHKzs5WR0eHXC6XEhMT/S4K9D3m05599lndeeed6u3tlSSNHz9eNptNZWVlKisr0/PPP68777xTxx13XMh+H2MBAX50a2lpMbsEIObxXhVAOBHgA8Ao8IEICL9x48YFhHTAcHV3d6uurs60459wwgmKj4+Xx+PR1q1bQx7gHzhwQFu3btWll16qG264QZmZmero6NATTzyhtWvX6t1339U999yjb33rWyE53r59+/Ttb39bLpdLM2fO1Ne//nUde+yxSkhIkNPpVElJiV555ZWAmYj5+fm67rrrtHr1ak2fPl0TJkyQ1+tVaWmpHnroIb344ou6/fbbtWTJEk2aNEmStHTpUr388su6/fbbtX79ep1++um6/fbbB6xt06ZNuuOOO5SQkKAvfOELuuiii3yz7cvLy3Xvvffqtdde0/e//309+eSTzMQPAi10old3d7cpFzCBsYYZ+ADCiR74ADAKBPhA+PGBCCNlGIbKy8tNbR2RnJysgoICSZ+0dgm1jo4OnX322fre976nzMxMSVJqaqquv/56ffGLX5QkPfnkkyFbbP03v/mNXC6Xpk+frgcffFCf+cxnfOFuUlKSFixYoG984xs65phj/Pa78cYb9eUvf1lz587VhAkTJElxcXGaNWuWfvazn+nEE09Ud3e3/vWvf42oLrfbrV//+teSpB/84Af66le/qilTpshms8lms6moqEi/+tWvdPLJJ6uzs1OPPfbYKH4LYw8z8KMX71WByOD9KoBwIsAHgBHq7OyUy+Uyuwwg5nFLMkaqoaFBnZ2dZpehiRMnSpKv5Uyo3XDDDf1uv+qqq5SYmCiPx6PXX3991MepqKjQ9u3bJUn/7//9v4B2NqNx4oknSpJ27Ngxov3ffvttNTQ0KDs7W+eff/6AjzvnnHMkSe++++6IjjNWMQM/OhmGQYAPRMCECRMUF0e8BiB8eCcGACPEByIgMpjRhJFwu92qqakxuwxJCusdALm5uZo2bVq/P0tNTdX8+fO1fft27d27d9TH2rlzp6RPZmN/5jOfCXr/AwcO6J///Ke2b9+u2tpadXV1Bfxu6uvrR1RbX/Df3t6uM888c8DHud1uSVJtbe2IjjNWMQM/OnV2dqqnp8fsMoCYx3tVAOFGgA8AI8CMJiBymIGPkaitrZXX6zW7DEmSw+GQJKWnp4d87MmTJw/6875+8q2traM+VlNTkyQpIyPD1wZnuJ588kn99re/9f2Z2Gw2paam+hbXdTqd6uzslNPpHFFtfS2C3G63mpubh3w8d9AFhwA/Og3n7wKA0QvlHWkA0B8CfAAYgfb2dvX29ppdBhDzUlJSaN2AoDmdzpD1fB+trq4uVVdXS5KmTp0a8vFtNlvIxwy10tJS/e53v5PX69Vpp52mq666SnPmzPFbnPq5557Tz3/+8xHfreDxeCR9smjwH//4x5DUjf/iPBx9DMMIyYU7AEPra5UHAOHCOzEAGAFm3wORwQcijERfYG4F77zzji9cPvLII33b+2Y0D9beoqOjY8jxh2o503cho2+B29HIycmRJNntdnV3dw97Fv7rr78uj8ejGTNm6Be/+EW/fYJHO1O4r7bi4uJRjYNANpvN72ILokNbW5vv3AMgfJKSknx3kwFAuLDKBgAEyev1ym63m10GMCYQ4CNYHR0dljlHu91u/f3vf5f0ye31q1at8v2s77U9WAC/e/fuIY9RX1+vqqqqfn/W2dnp630/f/784ZY9oKVLl0r6ZLb722+/Pez9+p7jEUccMeAif1u2bBlw/767DAabnd9XW0NDg2+hXYTG+PHjo+JOD/hjsgkQGbxXBRAJBPgAECS73W6ZvspALIuPj6f/PYI2UJgdaU6nU7fffrv2798vSfrCF76gtLQ038/nzJkj6ZMZ8h9//HHA/i0tLXr22WeHdaz777+/3+2PPvqoXC6X4uPjtXr16mCfQoBp06ZpxYoVkqS//OUvw7pDQPpvb+Di4uJ+Q/i3335bH3300ZD7960l0J+TTjrJNwv/N7/5zZC99Nva2oasG59ITEw0uwQEyePxWOZCJhDrCPABRAIBPgAEiQ/9QGSkpaUx6xNBsdvt6uzsNO34Xq9XxcXFevTRR3XZZZfp5ZdfliSdffbZuuaaa/weu2TJEk2ZMkWSdPvtt2vPnj0yDENer1cffvihbrrppmH1g09NTdX69ev1m9/8xhfYdXZ26sEHH9QDDzwgSbr00kuHXOx2uL797W8rMTFRFRUVuu666/TOO+/41oRxOp3atWuXfvGLX+j999/37XP88cdLkkpKSnTnnXf6/h3t7u7WM888o+9///uDLvA7a9YsSdL27dtVVlbW72MSExN1yy23yGazad++ffriF7+od999V2632/eY6upqPfPMM7r66qu1bt26Uf0exhIC/OjjcDhGvJ4EgOGz2Wx+F+cBIFzogQ8AQWpvbze7BGBMYEYTgmEYRsRn359xxhm+/+/p6VFnZ6ffHVoZGRn68pe/rIsuuihg37i4ON1666365je/qfLycl199dVKSkqSYRhyuVyaPn26vve97+mHP/zhoDXMmTNHixYt0sMPP6ynnnpKaWlp6ujo8PW+PuaYY/TVr341RM9Ymjt3rn7729/qlltu0aFDh/S1r31NCQkJSk5O9vv38YQTTvD9/zHHHKM1a9bolVde0dNPP62nn35aaWlp6urqksfj0fz583Xuuefqrrvu6veYq1ev1p///Ge1trbq4osvVkZGhq///i9+8QstXrxYkrRq1Sr99Kc/1R133KEDBw7o5ptvVnx8vFJTU9Xd3e233sDKlStD9juJdQT40Yf3qkBkpKamDtgaDgBCiQAfAILQ1dXlm2kIILyGE+Dffffd+ta3viXpk/7a+fn5KigoUEFBgfLz85Wfn6+cnBylpaVp4sSJSktLU1pamlJSUvjAFWOamprkcrkiesy+hVdtNpsmTJig7Oxs5eXlae7cuTr66KN18sknD7r45/HHH6/7779fDzzwgHbs2CGn06m8vDytXr1a1157ra9//VC+9rWvad68eVq3bp2Ki4uVkJCgWbNm6fzzz9cll1ziWzA3VI477jg9++yzevzxx/X222+rqqpKLpdL+fn5Kiws1CmnnKKjjjrKb5+f//znWrx4sZ5//nmVl5fL6/Vq1qxZWrNmja644grf3Qr9mThxou677z79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jB07VuXl5fJ4PFF7zr4VfRUVFdpll12G/Ljf/Gaurr/+Op15ywLtftjw9vIdqeqKD3XPuQdo56mTtXLlSuO3bH91dWA4HO6/kwEAEB/d3d06cub39eEny3X+vW+qZKc943r9z955WY/87w904oknav7f/77NCuSf/ezn+vvjT+qK59cpLT1TlmXpD7N30g9nHqb7779vWNdqamrq33M/VtP+vfbaS8uXL9fbb7+tAw44YEiPOemkk/T000/r8ssv1y233BLVPHV1dSopKZGUuIfZtre3q6qqKmG3yhkun8+n8ePHx+QuEQBIFhT4AICU0NnZqfXr16u7u9t0lLhzuVwqLS1VQUHBqAvs7u5uZWRkSBrexPiJJ57QqaeeqiMv/K0OO++Xo8owVJubanXH6d9QZ2uTamuqVVRUFJfrbs/rr7+uww8/PCn35wWARLF582YdMuNQra2q1k8eeFv55fE98HTFq0/rH9ecovPP/4nuuefu/tfmuro6TZg4UTPO/fVWr5Uv/ukK/ef//qb6utphbznS99z19fUqLCyM3jfxX++99572228/7bLLLqqoqBjSY9544w0ddthhkmJ7mG0s9tmPpVAopI0bN6q5udl0FCPy8vJUVlYmt9ttOgoA2A7LvgAASS0cDquqqkoVFRUpWd5LW/4ONmzYoE8//VRdXV2jeq709HTdfffdWr58+ZAf8/777+vsc87R3t8/XYeee82orj9UwUC3/n7ZD9XV1qyc7DG2KO8l6eyzz5Yk3XXXXYaTAEDqGjNmjBa//JKK8nL0158fobaG6rhef48jTtTx19yne+/9i6655sui/g9/+KOc7jTtf/LPt/r63Q+breamRi1btmzY1/rzn/8sSbr55ptHF3oQ3/72tyVJn3766ZC3xDn00EP1+OOPa8mSJTHJtGDBAknS+eefH5Pnj4WmpiatXLkyZct7SWpubtZ//vMfNTU1mY4CALbDCnwAQNJqaWnRhg0bor7HaqIrKipSSUlJXA4Oq66u1rf22Vfe/PE6/y9vyOMb/NC+aLEsS0/86gwtf/VJRcJhLVu2TN/97ndjft0d6e3tldfrlZS4t/UDQDLZsGGDvnvAgQp5MnX+fW8pMyc/rtd/a/7v9eLtv9Att9yiOXPmqHz8eO1zws911MW/2+rrIpGIbjm2XGefdpJuv/32YV2js7NTWVlZ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2XXGcQqFQlq0aJGR6+/IvffeK0k699xzDSf50llnnSVJeuyxxxSJRKLynO3t7dqwYUNUngvRwxwCQLJhCx0AgC01NTVp/fr1pmNgENnZ2ZoyZcqIyunPP/9cu+66q478+U06+Ee/2O7Xbm6q1d1n7asp40u19K03lZ6evtXne3t75fV6JW15c8HptMfahJKSEtXV1WnFihXafffdTccBAETZZ599pgMOPEjpBeN13t2vyZc18u3lRuqeH++vvacU69mFIztbJpYsy+p/Te7t7bXNWTAHHnigli1bpueff14/+MEPRvVcPT09Wr169YgXNSC23G63pk2b1j9OBIBEZo9ZLgAAX9HR0aGqqirTMbAdbW1tqqmpGdFjd9ppJ5133hy9+dA8dbe3Dvp1wUC35v/iOKW7peefe3ab8l6S/va3v0nasqrOLuV9b2+v6urqJInyHgCS1M4776xXFr+sturP9fdf/HDId5VF066HztZLL72kzs7OuF97RxwOh4477jhJ0vPPP282zFf0jRtmzZo1qucZ7R2JiL1QKMS/IwBJwx4zXQAA/qu3t1dr1qwRN4jZX11dnVpaWkb02Guv/bXCwYDeeuTWAT9vWZYW3HCeGtas0AvPP6fS0tIBv+6nP/2pJOmWW24ZUY5YeOqppyRJP/rRjwwnAQDE0t57760X//lPVf/nPf3jmlMUDgXjev3dDj1ePYGAXnrppbhed6huvXXLa/ypp55qOMmXpk798uDhke6TblmW1q1bp+7u+L9pg+Hp7u7WunXrmFcASHgU+AAA24hEIlqzZo1CoZDpKBii9evXq6ura9iPKy0t1aX/c6mW/eOP2txUu83nX3/wRn388j/0yMMP61vf+taAz7F27dr+PxcVFQ07Q6yceeaZkqTf/va3hpMAAGLtgAMO0MJnntHn77ykp687N2p7qw9FfvlUle68pxY880zcrjkcfWV5KBSy1V0Cd911lyTpqquuGtHja2tr1draGsVEiKXW1tb+OyMBIFFR4AMAbGOkZTDM6XvTJRgc/qrDK6+8QhnpPr3+wA1bfXzFawv0yl9+rblzr9NJJ5006OPnzJkjSXr22WeHfe1Y+eqBaePHjzeYBAAQL9///vc1f/58ffzSo3rh1kviutp31xmztWjRIvX09MTtmsNxySWXSPpy6xo7OO+88yRJDz300LD/XbW2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PfbY5jE1NTWaNWuWJOn5559XJBLRww8/rPfee0+NjY3Kz8/XCy+8MKTrJ5Nk+e8CgD1Q4AOADTU0NKixsdF0DAAAAGAbgUBAlZWVSVtS1tfXa9KkScN+3MKFC3XzzTcrFApJktLS0uRwOLRu3TqtW7dOzz//vG6++Wbtt99+gz7HSy+9pLlz5yoUCikrK0vhcFjV1dV67LHH9O677+pvf/ubMjIytnncc889pxtvvFGRSESSlJWVpdraWj300EN64403dPzxx283+8qVK3XZZZepublZ0pa9/30+n+rr67V48WK98sor+tnPfrbdNwOWL1+uefPmqaurSz6fT243lRMARIPTdAAAwNba29u1YcMG0zEAAACAbYTDYX3xxRcKh8Omo0TVrrvuKofDIUm6/fbbtX79+mE9fsmSJbrxxhslSeecc45eeOEFLVu2TG+//baefvppHXHEEers7NSVV16purq6AZ9j06ZNuv7663Xsscdq0aJFWrJkid566y1dccUVcrvdWrt2rR555JFtHldRUaF58+YpEonoW9/6lp5++mktWbJES5cu1Y033qjm5mY98MADg2avqanRxRdfrObmZh1++OGaP3++li1bpjfffFOLFy/WnDlz5HQ6ddddd2nJkiWDPs+8efM0efJkPfLII3r77be1dOlS3XnnncP6e0wWfT9LABANFPgAYCM9PT1as2aN6RiArTEhAgDADMuytHbtWvX09JiOEnWlpaU67rjjJElffPGFTjzxRJ1xxhm6+eab9dxzz+mLL74Y9I6DYDCoW265RZJ09dVX66KLLlJJSYkcDoccDocmTpyo3/3udzr44IPV2dmpRx99dMDnCQQCOvLII/WrX/2qf6sdn8+nk08+WaeccookDXiA7t13361wOKzx48frT3/6kyZOnChpy779M2fO1Lx589Te3j7o9/6nP/1J7e3tOvroo3XzzTdr2rRp/avnc3NzdcEFF+iSSy6RJN13332DPk92drbuvvtu7brrrv0fmzBhwqBfDwAYGgp8ALCJcDisNWvWJN1qJiDaKPABADCjurpamzdvNh0jZq688krNmTNH6enpsixLn376qZ566indcMMNOvXUUzVz5kz94Q9/6N9mps+yZcvU0NCgvLy8/v3gB3LMMcdIkt55551Bv+a8884b8ON9+/Jv2LBhqwN029vb9e6770qSzjrrrAH35t9///215557Dvi8bW1teuONNyRpm4N7B8r+2WefbfP99zn55JMH3N4nFTFeBRBNbEgGADZgWZbWrVun7u5u01EAAACAbbS0tKi+vt50jJhyu9264IILdOaZZ+qtt97Shx9+qFWrVqmyslLBYFAtLS167LHH9OKLL+r222/X7rvvLkn65JNPJEmbN2/W97///UGfPxgMSpJqa2sH/Hx2drbKy8sH/FxBQUH/nzdv3txf1FdUVPTve7/PPvsMeu199tlHy5cv3+bjK1as6H/8hRdeOOjjv6q2tlZ5eXnbfHyvvfYa0uMBAMNDgQ8ANlBbW6vW1lbTMYCEwIomAADiq6urS+vWrTMdI26ysrJ09NFH6+ijj5a0ZZvLjz/+WI8//riWLl2q1tZWXXnllXrmmWfk9XrV2NgoaUtBP9jq9K8abAui7a1ed7lc/X/uOyRX2vLGSp/CwsJBHz/Y5/qySxpSdklb3QHwVbm5uUN6fCpgvAogmijwAcCw1tbWQVfhANgWEyIAAOInGAxud//3VOD1evWd73xH3/nOdzR37lwtWrRI9fX1eueddzRjxoz+LTC/+93v6o477jCcdnj6snu9Xi1btmxUz+V0sktzH8arAKKJ364AYFBPT48qKytNxwASSt+hagAAILYsy+rfPgZbHH/88f1/7rsrIT8/X9KWw2/j7aur3hsaGgb9uq+utP+qvuw9PT3asGFDdMOlMMarAKKJAh8ADOmbEPXtOQlgaL56CzkAAIidhoYGtbe3m45hK1/d5iYtLU3Sl3u/NzQ06OOPP45rnmnTpvWvfP/ggw8G/br3339/wI/vueee/avFX3755egHTFGMVwFEEwU+ABhSV1enzs5O0zGAhMOECACA2Ovu7lZ1dbXpGHFTXV2t9evX7/DrFi1a1P/nadOmSZIOOuig/pXst91226B7xPdpa2sbRdKt+f1+7bfffpKk+fPnD7i//nvvvTfgAbbSlhX8hxxyiCTp73//+w7/DqKZPZkxXgUQTRT4AGBAV1cX+94DI8QtyQAAxFbfnaKptO/92rVrddJJJ+l//ud/tGjRItXU1PR/LhQKqaKiQtddd50effRRSdJuu+2mvffeW9KW/eOvuuoqORwOVVRU6Nxzz9U777yz1dZD1dXVWrBggc466yw99dRTUc1+wQUXyOVyad26dbr00kv7t/YJhUJ65ZVXdPXVV8vv9w/6+EsvvVTZ2dnq7OzUnDlz9Nxzz6mjo6P/862trXr99dd1+eWX65e//GVUsycjh8NBgQ8gqpgBA0CcRSIRrVu3LqUmREA0MSECACC2ampq1N3dbTpGXLndbkUiES1btqz/MFePx6OMjAxt3rx5q7H7tGnTdNttt211aOuMGTN0/fXX68Ybb9Rnn32miy++WC6XS1lZWeru7lZvb2//1/ateI+WXXfdVVdeeaVuuukmvf/++zrxxBOVlZWl3t5e9fb2auLEiTr++OP1xz/+ccDHl5WV6a677tIVV1yhmpoa3XDDDfrtb38rv9+vUCikrq6u/q/99re/HdXsyYixKoBoo8AHgDirq6tLuQkREE1MigAAiJ2uri7V1dWZjhF3+++/vxYuXKhly5bp448/1po1a/rPAPD5fCooKNAuu+yiQw89VEccccRW5X2fo446Svvuu6+eeuopvfPOO9qwYYM6OjqUnp6uiRMnau+999aMGTP0zW9+M+r5Z8+eralTp+qhhx7S8uXLFQgEVFxcrMMPP1znnHOOXn/99e0+ftq0aXryySf1/PPPa8mSJfr888+1efNmeTwejR8/XrvuuqsOPvhgHXDAAVHPnmwYqwKINofFElAAiJvu7m6tXr2a1ffAKJSVlamoqMh0DAAAko5lWaqoqNhqxTWA4cnMzOw/HwEAooE98AEgTizL0vr16ynvgVFiVRMAALFRX19PeQ+MEmNVANFGgQ8AcdLQ0KDOzk7TMYCExyG2AABEXyAQ2OrgVgAjw1gVQLRR4ANAHASDQSZEQJSwqgkAgOjbuHEjd4oCUcBYFUC0UeADQBzU1tYqEomYjgEkBSZFAABEV3t7u9ra2kzHAJICY1UA0UaBDwAxFggE1NjYaDoGkDS4LRkAgOixLEsbN240HQNIGoxVAUQbBT4AxFh1dbXpCEDScDgc8ng8pmMAAJA0Nm3axMG1QBR5vV7TEQAkGQp8AIihjo4Otba2mo4BJI20tDQ5HA7TMQAASAqRSITFJkCUpaWlmY4AIMlQ4ANADHE7MhBdrGgCACB6Ghsb1dvbazoGkFQYrwKINgp8AIiR1tZWdXZ2mo4BJBUmRAAAREc4HFZdXZ3pGEBScbvdHGILIOoo8AEgBjgMDIgNCnwAAKKjrq5OoVDIdAwgqTBWBRALFPgAEANNTU3q6ekxHQNIOuwpCgDA6PX29qq+vt50DCDpMFYFEAsU+AAQZZFIRDU1NaZjAEmJVU0AAIxeTU2NLMsyHQNIOoxVAcQCBT4ARFlzczO3IwMxwqQIAIDRCQaDamlpMR0DSEqMVQHEAgU+AESRZVncjgzECIeCAQAweg0NDay+B2KEAh9ALFDgA0AUtbW1sfc9ECNMiAAAGJ1IJKLGxkbTMYCkxXgVQCxQ4ANAFDU0NJiOACQtDgUDAGB0mpubFQ6HTccAkpLD4ZDH4zEdA0ASosAHgCjp6upSe3u76RhA0vL5fKYjAACQsNjqEYgtr9crh8NhOgaAJESBDwBRwoQIiK2MjAzTEQAASFhs9QjEFmNVALFCgQ8AURAMBrVp0ybTMYCkxqQIAICRY6tHILYYqwKIFQp8AIiChoYGWZZlOgaQtNxuN3vgAwAwQmz1CMQeBT6AWKHAB4BRikQiamxsNB0DSGqZmZmmIwAAkLDY6hGIPQp8ALFCgQ8Ao9Tc3KxwOGw6BpDUmBABADAybPUIxJ7P55PL5TIdA0CSosAHgFFqamoyHQFIehT4AACMTHNzM1s9AjHGWBVALFHgA8AoBAIBdXV1mY4BJD220AEAYGRaWlpMRwCSHmNVALFEgQ8Ao8CECIg9j8cjj8djOgYAAAmnu7tb3d3dpmMASY8V+ABiiQIfAEaBAh+IPSZEAACMDGNVID4YrwKIJQp8ABihzs5O9fT0mI4BJD1uSQYAYPgsy6LAB+IgPT1dTif1GoDY4TcMAIwQEyIgPljRBADA8HV2dqq3t9d0DCDpMVYFEGsU+AAwAqxoAuKHFfgAAAxfc3Oz6QhASsjKyjIdAUCSo8AHgBHYvHmzQqGQ6RhA0svMzJTb7TYdAwCAhGJZljZt2mQ6BpASxowZYzoCgCRHgQ8AI8DqeyA+mBABADB8bW1tCofDpmMASc/n8yktLc10DABJjgIfAIYpEomotbXVdAwgJVDgAwAwfCw2AeKDsSqAeKDAB4Bham1tVSQSMR0DSHoul4v97wEAGKZwOMxiEyBOKPABxAMFPgAMU1tbm+kIQErw+/1yOBymYwAAkFDa29tlWZbpGEDSczgc8vv9pmMASAEU+AAwTJs3bzYdAUgJrGgCAGD4GKsC8ZGVlSWnk1oNQOzxmwYAhqGrq0uhUMh0DCAlUOADADB8FPhAfDBWBRAvFPgAMAxMiID48Hq98nq9pmMAAJBQenp61NPTYzoGkBKys7NNRwCQIijwAWAYKPCB+GBFEwAAw8dYFYgPj8ej9PR00zEApAgKfAAYokgkoo6ODtMxgJTAiiYAAIaPAh+IDxabAIgnCnwAGKL29nZZlmU6BpD0HA6HsrKyTMcAACChWJal9vZ20zGAlECBDyCeKPABYIhY0QTER05Ojlwul+kYAAAklM7OToXDYdMxgKTndDqVk5NjOgaAFEKBDwBDRIEPxEdubq7pCAAAJBzGqkB85OTkyOmkTgMQP/zGAYAh6O3tVSAQMB0DSHoul4v97wEAGIG2tjbTEYCUkJeXZzoCgBRDgQ8AQ8CKJiA+cnNz5XA4TMcAACChhEIhdXV1mY4BJD232y2/3286BoAUQ4EPAEPQ0dFhOgKQEtg+BwCA4evs7DQdAUgJLDYBYAIFPgAMAZMiIPbS0tKUmZlpOgYAAAmHsSoQHyw2AWACBT4A7EAkEmH/eyAOWNEEAMDIsH0OEHter5fFJgCMoMAHgB1gQgTEByuaAAAYGcarQOwxVgVgCgU+AOwAEyIg9tLT05Wenm46BgAACScYDCoYDJqOASQ9CnwAplDgA8AOsKcoEHtMiAAAGBnGqkDsZWRkyOfzmY4BIEVR4APADrACH4gth8OhvLw80zEAAEhIjFWB2MvPzzcdAUAKo8AHgO0Ih8McYAvE2NixY+XxeEzHAAAgIbECH4gtl8vFYhMARlHgA8B2sKIJiL2ioiLTEQAASFiMV4HYKigokNNJfQbAHH4DAcB2MCECYsvv9ysjI8N0DAAAElJvb69CoZDpGEDScjgcKiwsNB0DQIqjwAeA7eCW5MQ0d+5c7bPPPpo7d67pKNgBVt8DADByjFWB2MrNzWWrRwDGuU0HAAA76+7uNh0hKu69917df//923zc4/EoOztbU6dO1RFHHKFjjz1WbjcvDYgPr9erMWPGmI4BAEDCSpaxKmBXrL4HYAe0NAAwCMuy1NPTYzpG1H31AKbOzk41NTWpqalJ7777rp555hndeeedCV+q5ufna8KECcrPzzcdBdtRVFQkh8NhOgYAAAkrEAiYjgAkLbZ6BGAXFPgAMIhgMCjLskzHiLqXX355q/9fV1enBx98UAsXLtSqVat066236oYbbjCULjouuugiXXTRRaZjYDtcLtdWbyYBAIDhS8bFJoBdsNUjALtgD3wAGESqTIiKi4v1y1/+Ut/+9rclSa+++iqH9yLmCgoK5HQyDAEAYDR6e3tNRwCSks/nS/i7kgEkD1bgA8AgUqXA77Pffvvp3//+t4LBoKqqqjRt2rStPt/T06OFCxfq9ddf15o1a9TZ2ans7GztvvvuOuGEE/Td7353u8+/cuVKLViwQB999JGamprkcrlUWFio3XffXTNnztR+++034OOWLFmiF154Qf/5z3/U2tqq9PR0TZ06VTNnztRxxx034J79c+fO1aJFi3Tsscf2H2Tb0tKio446SuFwWL///e91yCGHDJr1nnvu0YMPPqiysjI9++yz23y+oqJCTzzxhD788EM1NTXJ6XSqrKxMBx10kE4//XTl5ORs85i+cwi++c1v6r777tNrr72mZ555Rp999plaW1s1Z84c/fSnP93u32GycDgc7CcKAMAohcNhhUIh0zGApFRYWMhWjwBsgwIfAAaRagX+V7cLikQiW32uqqpKl156qaqqqiRtKWAzMzPV3NysN998U2+++aZOPPFEXXXVVds8bzgc1h//+Ec9/vjj/R9LT09XOBxWZWWlKisr9cYbb2jJkiVbPa6rq0u//OUvtXTp0v6PZWZmqqOjQx999JE++ugjvfjii7r99tuHtDomNzdX+++/v95++229+OKLgxb4lmXppZdekiQdffTR23z+3nvv1QMPPND/9+Xz+RQKhfT555/r888/1/PPP6/bb799mzdAvuqPf/yjHn30UTkcDvn9/pRbiZ6XlyePx2M6BgAACS3VxqpAvLjdbrZ6BGArFPgAMIhUmxS9++67kraU86Wlpf0fb29v10UXXaSamhrtu++++slPfqLddttNaWlp6ujo0HPPPad7771XTz/9tCZMmKDTTjttq+e96667+sv7WbNm6eyzz9aECRMkbVkVv3z58m325Zeka6+9VkuXLlV5ebl++tOf6qCDDlJmZqZ6enr07rvv6g9/+IOWL1+u66+/XrfddtuQvsdjjjlGb7/9tpYuXar29nb5/f5tvuaTTz5RdXW1pG0L/Mcee0z333+/MjMz9eMf/1jHHnus8vPzFQ6H9dlnn+mOO+7Q+++/r8suu0xPPfXUgIdeVVRU6MMPP9RZZ52lH/3oRxo7dqx6e3vV3Nw8pO8h0TkcDpWUlJiOAQBAwku1sSoQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig with a global cache.\n", + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(4.5, 3.5, box_bg))\n", + "\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrowPatch( (3.4,4.4), (3.4,4.5), connectionstyle=\"arc3,rad=10.5\" ),\n", + " FancyArrowPatch( (3.3,4.2), (3.3,4.3), connectionstyle=\"arc3,rad=10.5\" ),\n", + " FancyArrowPatch( (3.2,4.0), (3.2,4.1), connectionstyle=\"arc3,rad=10.5\" ),\n", + " \n", + " FancyArrowPatch( (6.2,4.4), (6.2,4.5), connectionstyle=\"arc3,rad=-10.5\" ),\n", + " FancyArrowPatch( (6.3,4.2), (6.3,4.3), connectionstyle=\"arc3,rad=-10.5\" ),\n", + " FancyArrowPatch( (6.2,4.0), (6.2,4.1), connectionstyle=\"arc3,rad=-10.5\" ), \n", + " \n", + " FancyArrowPatch( (6.4,3.3), (6.5,3.4), connectionstyle=\"arc3,rad=-16.5\" ),\n", + " FancyArrowPatch( (6.3,3.1), (6.4,3.2), connectionstyle=\"arc3,rad=-16.5\" ),\n", + " FancyArrowPatch( (6.2,2.9), (6.3,3.0), connectionstyle=\"arc3,rad=-16.5\" ), \n", + " \n", + " FancyArrowPatch( (3.3,3.3), (3.2,3.4), connectionstyle=\"arc3,rad=12.5\" ),\n", + " FancyArrowPatch( (3.3,3.1), (3.2,3.2), connectionstyle=\"arc3,rad=12.5\" ),\n", + " FancyArrowPatch( (3.3,2.9), (3.2,3.0), connectionstyle=\"arc3,rad=12.5\" ), \n", + " \n", + " FancyArrowPatch( (3.1,1.6), (2.9,1.65), connectionstyle=\"arc3,rad=28\" ),\n", + " FancyArrowPatch( (3.3,1.5), (3.1,1.55), connectionstyle=\"arc3,rad=28\" ),\n", + " FancyArrowPatch( (3.5,1.4), (3.3,1.45), connectionstyle=\"arc3,rad=28\" ), \n", + " \n", + " FancyArrowPatch( (5.9,1.6), (6.1,1.65), connectionstyle=\"arc3,rad=-28\" ),\n", + " FancyArrowPatch( (6.1,1.5), (6.3,1.55), connectionstyle=\"arc3,rad=-28\" ),\n", + " FancyArrowPatch( (6.3,1.4), (6.5,1.45), connectionstyle=\"arc3,rad=-28\" ), \n", + " \n", + " \n", + " \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(4.24,4.3, 'Duplicate', fontsize=18)\n", + " plt.text(4.24,4.1, 'Suppression', fontsize=18)\n", + " plt.text(2.2,0.75, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'With a Global Cache',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "9ecbc4a5", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Global Duplication Suppression\n", + "---------------------------------------------\n", + "\n", + "* create a service for all the processes participating in a flow to contact.\n", + "* ask the service if a partipant has already seen a message with this checksum.\n", + "* if the answer is yet, discard the message.\n", + "* totally global is the worst case.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0a866858", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Distributed Duplicate Suppression\n", + "-------------------------------------------------\n", + "\n", + "* hash the checksum to get a modulo N\n", + "* publish the message to an array of destinations modulo the checksum\n", + "* a single subscriber on each destination will get 1/N messages.\n", + "* Every occurrence of the same message will be sent to the same subscriber.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "501d4f4d", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3xb1f3/8be89x7Ze4cMICHMEkbKDGGn7LChhZaWXaCMFiiUAm0pkLL3JmWkbBK+BEggA2iAEJLYceLE25a3LUv390dq/2JLtiVZ0r2SXs/HQ4/E17r3fCRLuud+dM7n2AzDMAQAAAAAAAAAACwlxuwAAAAAAAAAAACAOxL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AES5uXPnymazdd3mzp1rdkhhZfny5d2eP5vNpuXLl/e73y233OK2H6LXk08+6fZ6KC4uNjssmGTUqFHdXguLFi0K6n4AAPMUFxe79QGefPJJs8MCAFhInNkBAAAAAADM19bWpm+//VY//fST7Ha76urqJEmpqanKyMjQ8OHDNWrUKI0cOVJxcVxKAgAAhAK9LgAIgeLiYo0ePdqr+8bGxiopKUlJSUnKzc1VQUGBRo0apYkTJ2rGjBk64IADlJOTE+SIAUSDRYsW6amnnur3fjabTQkJCUpMTFR6eroKCgo0ePBgTZgwQZMmTdK+++6radOmKSaGyZ1AuGlsbNSLL76oJ598Ul9++aUcDke/+yQlJWnmzJmaPXu2DjnkEB1++OFKT08PQbQAAADRhwQ+AFiM0+lUU1OTmpqaVF1drY0bN2rFihVdv7fZbNpzzz11yimn6KyzztLQoUNNjDb45s6dq08++aTr54MPPtirEjWwtp4lg26++Wbdcsst5gSDfhmGoba2NrW1tam+vl6lpaVat26d/vOf/3TdJzMzU/Pnz9cvfvELHXXUUSTzYTmjRo3S1q1bu34+55xzor5MxeOPP66rrrpKtbW1Pu3X2tqqlStXauXKlfrHP/6h+Ph43X777br66quDFCkAAED04soKAMKMYRhau3atrr/+eo0ePVpnn322SkpKzA4LQJSz2+169tlndeyxx2rixIl66qmnZBiG2WEB8KCtrU0LFizQ+eef73Py3hOHw6HS0tIARAYAAICeGIEPACZJTU3VuHHjPP6uoaFBdrtddrtdHR0dvR7D4XDomWee0auvvqo77rhDV1xxRZCiBRAtxo4dq7S0NLftDodDdXV1stvtampq6vMYmzZt0qJFi7R48WI9//zzGjVqVJCiBeArp9OpE088sdsMmt1NmjRJBx10kKZMmaLc3FwlJyervr5eNTU12rBhg1avXq3vvvuuz/4JAAAAAocEPgCYZNasWV6VgikvL9eXX36pL7/8Uh988IFWrVrldp+Wlhb99re/1Zdffqmnn37ap4XlKEczMHPnzmWUMSLKo48+qrlz5/Z5H4fDofXr12vVqlVatWqVlixZIrvd7na/L774QnvttZfefPNNHXjggUGKGJ2Ki4vNDgFh4B//+IfH5P2hhx6qu+66S7Nmzer3GHa7XW+99ZZef/11LV26VO3t7cEIFYgKo0aNoi8JAOgTJXQAwOIKCws1f/58/fGPf9TKlSu1evVqnXnmmW41xCXphRde0FlnnWVClACiSXx8vPbcc09dcskleuKJJ7R9+3b985//9DjSvra2VkcddZRWr14d+kABdGO323XzzTe7bb/sssv04YcfepW8l3ateXHmmWfq9ddfV0lJiW699VYNGTIk0OECAABAJPABIOzsvffeeuaZZ/Tmm28qNzfX7fcvvvii7r33XhMiAxCt0tLS9Mtf/lLffPONTj31VLffNzY26uSTTw5IrW0A/nv77bdVX1/fbdtee+2l+++/3+PAAG8UFhbqD3/4g6655ppAhAgAAIAeSOADQJg69thjtXr1auXn57v97oYbbqCUAoCQy8jI0EsvvaSrr77a7Xdbt27V73//exOiAtDpnXfecdt26aWXKjY21oRoAAAA4A1q4ANAGBs1apReeuklzZs3T06ns2t7a2ur/vCHP+jpp58OWSwOh0M//PCD1q9fr5qaGtXX18tmsyk5OVlZWVkaMWKExowZExaLWRqGoe+++07r16/Xjh071NzcrKSkJI0bN07HH398SGOprq7WqlWrtHnzZtXX1yszM1NDhgzRjBkzNHbs2JDGEq5aWlr05ZdfaufOnaqoqFBjY6Nyc3OVn58ftOfR6XRq5cqVKioq0o4dOyRJubm5mjp1qmbNmuXTOhXh6M4779S6dev04Ycfdtv+yCOP6He/+53Gjx9vUmSB9f333+vrr7/Wzp071d7eroKCAg0bNkwHHHCAx4WAw1Hn5+FPP/2k6upqVVVVyWazKSMjQ8OHD9fkyZM1ZswYv0dvS7veL1u2bNGGDRtUWlqq+vp6OZ1OZWdnKzs7W5MmTdK0adMUExM+Y48cDodWr16tbdu2qbKyUna7XVlZWcrPz9eUKVM0depUU+LaunWr27a9997bhEisraGhQStXrtRPP/2kuro6paSkaMiQIZo6dWpI/nY7duzQV199paKiIjU1NSknJ0dDhgzR7NmzQ1KqqKSkROvWrdPWrVvV0NCg2NhYFRYWauHChUpJSfHqGKE+927btk3ffPNN12dIW1ubkpOTlZqaqiFDhmjUqFGaMGGCkpKSBtROeXm5vvnmG23dulX19fVqaWlRUlKSUlJSNGjQoK52zD4HOJ1OrV27VkVFRV2fQTk5OcrPz9eECRM0bdq0kMRRWVmplStXasuWLWpsbFRmZqYKCgo0Z84cjRw5MiQxAEBEMAAAQVdUVGRI6nY7+OCDA3b8yy+/3O34CQkJxs6dO/vd9+CDDx5QXB9//LGxcOFCIzk52S0GT7e8vDzj2GOPNf71r38ZlZWVHo/pzXH6uxUVFbkd19Pf4Yknnuj6fVVVlXH99dcbgwYN8njMkSNHuh1z2bJlbvdbtmxZv8/bzTff7Lbf7pYvX278/Oc/N2JjY3t9jHvttZfxyCOPGC6Xq9/2eup5rJtvvtnnYxiGYZxzzjn9PkeG4fl58vXW27E9cTqdxtNPP23MmzfPSEpK6vO4o0ePNq699tpeX4++qKysNC6//HIjPz+/1/ays7ON3/72t0ZFRUXXfk888YRXr+FA6/n38/b1640dO3YYCQkJbse//PLL+913oJ9Lnfx9XkeOHNltn3POOafrd21tbca9995rjB49ute/cXJysnHSSScZ33zzjV9x99V+MPbz5JNPPjF+8YtfGAUFBf2+NwsLC40zzjjDWLp0qeF0Or06/saNG40///nPxs9//nMjNTW13zYyMzONk08+2Vi5cqXXj8HTZ74/N1+88cYbxnHHHWekp6f3eczBgwcbl156qbF161afjj9QEyZMcIvlxx9/DFn7/p4zPfHn9d7fZ8K6deuMk046yeNnV+dt0qRJxl/+8hejvb3d55j7O2cuWbLE2G+//XptOyYmxjjooIOM1157zee2DaPv56y9vd148MEHjWnTpvXafn+fn6E+9+7YscO4/vrrjVGjRnn1Xk5ISDD22Wcf4/e//72xbt06r9upra01br/9dmPKlCletRMbG2vMmDHDuOKKK4wVK1b020/rr3/qi6+++sr4xS9+YeTk5PQZ45AhQ4wLL7zQ2LRpk1/t9HeeXrZsmTFv3jwjJiam1ximTJliPPvss371YwEg2pDAB4AQCHYCf8uWLR4TvX/729/63dffRFltba1xwgknDCgpsnDhQo/HHsgx+7rI7OsC6a233ur3YicUCfyOjg7jV7/6lU+P9cADDzS2bNni1d+tt+c4khL477zzjtcX2bvf0tPTjfvvv9+v58EwDOOFF14wcnNzvW4vNzfXeOeddwzDiMwEvmEYxrnnnut2/Ly8vH6TvFZN4G/atMmYOnWq13/juLg44/rrr/c6qd1f+8Hab3dff/21ccghh/j9Pp0/f36fx6+qqjL23HPPAX0WHHfccUZtbW2/jyWUCfxVq1YZ++67r8/HTkhI8Os14q/p06e7xfD++++HpG3DsHYC/09/+pMRFxfn9d9ujz32MNauXetTzL2dM5uamowTTzzRp9fOggULfE5+9/acbdy40dhjjz36bbOvz89Qn3sfeOABIy0tbUDv7ZaWln7befnll/v8Ut6b2w8//NBnG4FI4FdWVhoLFy40bDabT7HFx8cbv/71r43W1laf2uvtPN3a2mpccMEFPsXw85//3GhsbPSpfQCINuEzDxUA0KvRo0dr/vz5btvffvvtoLRXW1urgw8+WEuWLAnK8UPtpZde0oIFC1RTU2NqHIZh6Mwzz9Q///lPn/ZbsWKFDj74YG3ZsiVIkYWPe+65R8ccc4y+//57n/dtaGjQFVdcoQsuuEAdHR0+7fuvf/1Lp59+uqqrq73ep7q6WvPnzw/a+9QKfvOb37htq6qq0sqVK02IZmCKiop0wAEH6LvvvvN6n46ODt15551atGiRXC5XEKMLjJdeekn777+/li1b5vcxei6Q2lNDQ4PWrVvn9/El6c0339Q+++yj7du3D+g4gfL888/rZz/7mV+v6/b2dt1555067rjj1NjYGITouhs0aJDbtpdffjno7VrdtddeqxtvvNGnz/7169frkEMO0erVqwfUdltbm4466ii9/vrrPu33xhtv6LDDDvPpvOPJhg0btN9++2n9+vV+HyPU596bbrpJl112WdDfM48++qgWLlyoysrKoLYzUFu2bNH++++vl156SYZh+LSvw+HQ3//+d82bN2/AC823trbqqKOO0qOPPurTfu+//76OPvrobuVAAQDdRXYBVgCIIocddpj+/e9/d9u2cuVKuVyugNcN/t3vfqdvv/3WbfuECRN0+OGHa9KkScrNzVViYqIaGxtVV1enjRs3av369Vq1apXa2tr6PP6MGTO6/r9p0yY1NTV1/Zyamqpx48b1G2NCQoJXj2X9+vV64IEHupJrsbGxOvDAA3XYYYdp2LBhSk5OVmlpqdatWzfgpFN//vrXv+rFF1/s+jk9PV0LFizQ7NmzVVhYqLq6Om3YsEGvvfaatm3b1m3fbdu26dBDD9XXX3+trKysoMbpj7S0tG5/12+++abb7wsLCz0mlnbXX93f6667TnfddZfb9pycHM2bN0977723CgoKlJKSorq6On333Xd699139eOPP3a7/2OPPaasrCzdc889/T0sSdJrr72mSy65xO2iOTY2VocccojmzZunoUOHyuFwaNu2bXr77bf15ZdfStqV4D3ttNN05ZVXetVWuJkxY4by8vJUVVXVbfvnn3+u/fff36SofOdwOHTiiSeqvLy8a9uECRN04oknauzYsUpLS1NZWZk+/fRT/ec//1Fra2u3/Z955hllZ2frb3/7W6hD99rixYt1ySWXePzd0KFD9fOf/1zTpk1Tfn6+4uPjVVtbqy1btmj16tX64osv3B6zt9LS0jR79mxNnjxZ48ePV2ZmptLT09Xe3q7a2lp9//33WrZsmX744Ydu+/30009auHChPvnkk17XlEhISOj2ufP999/L4XB0/Zydna0RI0b4FXenhx56SL/85S/dtqempmrevHmaPXu2Bg8erPT0dNntdv3000/64IMPtHbt2m73X7p0qRYtWqRXX311QPH0Z//999f777/fbdsTTzyhY445JuRrvFjFK6+8orvvvrvr56SkJB111FE66KCDNHjwYDU2Nmrz5s1asmSJ2/nCbrdr3rx5WrNmjcaMGeNX+1deeaX+7//+r+vnvLw8nXjiiZo+fXrX5+d///tfvf76626J5G+//VZHHHGEvvjiC8XHx/vcdnNzs4477rhuXwLsscceOuqoozR27FhlZ2eroqJCGzdu1CuvvOLxGKE+9y5fvly333672/bMzEzNmzdPM2fO1PDhw5Wamqq2tjY1NDSopKRE3333nVatWqWysjJvnhpt3LhRl112mdu5PTk5WYceeqhmz56tkSNHKi0tTR0dHaqvr9fOnTv13Xff6auvvlJxcbFX7QxURUWFDjzwQO3cudPtd8OGDdOJJ56oyZMnKycnRxUVFfrmm2+0ZMkSty9+Pv30Ux1++OH6/PPPlZiY6Fcs5513XrcvgCdOnKijjjpKkyZNUk5Ojux2u9atW6fXXnut2/lUkv7v//5P9913n6666iq/2gaAiGfuBAAAiA7BLqFjGIaxevVqj9NS+6tt62upipKSErfpufn5+cabb77pVZyNjY3GkiVLjGOOOcY4/fTT+71/oEppGIbnv8PupYcOPvhg47vvvut1f09TrQNZQmf3erHnnnturyUinE6ncc8993isL7to0SKvnoue+wW7hE6w2u/0+uuvux0zOzvbWLx4cZ9T5F0ul/H66697rPP9xhtv9NtuZWWlx6n1++yzT5+vpU8++cQYO3Zs1/09rSERCSV0DMMwjj32WLc2TjvttD73sVoJnd3fa1lZWcaTTz7Z676lpaUeH7PNZvP6uQ11CZ3PP//cY83vMWPGGC+++GK/9Ynr6+uN559/3jjggAOMuXPn9nnfoqIiIysry7jsssuM5cuXe11H/LPPPjNmzZrlFuNf/vIXr/Y3jMCuEWAYu8rm9HzekpOTjTvvvNOw2+197rt8+fJunwGdt7///e8Diqk/3377rccSGzabzTj77LN9qgvuDyuW0Nn9/X3MMccYpaWlve7/1FNPGVlZWW7HOOSQQ7yq493zMzcxMbHr7xETE2Nce+21vZ6zWlpajOuuu85jTfFbbrml37YNw/05270PNGrUKOPtt9/udV+Hw2E4HI5u28w49x522GFu+1x55ZVGfX19v4/f5XIZq1evNq6++mojJyenzxjPP/98t3bOPPPMbuvX9OW7774zbr31VmPo0KFBK6HjcrmMo446ym3f5ORk49577zU6Ojo87tfc3GxcffXVHl9LV1xxhVePr+d5evf30aBBg4xXXnml130bGhqMs88+263trKwso7m52av2ASDakMAHgBAIRQLf4XAY8fHxbu101tjuja+JsgceeMCtjeXLl/sVc1NTU7/3CXYCv/N24okn+rUgXSAT+J236667zqu233rrLY/1ej/55JN+9+25Tzgn8MvLy43MzMxuxxs/fryxbds2r49RUlJiDBs2rNsxpkyZ0m9CxlON94MOOsirC9CysjJj/Pjxvb4OIiWBf+utt7q1MWfOnD73sVoCv/OWnp5ufPnll/3u73Q6jdNPP91t//Hjx3tV6zyUCfy2tjZjxIgRHs9RNTU1XrW7u/6e37a2Nr8TNC0tLcaRRx7ZLc7hw4e7JRV7E8gEfmtrq9vCmQUFBca3337r9THq6urcatLn5uYGvRb0Kaec0uvnjiRj7NixxqWXXmo89dRTxvfffx/Q+vxWTOB33k4//XSvHuuXX37pcZHip59+ut99PX3mdt4eeughbx628dBDD7ntGx8f79VipL19tk2cOLHPLy48MePca7fb3dZ88nbgQk/Nzc19nuPz8vK6tePtlzQ9ORwOo62trc/7+JvAf+6559z2S0pKMj788EOvYlu8eLHb/jabzfjqq6/63bfnebrzNmbMGKO4uLjf/V0ul3HEEUe47f/MM894FTsARBtq4ANAhIiLi1NOTo7b9kDXCO5ZZ338+PE6+OCD/TpWSkpKIEIasJEjR+qpp57ya/p5oM2dO1d33nmnV/c99thjdeONN7pt//vf/x7osCztb3/7m+x2e9fPKSkpevfddzVs2DCvjzF8+PBu5YukXeU23nzzzV73qaqq0vPPP99tW3Z2tl577TUlJyf322ZhYaGWLFnSa/mPSFFQUOC2zSq1y331j3/8Q7Nnz+73fjExMXriiSc0YcKEbtt/+ukn/ec//wlWeH558sknVVJS0m3bhAkTtHTpUmVnZ/t8vFGjRvX5+4SEBK/eH54kJSXpqaee6nbu2LZtm1tJmFB4+umnu5XIiImJ0ZIlSzRt2jSvj5GZmaklS5Z0K/lWXV3tc/1oXz300EN9lqLbvHmzHnroIZ1zzjmaMmWKMjMz9bOf/UzXXHON3nrrrQHXybaiCRMm6IknnvCq5ODs2bP1j3/8w237QEpkLVq0qNcSVj1dcsklWrRoUbdtDodDDz30kF9tx8XF6YUXXui3TF1PZpx7t27d6lYn/cILL/Qh6v8vOTlZNpvN4+8aGhrcSr9dcMEFvd6/L3FxcV6XdfTVvffe67bt7rvv1mGHHebV/hdddJHb684wDI/H9UZ8fLxefvlljRw5st/72mw2j+289957frUNAJGOBD4ARBBPtc/7W1DQVw0NDd1+zs3NDejxzXDbbbcpLS3N7DAk+Z58v/baa90ult944w2PtVAjUWNjox588MFu26688kq/ahEfcMABbhe9fS3U/MQTT7it53DLLbcoPz/f6zanTp2qSy+91LdAw0woPpdCYdasWTr77LO9vn9CQoL++te/um1/+OGHAxnWgO1e+7vTU089pdTUVBOi6V9BQYGOPPLIbttWrFgR0hgMw9Bf/vKXbtvOOOMMv9Z1GDNmjM4666xu24K9QHxubq6WLVum/fbbz6v7NzY26tNPP9Vf/vIXHXfccSooKNCxxx6rV155JWIWnfzrX//qU5L17LPP1qxZs7ptW7NmjV8L2iYnJ3usId+Xu+66y+2LME/nJG+cddZZ2nPPPX3ax6xzb88+qBScfmio2hmIlStXas2aNd22TZs2Tb/61a98Os6dd97p9mXtq6++6laj3hunn3669t57b6/vP2XKFO21117dtvV8TACAXUjgA0AE8TRasqWlJaBt9LyA+e9//9ttBFa4ycjI0CmnnGJ2GJKkfffd16fRm9KuEak9kz8dHR368MMPAxmaZX344Yeqq6vrtu3888/3+3jHHHNMt58/+eSTXu/7zjvvdPs5MTHRpwRvp4svvtjnfcJJKD6XQsGf0ZdHH3202xdsH330UbeFVM303XffafPmzd22HXTQQdp3331Nisg748eP7/bzypUrQ9r+N998o59++qnbtgsuuMDv4/X83PFmsfeBGjZsmD755BP9/e9/1+DBg33at6OjQ0uXLtWpp56qGTNmhP2I2SFDhujoo4/2aR+bzeZx5HfP84I3FixY4HGmUl8KCgrcFh2uqanpWiDdF/6cM80693pKogfjC7ycnBy3z/tQf1HYnw8++MBt28UXX+zVLJLdZWVl6bTTTuu2zeFwaPny5T7H5M9siH322afbzxs3bvT5GAAQDUjgA0AEcblcbtv8me7blzlz5nT7uampSb/4xS9UU1MT0HZCZc6cOX6Xcwi0nhfj3jrxxBPdtoU6oWWWnhf5Q4cO9Wrqdm9Gjx7d7efi4mK3JIW0673Wc6TlIYcc4nG0eX+mTp3qVmolkoTicykU/Hl/xsTEaMGCBd22tba26uuvvw5MUAPkKUHT8wvBUCgtLdUrr7yim266SQsXLtSRRx6p/fffX3vuuadmzpzpdnvyySe77d+zBFCw9fzciY2NdUtC+aLn505ra6t++OEHv4/nrfj4eF1++eXasmWLXnzxRc2fP9/n8+F3332nI488UjfccIPH93o4OO6443xOekqBO/eaee5PTk726ws7s86948ePdysXee211+rTTz/1u21PkpKSNH369G7b7rnnHr322msBbWcgPvvsM7dtJ510kl/HOvXUU706fl+Sk5P9+hwcO3Zst5+dTqcaGxt9Pg4ARLrILroKAFHG08VOoJPTRxxxhAYPHtytRMu7776rMWPG6IwzztDJJ5+sAw880BL15L3Rc+qumXyZdry7adOmKT4+vtuo3miZgtzzArO2tlYzZ870+3ieLhqrqqrcEvM//vij2xR7f/9+nftG6qizUHwuBdvQoUNVWFjo176ePmPWrFnjVS39YPviiy/cth1wwAEha//VV1/Vgw8+qE8++WRAyV9Pr7Fg6vm5Y7PZBjRrob293W1bz/rbwZSUlKSFCxdq4cKFam9v1+rVq7VixQqtXr1a69at0+bNm2UYRp/HuOOOO9Te3u5WWigc+PvZnZeXp+HDh2vbtm1d2/w59/rbfm+fLb6YPn26YmNjfW7brHNvTEyMzj77bN1///1d26qrq/Wzn/1M8+bN05lnnqmjjz5aeXl5fsfS6dxzz9UVV1zR9XNra6tOPvlk7bvvvjrnnHN07LHH+lTvP9DWrl3b7edhw4Zp0KBBfh1r7733VkxMTLfP4Z7H78/IkSP96vtnZma6bbPb7ZYpbQkAVkECHwAiiKeF5TIyMgLaRnJysh544AGdfPLJ3S7o7Xa7HnzwQT344INKSUnRfvvtpzlz5mjOnDk66KCD/FoMMRR8nbYeTBMnTvRrv8TERI0aNapbSYeKiopAhWVpPRdDbW5u1jfffBPQNqqrq90WfPT0/Pr795OkSZMm+b2v1YXicynYAv23tcr7s2eN49jYWE2ePDno7e7YsUNnnXWWPv7444AcL9Rl3Hp+7nR0dATlc8cMCQkJ2n///bvV86+vr9dnn32mjz76SK+88kqvMx7uuece/exnP9P8+fNDFW5ADPT9vXsCv7KyUoZheD3LKC4uzm0EsrfGjBmjhISEbl8A+frZ4m8fyKxzryTdeOONevPNN7Vly5Zu2z/44AN98MEHstlsmjp1qvbff3/Nnj1bBx10kF9/40suuUTPP/+8W1milStXauXKlbr00ks1btw4HXjggZo1a5YOPPBATZ8+PSQzzAzDcJv5OpDP7rS0NA0fPlxbt27t2ubrl4g9Z0Z4y1PS3ypl5gDASiihAwARor293WOibPjw4QFv68QTT9Szzz7b6yKHzc3N+uijj3THHXdowYIFysvL06xZs3TXXXd1u9C1AislEj2NQvJ331CPSDVLKEo3earX7un5DeTfL5J4WggvGJ9LwRTov61V3p893z9ZWVlBTz6VlpZq7ty5AUveS7sS6KEUiuS6ldaJyMjI0FFHHaV77rlHRUVFevHFF3t9D99www39jta3mkC+v10ul8cFUHuTnp4+oPdczz6Mr58t/vaBzDr3Srvq4H/44Ye9LrxrGIbWr1+vf/3rX7rwwgs1adIkDR48WJdcckmf69r0lJiYqKVLl+rwww/v9T6bNm3Sk08+qcsuu0wzZ85UXl6ezjrrLP3nP/8Jakmp+vp6twWk/Snht7ueA218/RuHy8xbAAhXJPABIEKsW7fOYxJjzJgxQWnv9NNP148//qhf/vKX/V4AulwurVmzRtddd53Gjh2rCy64QJWVlUGJy1dxcdaZjNbbFyL+7OtLAiFcNTc3B32hx954en4D+feLJJ4WVQzW51KwBPpva5X3Z319fbefQ1GyYNGiRW4LwErSzJkzdf3112vJkiVau3atysrKVF9fr/b2dhmG0e128803Bz3Ovnj6sjxaxMTEaOHChfrmm288lnD573//q1WrVpkQmf/MfH8P9LN/oOd+f/pAZp57O40ePVpffvmlFi9e7NXo+rKyMi1evFhz587V7NmztWzZMq/aycvL0/vvv6+XXnrJq1JHNTU1evbZZ3XMMcdoypQpQauZH+g+iKf9rXKeAgDsQgIfACKEp1rGmZmZfk/N9sbQoUP1z3/+U2VlZXr99dd12WWXacaMGX3WU3U4HHrsscc0ffp0n+trRrqmpqaA7Zuenj7QcCwvKSnJbeHB448/3i3ZN9Db3Llz3dr29PwG8u8XKQzD8JjMs9LaE94I9N/WKu/Pnl++BnvhwKVLl+rDDz/stq2goEDvvvuu1q1bpzvuuEPHH3+89txzTxUWFio9Pd3jqE6zR6f3XMNh5syZAf/cWbRokTkPzkvZ2dl67bXXlJCQ4Pa7jz76yISI/Gfm+3ugn/1mnPvNPPfuLi4uThdddJE2bNig1atX609/+pN+/vOf9zuoZPXq1TrssMN0++23e/V4bTabTj31VK1evVrff/+97rnnHs2fP1+5ubl97vfjjz/q5JNP1qWXXhrwWSmB7oN42t8q5ykAwC4k8AEgQvRMikjSfvvtF5JanMnJyTrhhBP0j3/8Q19//bXq6ur0wQcf6Oabb9acOXPcLvSkXaOhjjnmGNPq/FrRQOo499x3oFOpvWVmndKYmBi3x1lUVBSStj09v4H8+0WKNWvWeBytHKqFUgP1+gz03zZU78/+9ExA1dXVBbX8yQsvvNDt59jYWL311ls64ogjfDpOKMp39KXnApmh+tyxmlGjRum4445z2/7jjz+GpH0rvr9jYmJ8Snw2NDQM6D3XcxZNKD5bzDz39mbvvffWDTfcoPfee0+1tbX69ttv9c9//lMnn3yyx4S+YRi68cYb9dxzz/nUzuTJk3XllVfqzTffVFVVlTZu3KjHHntMZ511Vq8L5z788MO64447/HpcvcnIyHAbLDPQ0mw99/e3pj0AIDhI4ANABPjpp5/0zjvvuG03ayG5tLQ0HX744brlllu0cuVKbd26Vddff72SkpK63a+srEx33323KTFa0caNG/3ar729XcXFxd229bcwXc9p8/4mQsz+AqawsLDbzxs3bgzJ1H5Pz+9AklYbNmwYSDiW9be//c1tW2FhofbZZ58+9+s56trs16e/703J8+vCKotnDxo0qNvPTqdT33//fdDa++CDD7r9fOSRR/b7WvCk5+KVodbzc8dut/e6sGuk8/T362vxy0AuWBmoL3IC+f7Oz8/3aeBER0eH36/noqKibgvYSqH7bDHr3OuNmJgYTZs2Tb/85S/1yiuvqKKiQs8//7wmTJjgdt9rr712QGtojB8/Xuedd56efvpplZWV6a233tLs2bPd7nfHHXf4vChsX2w2m9sXsD/88IPfx2tqanL7DOvtCwkAgDlI4ANABLjvvvvcFstKTEzUKaecYlJE3Q0bNkx33HGH3n//fbcRQ8GqDxqO1qxZ49d+3377rVsCpL9arT1HpPUcxeetTZs2+bVfoPRMHrW0tGj58uVBb3fixIlu9cL9/fsNdF+r2r59u1566SW37WeccUa/CS6rvT63b9+uiooKv/b19Lf1ppZyKOy3335u2z777LOgtNXe3u72HB500EE+H8fpdHpcVyGUPCWtPX2JHg08LQDbV111T6Oh/Xl/b9++Xa2trT7v54m/n79VVVVuSU9/3tv+tm/mZ4tZ515/JCYm6rTTTtOaNWvcFr4tLS3VypUrA9JObGysjj32WH3++ec66qijuv2uublZ7777bkDa6dSzFN327ds9LhrvjTVr1rhdR1jlPAUA2IUEPgCEuQ8++ECLFy92237GGWcoPz/fhIh6d9BBB7nNCti8ebOam5t73adnIsDpdAYlNiv497//7dd+r7/+utu2fffdt899ek5/92cEYGlpqTZv3uzzfpLcvsjx9+86b948t23PPvusX8fyRUxMjNsou2XLlvk1hf37778f0AhQK3I6nTr77LPdvliKi4vTZZdd1u/+PV+fxcXFfpWZ+OSTT3zepzf+vD9dLpfeeOONbtuSkpI0c+bMwAQ1QJ5qTD/zzDNBacvT6FN/SjT85z//8btWf6DOJ2Z97liRp4Rhz9HZu/NU4sWf808g39tvvvmmW/LSG/6cez0J5bk/UMLxPZCWluZxAexvv/02oO3ExcXpzjvvDHo7+++/v9u2V1991a9jvfLKK14dHwBgHhL4ABDGtmzZotNOO83twjMlJUW33nqrSVH1bdKkSW7b+qo/27OWbLAXWTTTF198oe+++86nfdra2twSbnFxcTr88MP73G/ixIndfl69erXPCYx//etfPt1/d4H6ux5xxBFupZleeOGFkNRg7jnCztPfwhsDeR6t6uqrr9ayZcvctv/yl7/U6NGj+92/5+uzsbHR59Iuy5YtC+gMkUcffdTnfd555x1t376927bDDjvMYxkRM0yePNntuV6xYoXHRdEHKjU11W2bPyUl7r33Xr9jCNTnzpw5c9yS1CtWrPC4Fk2k+/jjj922jR07ttf7Dx061G32kj8zKgL5uVlaWurXDIrHHnvMbVvP84I3/v3vf6uystKnfSorK92+HMzJyfGrJJU/zDz3DoSvfVArt+Np7ZB//etfPvfl7Ha7nn/++W7b4uPjdcghhwwoPgBAYJHAB4Aw1Vln01ON57vuukvDhg0zIar+7dy5s9vPNputzzqb2dnZ3X4uKioK6iKLZvvNb37j0/3vvvtutwThggULNHjw4D736zk1uqKiQh999JHX7ZaUlOjvf/+794H20PPv6m8N4Ly8PF100UXdtjmdTp1++ulqaWnxOz5vnHvuuUpMTOy27dZbb/Wp7vr333+vBx98MNChmcZut+uUU07Rfffd5/a7MWPG6LbbbvPqOJ6m7vdMMPTF4XDouuuu8/r+3vjqq698+oLG4XDoqquuctt+ySWXBDKsAbv22mvdti1atEhNTU0BbSczM1MpKSndtr3//vs+HePRRx8dUJmOQH3uxMXFeXzezj//fJ8TsaH24osv+l1mo6fPPvvM40j4Y489ttd9YmJi3GagLF261Kfk5ptvvqn/+7//8/r+3rjqqqt8qsX/zDPPuH3xsNdee2nWrFk+t93S0uLz59V1113nNnvR0zkpWMw89w5Ezz6opKDMVg1FO/vss4/b6+3bb7/Vww8/7NNxbrjhBrf1JE499VTLrNUCANiFBD4AhJnVq1frrLPO0nHHHedxAbdzzjnHqxIV/rrlllu0atUqv/bdtm2blixZ0m3b5MmT+xyNOm3atG4/2+12ff755361Hw4++ugj3XjjjV7d95133tEf//hHt+2//vWv+93X0yjB6667zqsERm1trU4++WS/ysV06vl3/eSTT/xOGF5//fVuo3vXrl2rE044QbW1tX4dc+vWrbr88su1fv36Xu+Tl5en008/vdu26upqnXzyyV7VZq6oqNBJJ53k9wKOVtLY2KgHH3xQM2fO9DiFPzMzU6+99prHetme7L///m73/fvf/66tW7f2u6/L5dJll10WlDrpl112mdauXdvv/QzD0Pnnn++2OPG4ceN09NFHBzyugTjzzDM1ZsyYbts2btyoY445xq/3eM8FtXd34IEHdvt5+fLl+s9//uPVcd99912vPtv60vNzZ/369dq2bZtfx7r00kvdvigvKSnRUUcd5falqrcqKyt14403ui32G0hPPvmkRo8erd/+9rd+P3Zp14KZp512mtsX6nvssYf22GOPPvftef5paWnx+rz37bff6txzz/UtWC9s2LBB559/vlcDBNauXeuxn+XrF/C7e/zxx/XII494dd9HHnlEjz/+eLdt8fHxuvTSS/1u3x9mnHvffPNNPfHEE34vmOtp4MGMGTPctn3++ef629/+poaGBr/a8bSAu6d2Bup3v/ud27arrrrK6xJTjz/+uNsgApvNpt/+9rcBiQ8AEEAGACDoioqKDEndbgcffLBX+1ZUVBhvv/22cdNNNxlz5sxxO87ut3POOcfo6OjwKbaDDz7Yp7hmzJhhSDLmzJlj3H///cbWrVu9auezzz4zxo0b5xbzHXfc0ed+K1eudNtnwoQJxscff2w4nU5vH6ZhGJ7/Dk888YRPx+hp2bJlbsdctmxZv/vdfPPNbvslJSV1/f+CCy4w6urqPO7rdDqN++67r9v9O2+LFi3yOvYpU6a47X/sscca1dXVve7z8ccfGxMnTvQYsyRj5MiRXrX95z//2a3tY445xvj++++9jn93r776qmGz2dyOOXr0aOOZZ54xHA5Hv8dobGw0XnzxReOEE04w4uLiDEnGV1991ec+FRUVRl5enlu7++23X5+P5dNPPzXGjx/fdf/k5GS3YxQVFfn6NPjsnHPO8ev163A4jHXr1hmLFy82Fi1aZGRkZPT6uZSbm2usWrXK59guvfRSt2ONGzfO+O6773rd56effjKOPvroXl+f3j6vI0eO7PW9mZ2dbTzzzDO97rtjxw5jwYIFbu3abDbj448/9uqx92z/nHPOCep+X331lZGYmOgW89ixY42XX37ZcLlcfe7f+d456KCDjLlz5/Z6v0cffdStjbS0NOOVV17pdZ+Wlhbj1ltvNRISErr28fR688aLL77ott+cOXP6fZ/35vPPP/f4vOXn5xv/+Mc/jObm5n6P0dbWZrz11lvGWWed1fU50NfzMVBHHHFEV5wxMTHGoYceajzxxBN9fu7vrra21rjzzjuN9PR0j+93bz4/SktLjdjYWLd9//CHP/T6Wd3R0WE8+uijXe3abLZurwlvX+9PPPFEn+fe4447ztixY0ev+z/77LNGdna22zEOOeSQft8nhuH+mZuYmNh17oqJiTF+//vfG62trR73bW1tNX7/+98bMTExbu3fcsst/bZtGP5/RvQm1Ofe++67z5BkFBQUGL/97W+NTz/91Ku+YGVlpXH22We7xTlhwgSP91+yZEnXZ83FF19svPfee0Z7e3u/7TQ0NBjXXHONWzuZmZl9fh4MpH+6+zmv85aammr84x//6PW5aWlpMa677jqPr6Xf/va3XrXr6/VDbzy9J0PR/wGAcNN9JScAQMisXr2614UMm5qaVFdXJ7vd7tXo3NTUVN1111361a9+FeAoe7dq1SqtWrVKV1xxhSZOnKiZM2dq2rRpys/P71qkrq6uThs3btSyZcs8jlodP358vyPW5syZoylTpnSrf71x40YdeuihSk5O1rBhw9zKMki7FjocMmTIwB5kiN1222265pprJO0qFfHyyy/r+OOP1+zZs1VQUKC6ujpt2LBBr732mkpKStz2HzlypMfSJb254447dPzxx3fb9vbbb2vs2LE6+eSTNXv2bGVnZ6u+vl6bNm3Se++9p3Xr1nXd98ADD9TIkSP13HPP+fxYzz77bN14443q6Ojo2rZ06VItXbpU2dnZKiwsdCsFMGTIkF5H65500km67bbbdNNNN3XbXlRUpLPOOktXXXWV5s6dq1mzZik/P1+pqamqr69XXV2dNm3apNWrV+vbb7/1eVRffn6+Hn74YZ1yyindRm5+8cUXmj59ug499FAdfvjhGjp0qDo6OlRSUqKlS5dq1apVXfdPS0vTlVdeaZl1Ky644AK3GtWS1NHRIbvdLrvd7vWoxIMOOkjPPfechg8f7nMc119/vZ577jnV19d3bdu0aZNmzJih+fPna+7cuRo0aJBaW1tVWlqqZcuWadmyZV2vqby8PP3mN79xe03448QTT9R3332nb775RrW1tTrrrLN0++2364QTTtC4ceOUmpqqsrIyrVixQkuXLvVYQuLyyy+3bE3hWbNm6YEHHtBFF13U7XW8efNmnXrqqRo2bJiOOOIITZs2TXl5eYqLi1NdXZ2Kioq0du1affbZZ13lPA4++OBe2zn77LN15513dlv8urGxUaeccor22msvzZ8/X+PGjVN8fLwqKiq0Zs0avf32293KUk2ZMkXz58/XXXfd5fPjXLBggXJycrrNXlu1apVmz56t9PR0DRkyxK2utyR9/fXXHo+333776V//+pcWLVrU7XmrrKzU5ZdfrptuukkHH3yw9t13XxUUFCgjI0ONjY2qq6tTcXGx1qxZo3Xr1gW8XJG3XC6XPv74Y3388cey2WyaMGGC5syZowkTJig3N1c5OTlyuVyqr6/Xli1btG7dOi1fvlzt7e0ej3fNNdd4XBi5pyFDhujyyy/X/fff3237bbfdpueee04nnXSSJk+erJSUFFVXV+u///2vli5d2u28d+211+qFF17walZOf3Y/97755pv64IMPdPTRR+vAAw/U4MGD1dTUpE2bNmnJkiVus2qkXQvzPvroo7LZbD63PWjQIB1zzDF68MEH5XK5dMcdd+iRRx7RSSedpOnTpys3N1fV1dX69ttv9frrr6uiosLtGHvvvbd+//vf+/7AA8Csc29FRYXuu+8+3XfffcrNzdVee+2lmTNnasSIEcrKylJycrKam5u1detWffXVV/rggw/cPpdtNpseeOCBPtupr6/X4sWLtXjxYqWnp2uvvfbSnnvuqdGjRysrK0upqalqbW3V9u3btXbtWr333nsey0Hdc889Sk5O9ukxeuuJJ57QzJkzu5XtaWpq0uWXX66//OUvOvHEEzV58mRlZWWpqqpKX3/9tZYsWeJxDZK99trL4wK8AAALMPf7AwCIDp5G1gTiFh8fbyxatMjYtm2b37H5OwJ/oLdhw4YZ69ev9yrGjz76yONovb5unkbvWH0EvsvlMk499VS/n8/Nmzf7HP+5557rV3uTJ082Kisr3UYTejsC3zAM45ZbbvGpTW+O/dhjj3kcde3vzduRuQ8//LDHUYj93eLi4ow333zTtBFonkbgB+I2YcIE45lnnvFqRGpfnnzySb/aT0tLM1auXOn38+pplOqWLVuMwsJCv+I588wzfZoxFOoR+J1efPFFj7NBfLn1dw5Zu3atkZKS4texhw4dahQVFXn8/PTWU0895XO7/Xn77bc9jsr29xaqEfiBvN1www0+xdHU1GTssccefrW1cOFCw+l0+vV67+0z4eqrr/YrlszMTOPLL7/0+nF7Ome2trYaBx10kF/tT58+3aiqqvK6/UCPwO8UqnNv5wj8gd5sNptx33339fp4OkfgB+LmzYj2gfZPN2/e3G1Wnz+3Aw880KipqfG6TUbgA0BoUQMfAMKMzWbTrFmzdOedd6q4uFhPPPFESBesHTRo0ICPMX/+fK1cuVJTp0716v6HHnqolixZosLCwgG3bWU2m03PPfecz4tcHnDAAfrkk0/c6lh745FHHtGFF17o0z7z5s3TihUr+lx82Bs33XSTbr/9diUkJAzoOLs777zz9MUXX+jQQw8d0HGSkpL0i1/8QiNGjPDq/hdffLGee+455eTkeN1Gdna23njjDc2fP9/fMC0lOztbZ511lv7zn/9ow4YNOvPM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vmTPgSVxcXGaPn26pk+frmuuuUYtLS1BizVcNDc3a8uWLWaH4bfU1FTl5+ebHQYA9IsR+ADgQSRdFO23335asmSJrrrqKh1++OEaPXq0EhIS1NDQoKSkJI0YMULz5s3THXfcoaefftqtExsbG6v7779fF198sUaNGqX4+HjZbDbtu++++uc//6mzzjrLq0TymDFjtG7dOv3qV79Sfn6+2tvbVVBQoNNOO03r1q3TMccc47bP+++/r+uvv14HHXSQhg8f3nWhNG7cOJ177rn66quvdMUVV7jtFxsbqwceeEArVqzQGWecoREjRsjhcCghIUFTp07V+eefr9dee82/J9RL2dnZ+vLLL3XddddpxIgRamtrU05OjubPn6/PPvtMF154Ya/7Xn311fr00091+umna/z48UpISFBjY6NSUlI0ZcoUnX322Xr88cd15JFHBvUxDERKSooKCwvNDgMAgIgUSX3VUOkvgf/11193/X/BggX9Hi85Odnj9s2bN+vyyy/X5MmTlZaWppSUFE2ePFlXXHGFSkpKPO7Tc4HZNWvW6NRTT9XgwYOVmJioMWPG6He/+51qa2v7jGnlypU6/vjjlZeXp+TkZE2cOFE33HBD16CZ/tjtdt1+++2aM2eOsrOzlZiYqOHDh+u0007TypUru923ublZGzdu1LZt2zRr1izNmjVLO3bs0Pbt23X77bfruOOO03777af58+d71Xao2Ww2jRw50vIDYgBAkmwGS9cDgJv169eH7SJMZkhNTe1WNx8DU1ZWptLSUrPD8JvNZtOkSZO6LYwMAAACp7S0VGVlZWaHEVamTZvWZx38V155RaeeeqqkXYNI5s2b53MbjzzyiH71q1/J4XBI2vWlQUxMTNcglIyMDL366qtux37yySd17rnnauTIkbrjjju0aNEiORwOZWZmqqGhQS6XS9KuUpcrV65UWlqaW9uPP/64Lrzwwq77ds5cbW9v16RJk3TRRRfpd7/7nUaOHKni4mK3/VetWqUFCxaovLxc0q4BMSkpKV2zbm02m26//XZdf/31Xcl7p9OpHTt26LjjjpO0a+bCHXfcoebmZiUlJclmsykrK6trZoOVDB48WEOGDDE7DADwCiPwAcADRjX5hjIpgeFyuVRSUhLWyXtpVy1/kvcAAAQPfVXf2Gy2fuucz549u2s0dmf9e1/8+9//1kUX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rNooy9sawl7T09Oam5szHaMm/f397H0PwHEo8AFgDV47FCwQCOjGG2/Uv/7rv+rWW2/V61//el100UUrq+WnpqZ055136k1vepOefPLJmq9zvgL/pS996QU/Nzk5qaNHj658/Hd+53ckSR/4wAd0ww036L777tPs7Ox5Myyv2n/5y19e8wTtqquuqulxz7SR51upVFZto/OjH/1IkvTkk0+qp6dnzX/S6fTK9kDrvbExMDCgjo6OLT+Heuvt7d3yGyMAAOAMVuBvHqvv7Vcul3XixAnTMWoSj8dXFhABgJOwhQ4ArMGrE6J4PK5XvepVetWrXiXpzPN89NFH9eUvf1kPPvigZmZm9P73v1933333ecv49Zxva5T+/v4NfW58fHzloN2bbrpJjz32mL761a/q9ttv1+233y7LsnTppZfqla98pf7rf/2vuuiii1Z9reXbdbdv377p/MtSqVTNj122mee77OTJk5LObDe0sLBwwWsUCoU1P+7E8j4cDqu7u9t0DAAAPMOr41U7UeDb7+TJkyqVSqZj1OR843cAMIkV+ACwhmaZEIXDYT3/+c/X3/zN36ysds9kMnr44Ydr+nrnu920lltRg8GgvvKVr+jRRx/VRz7yEb30pS9VLBbTk08+qc985jO65JJL9NnPfrZu11tWj1XitbwW5XJZknTjjTeqWq2e88+JEyf085//fOWfb33rW2t+HSceutXX18ftyAAA1Em1WmULnRokEgnTETxtfn5+1eIUN2lra1M8HjcdAwDW5LwZPgA4QDNOiH73d3935c/Hjh1b+fNymX2+12Qje1yOjIys+7nR0dGVP6+1+v3yyy/XLbfcou9973uamZnRAw88oN/8zd9UuVxeWaW/bPmg1mc+BxPO93yf+blnPt/l/eGfeOKJcx4zNzenTCZTx4SNE4vF1N7ebjoGAACeUSqVVK1WTcdwlUAgoECATQjsUqlUVm2F6SaWZbH6HoCjUeADwBq8tP/9RsVisZU/P3MrnOWVSuuVx/Pz8xsqy3/wgx9c8HMdHR0r2+esJxAI6GUve5m+/e1vKxwOq1qt6oEHHlj5/Ite9CJJ0ne/+911t5hphI08X5/Pp+c+97krH1/ee/8nP/mJjh8/vvLxUqnk2gmRdGZPflbfAwBQP804Vt2qWraHxMYdP37ctXcxd3V1cbgxAEejwAeANSxv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg),\n", + " \n", + " Circle((x+0.16, y+0.52), 0.2, fc=\"white\"),\n", + " Circle((x+0.36, y+0.72), 0.2, fc=\"white\"),\n", + " Circle((x+0.56, y+0.92), 0.2, fc=\"white\")\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(2.2,0.75, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,2.35, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,4.15, 'Subscriber', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'Distributed Duplicate Suppression',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "851b0d84", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Each process has it's own duplicate suppression (the white component within each instance.) As The number of instances grow, the duplicate suppression performance grows naturally. This meets the \"embarassingly parallel\" goal. but it does cost complexity for analysts to set up." + ] + }, + { + "cell_type": "markdown", + "id": "1a1ad83a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Observations\n", + "----------------\n", + "\n", + "* If you are going to use a global cache, it better be really good.\n", + " * need to have a scaling architecture for the cache itself.\n", + "* If you are using a distributed cache, a fair one is fine.\n", + " * scales with the data transmission naturally.\n", + " * overall performance is far less sensitive to it's performance.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "789fb938", + "metadata": {}, + "source": [ + "\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/SundewDesign.html b/Contribution/Philosophy/SundewDesign.html new file mode 100644 index 000000000..9f50229e0 --- /dev/null +++ b/Contribution/Philosophy/SundewDesign.html @@ -0,0 +1,623 @@ + + + + + + + Sundew Algorithmic Design — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sundew Algorithmic Design

+

Sundew was built because PDS was too slow for message routing, which typically had:

+
    +
  • hundreds of thousands of different products to route (order of 300,000 products in the routing table.)

  • +
  • several dozen destinations.

  • +
  • need to route 20 messages per second during the day, but during an outage, need to recover the failure quickly (say 100 messages per second, so a one hour outage would be recovered in 12 minutes or so.)

  • +
  • we were seeing peak routing speed in the teens to low 20’s. It could keep up with routine traffic, but would often fall behind during traffic spurts, and took a very long time to recover after outages.

  • +
+

Sundew, a replacement implementation of PDS, was eventually made to solve this problem. It made the following alrogithimic design changes:

+
+

Eliminate Dispatcher

+

Having a single process doing the routing limits scaling. Instead, have each receiver identify the outputs for files it receives. This means operating multiple routing engines in parallel, which gives the potential for name clashes in the destination directories (if two receivers choose to same names for different data)

+
    +
  • Make sure all file names chosen for routing are unique.

  • +
+

On systems with more cores available, having more processes able to route is a benefit. Comparing regular expressions is a cpu-bound problem. With 10 receivers, one can expect a 10-fold improvement in routing performance… eg. the routing performance of the server should scale with the number of products being received.

+

It should also be noted that while scanning directories is an i/o bound process with lots of waiting, routing is a completely cpu bound process, so combining the two doesn’t worsen the overall load of the receivers substantially.

+
+
+

Eliminate Log Locking

+

Contention for writing the combined log of all processes is performance limiting, and wastes a great deal of cpu on locks to serialize access. if each process writes its own log, there is no contention for the log file. Dropping the constraint of a single log file unlocks a great deal of performance potential.

+

From an architectural perspective, this is just a pure win, there was a hard limit on absolute performance, and it is completely eliminated.

+
+
+

Dictionary Pre-Routing

+

Even if we parallelize routing, the algorithm itself is quite intensive. for 10 destinations, with 10 regular expressions each, that means 100 regular expressions to evaluate (slightly less, because of shortcuts from exclusion masks (rejects) but the order of magnitude is about right.)

+

Added the use of a python dictionary listing product keys, and a set of destinations associated with each key. So there are now two steps to routing, the first narrows down the number of destinations to a subset… say 2/10 of destinations, and then the regular expressions are only applied on that subset.

+

The cost to route a single product through the application (take it from seeing it in a reception directory to queuing it up for sending.) can be modelled as follows: It is the regular expression comparisong that dominate the routing time, so just calculate how many regular expressions are needed, on average, to complete routing of a product.

+
    +
  • Sundew Routing cost: log(d) * s * (re/2) … log(300000)* 10 * (10/2) = 18105 = 900

    +
      +
    • each receiver only routing for a single receiver.

    • +
    • d = 300000, the size of the product dictionary (around 300000)

    • +
    • re = 10, the dictionary allows us to reduce the number of regex’s used for product

    • +
    • s is the subset of destinations chosen for a typical key (10)

    • +
    • dictionary lookup time is a log of the number of different product keys to assign.

    • +
    +
  • +
+

It is important to note that S grew substantially once Sundew was in place, we peaked at about 500 senders… with PDS, that would have meant 25000 regex comparisons per product routed, while Sundew routing algorithmic cost is independent of the number of senders extant.

+

Note that a single process does all routing in PDS, where each receiver (initial configurations had R=10) distributed routing duties to each receiver, giving 10x available algorithmic speedup, as routing is embarassingly parallel (completely perallelizable.)

+

NOTE: this algorithm only benefits “message” type routing with the dictionary. for file type routing, used in PDS traditionally, we fall back on pure regexes.

+
+
+

Write it in Python

+

Sundew was built in Python. Industry lore is that Python is much slower than C, the measurable amounts vary, but as a rule of thumb, one can attribute approximately an order of magnitude there as well. It was judged safe to use Python in this case because the application is entirely i/o bound, and thus the speed of C was not material to the problem at hand. When using python, code is much simpler to write, and much shorter. the PDS application was approximately 150,000 lines of C clode. The +Python application is 25,0000 lines.

+

Sundew also replaced another message router, called “Tandem Apps”, which was 450,000 lines of code in proprietary language. replacing 600,000 lines of code is a major project, writing 25,000 lines is a biggish script. So a 96% reduction in the source code size. This choice:

+
    +
  • made the project feasible.

  • +
  • made the algorithms in use far simpler to understand and implement.

  • +
+
+
+

Eliminate Dispatcher

+
    +
  • replace single task routing for all receivers

  • +
  • every receiver queues products for senders.

  • +
  • routing performance expands as a function of the number of receivers.

  • +
+
+
+

Dictionary Pre-Routing

+
    +
  • map bulletin names to a key, list the keys in a table (aka python dictionary) that lists senders.

  • +
  • selects a subset of senders to which their regex’s are applied.

  • +
+
+
+

Sundew Routing cost:

+
    +
  • Rp = 900 = log(d) * s * (re/2)

  • +
  • r = 10, each receiver only routing for a single receiver.

  • +
  • d = 300000, the size of the product dictionary (around 300000)

  • +
  • n = the length of each key.

  • +
  • re = 10, the dictionary allows us to reduce the number of regex’s used for product

  • +
  • s is the subset of destinations chosen for a typical key (10)

  • +
  • dictionary lookup time is a log of the number of different product keys to assign.

  • +
+
+
[102]:
+
+
+
+from math import log
+
+r=10 # number of receivers, each receiver is routing for itself alone. naturally de-composing the routing problem.
+d=300000 # number of products in the routing table, pre-existing table used by previous system.
+re=10 # number of regexes in each sender, given that pre-routing with the dictionary has occurred.
+s=10 # number of destinations selected by Pre-Routing.
+n=10 # the number of characters in each key.
+
+# cost to look up each string to find a corresponding integer is in the log(d)
+Lum=log(d,2) # map key string to a token number.
+Luk=log(d,2) # lookup the token in the dictionary.
+
+# cost to search a dictionary to find a match. (integer ops...)
+Rpr = 2*log(d,2)
+
+#convert integer ops, to RE ...
+RE=100
+
+#Client side routing cost:
+
+Rpc=s*RE*re/2
+
+RP=Rpr+Rpc
+
+print(f"total routing cost, pre product, done by each receiver: {round(Rp)} instructions")
+
+print( f"in terms of load distribution, the pre-routing falls on the receivers: {round(Rpr)} " )
+print( f"and the rest {Rpc} falls on the senders" )
+
+# for files, instead of messages.
+
+
+
+
+
+
+
+
+
+total routing cost, pre product, done by each receiver: 181946 instructions
+in terms of load distribution, the pre-routing falls on the receivers: 36
+and the rest 5000.0 falls on the senders
+
+
+
+
+

Write it in Python

+
    +
  • made the project feasible (10x smaller)

  • +
  • made the algorithm simpler to understand an implement.

  • +
  • problem i/o bound, python performance not an issue.

  • +
+
+
[98]:
+
+
+
+%matplotlib inline
+
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow
+
+def directory_polygon(x,y,box_bg,arrow1):
+   return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),
+               Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),
+               Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),
+            FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25,
+                       head_width=0.5, head_length=0.2),
+            Circle((x+2.5, y+0.6), 0.5, fc=box_bg)
+          ]
+
+
+def create_base(box_bg = '#CCCCCC',
+                arrow1 = '#88CCFF',
+                arrow2 = '#88FF88',
+                supervised=True):
+
+    fig = plt.figure(figsize=(15, 15), facecolor='w')
+    ax = plt.axes((0, 0, 1, 1),
+                 xticks=[], yticks=[], frameon=False)
+    ax.set_xlim(0, 9)
+    ax.set_ylim(0, 6)
+
+    x=0
+    y=3.6
+    patches = []
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    y=0.2
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+    y=1.8
+    patches.extend(directory_polygon(x,y,box_bg,arrow1))
+
+    len=1.8
+    patches.extend(
+        [
+          FancyArrow(3.1, 4.3, len, -0.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 4.1, len, -1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 3.8, len, -2.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          FancyArrow(3.1, 2.6, len, 1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 2.2, len, -0.9, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          FancyArrow(3.0, 1.2, len, 2.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 0.9, len, 1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+          FancyArrow(3.1, 0.7, len, 0.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),
+
+          Circle((5.8, 3.9), 0.5, fc=box_bg),
+          Circle((5.8, 2.4), 0.5, fc=box_bg),
+          Circle((5.8, 1.1), 0.5, fc=box_bg)
+        ])
+    for p in patches:
+        ax.add_patch(p)
+
+    plt.text(2.2,0.75, 'Receiver', fontsize=18)
+    plt.text(2.2,2.35, 'Receiver', fontsize=18)
+    plt.text(2.2,4.15, 'Receiver', fontsize=18)
+    plt.text(5.5,1.05, 'Sender', fontsize=18)
+    plt.text(5.5,2.35, 'Sender', fontsize=18)
+    plt.text(5.5,3.85, 'Sender', fontsize=18)
+create_base()
+plt.text(3.0, 5, 'Sundew Component Design',fontsize=36)
+plt.show()
+
+
+
+
+
+
+
+../../_images/Contribution_Philosophy_SundewDesign_7_0.png +
+
+
+
+

What difference did it make?

+

On a given linux server used to run both applications for benchmarking purposes:

+
    +
  • The original (PDS) C-code was performing 10-20 product routings per second,

  • +
  • Sundew (the python replacement) was peaking at 150 messages per second with files,

  • +
  • Sundew was file system bound, and was also a message router, capable of routing over 400 messages/second.

  • +
+

It feels safe to say it was approximately an order of magnitude improvement in performance.

+

Note:: These tests were done in 2005, using, for example, hard disks for storage. Performance on current hardware is far superior.

+
+
+

Bonus

+

The original PDS application was surrounded by scripts and code to do many kinds of name processing, and with the original built in C, this was done with shell scripts. As a happy side-effect of implementing in Python, it became feasible to implement a plugin architecture to customize processing in a much more efficient way.

+

Instead of forking of processes, as required by additional functionality in PDS, we could define routines that would be called from the python code. These routines could be written in Python, and thus save enormous amounts of overhead from the fork/reap pattern.

+

While plugin architectures can be implemented in C, they are much more daunting, and having operations people writing C is a high bar to customize processing.

+

So a great deal of peripheral, or customized processing was accellerated by the use of Python as the implementation language.

+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Philosophy/SundewDesign.ipynb b/Contribution/Philosophy/SundewDesign.ipynb new file mode 100644 index 000000000..3f0f3b34f --- /dev/null +++ b/Contribution/Philosophy/SundewDesign.ipynb @@ -0,0 +1,376 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "543de844", + "metadata": {}, + "source": [ + "Sundew Algorithmic Design\n", + "=======================\n", + "\n", + "Sundew was built because PDS was too slow for message routing, which typically had:\n", + "\n", + "* hundreds of thousands of different products to route (order of 300,000 products in the routing table.)\n", + "* several dozen destinations.\n", + "* need to route 20 messages per second during the day, but during an outage, need to recover the failure quickly (say 100 messages per second, so a one hour outage would be recovered in 12 minutes or so.)\n", + "* we were seeing peak routing speed in the teens to low 20's. It could keep up with routine traffic, but would often fall behind during traffic spurts, and took a very long time to recover after outages.\n", + "\n", + "\n", + "Sundew, a replacement implementation of PDS, was eventually made to solve this problem. It made the following alrogithimic design changes:\n", + "\n", + "\n", + "Eliminate Dispatcher\n", + "-----------------------------\n", + "\n", + "Having a single process doing the routing limits scaling.\n", + "Instead, have each receiver identify the outputs for files it receives.\n", + "This means operating multiple routing engines in parallel, which gives the potential for name clashes in the destination directories (if two receivers choose to same names for different data) \n", + "\n", + "* Make sure all file names chosen for routing are unique.\n", + "\n", + "On systems with more cores available, having more processes able to route is a benefit.\n", + "Comparing regular expressions is a cpu-bound problem. With 10 receivers, one can expect a 10-fold improvement in routing performance... eg. the routing performance of the server should scale with the number of products being received.\n", + "\n", + "It should also be noted that while scanning directories is an i/o bound process with lots of waiting, routing is a completely cpu bound process, so combining the two doesn't worsen the overall load of the receivers substantially. \n", + "\n", + "\n", + "\n", + "Eliminate Log Locking\n", + "--------------------------------\n", + "\n", + "Contention for writing the combined log of all processes is performance limiting, and wastes a great deal of cpu on locks to serialize access. if each process writes its own log, there is no contention for the log file. Dropping the constraint of a single log file unlocks a great deal of performance potential.\n", + "\n", + "From an architectural perspective, this is just a pure win, there was a hard limit on absolute performance, and it is completely eliminated.\n", + "\n", + "\n", + "\n", + "Dictionary Pre-Routing\n", + "---------------------------------\n", + "\n", + "Even if we parallelize routing, the algorithm itself is quite intensive. for 10 destinations, with 10 regular expressions each, that means 100 regular expressions\n", + "to evaluate (slightly less, because of shortcuts from exclusion masks (rejects) but the order of magnitude is about right.)\n", + "\n", + "Added the use of a python dictionary listing product keys, and a set of destinations associated with each key. So there are now two steps to routing, the first narrows down the number of destinations to a subset... say 2/10 of destinations, and then the regular expressions are only applied on that subset.\n", + "\n", + "The cost to route a single product through the application (take it from seeing it in a reception directory to queuing it up for sending.) can be modelled as follows:\n", + "It is the regular expression comparisong that dominate the routing time, so \n", + "just calculate how many regular expressions are needed, on average, to \n", + "complete routing of a product.\n", + "\n", + "\n", + "\n", + "* Sundew Routing cost: log(d) * s * (re/2) ... log(300000)* 10 * (10/2) = 18*10*5 = 900 \n", + "\n", + " * each receiver only routing for a single receiver.\n", + " * d = 300000, the size of the product dictionary (around 300000) \n", + " * re = 10, the dictionary allows us to reduce the number of regex's used for product \n", + " * s is the subset of destinations chosen for a typical key (10)\n", + " * dictionary lookup time is a log of the number of different product keys to assign.\n", + " \n", + "It is important to note that S grew substantially once Sundew was in place,\n", + "we peaked at about 500 senders... with PDS, that would have meant 25000 regex comparisons per product routed, while Sundew routing algorithmic cost is independent of the number of senders extant.\n", + "\n", + "Note that a single process does all routing in PDS, where each receiver (initial configurations had R=10) distributed routing duties to each receiver, giving 10x available algorithmic speedup, as routing is embarassingly parallel (completely perallelizable.)\n", + "\n", + "**NOTE:** this algorithm only benefits \"message\" type routing with the dictionary. for file type routing, used in PDS traditionally, we fall back on pure regexes.\n", + "\n", + "\n", + "\n", + "\n", + "Write it in Python\n", + "------------------------\n", + "\n", + "Sundew was built in Python. Industry lore is that Python is much slower than C, the measurable amounts vary, but as a rule of thumb, one can attribute approximately an order of magnitude there as well. It was judged safe to use Python in this case because the application is entirely i/o bound, and thus the speed of C was not material to the problem at hand. When using python, code is much simpler to write, and much shorter. the PDS application was approximately 150,000 lines of C clode. The Python application is 25,0000 lines.\n", + "\n", + "Sundew also replaced another message router, called \"Tandem Apps\", which was 450,000 lines of code in proprietary language. replacing 600,000 lines of code is a major project, writing 25,000 lines is a biggish script. So a 96% reduction in the source code size.\n", + "This choice:\n", + "\n", + "* made the project feasible.\n", + "\n", + "* made the algorithms in use far simpler to understand and implement.\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "ac0019b8", + "metadata": {}, + "source": [ + "\n", + "\n", + "Eliminate Dispatcher\n", + "-----------------------------\n", + "\n", + "* replace single task routing for all receivers\n", + "* every receiver queues products for senders.\n", + "* routing performance expands as a function of the number of receivers.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7b8c8799", + "metadata": {}, + "source": [ + "\n", + "Dictionary Pre-Routing\n", + "---------------------------------\n", + "\n", + "* map bulletin names to a key, list the keys in a table (aka python dictionary) that lists senders.\n", + "* selects a subset of senders to which their regex's are applied.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1581cfcc", + "metadata": {}, + "source": [ + "Sundew Routing cost:\n", + "--------------------------------\n", + "\n", + "* Rp = 900 = log(d) * s * (re/2)\n", + "* r = 10, each receiver only routing for a single receiver.\n", + "* d = 300000, the size of the product dictionary (around 300000) \n", + "* n = the length of each key.\n", + "* re = 10, the dictionary allows us to reduce the number of regex's used for product \n", + "* s is the subset of destinations chosen for a typical key (10)\n", + "* dictionary lookup time is a log of the number of different product keys to assign.\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "e1210444", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total routing cost, pre product, done by each receiver: 181946 instructions\n", + "in terms of load distribution, the pre-routing falls on the receivers: 36 \n", + "and the rest 5000.0 falls on the senders\n" + ] + } + ], + "source": [ + "from math import log\n", + "\n", + "r=10 # number of receivers, each receiver is routing for itself alone. naturally de-composing the routing problem.\n", + "d=300000 # number of products in the routing table, pre-existing table used by previous system.\n", + "re=10 # number of regexes in each sender, given that pre-routing with the dictionary has occurred.\n", + "s=10 # number of destinations selected by Pre-Routing.\n", + "n=10 # the number of characters in each key.\n", + "\n", + "# cost to look up each string to find a corresponding integer is in the log(d)\n", + "Lum=log(d,2) # map key string to a token number.\n", + "Luk=log(d,2) # lookup the token in the dictionary.\n", + "\n", + "# cost to search a dictionary to find a match. (integer ops...)\n", + "Rpr = 2*log(d,2)\n", + "\n", + "#convert integer ops, to RE ... \n", + "RE=100\n", + "\n", + "#Client side routing cost:\n", + "\n", + "Rpc=s*RE*re/2\n", + "\n", + "RP=Rpr+Rpc\n", + "\n", + "print(f\"total routing cost, pre product, done by each receiver: {round(Rp)} instructions\")\n", + "\n", + "print( f\"in terms of load distribution, the pre-routing falls on the receivers: {round(Rpr)} \" )\n", + "print( f\"and the rest {Rpc} falls on the senders\" )\n", + "\n", + "# for files, instead of messages.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c61e039a", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5daf576f", + "metadata": {}, + "source": [ + "Write it in Python\n", + "------------------------\n", + "\n", + "* made the project feasible (10x smaller)\n", + "\n", + "* made the algorithm simpler to understand an implement.\n", + "\n", + "* problem i/o bound, python performance not an issue.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "570d1d3e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gUZfv28XNDQhICJJTQkSIixQJIl14VsAEq5VFRUVHBguURC2JBVFRUsKKCPCoiIlKlI9KkF6UpSA2BFEjvm3n/8Je8bHaT7CabzCT5fo7DQzK7M3NtMjubnHPPddsMwzAEAAAAAAAAAAAsxcfsAgAAAAAAAAAAgDMCfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AACAEmz27Nmy2WwO/504ccLssgAAQAFNmjTJ6bMdAFB2+ZpdAAAAsKaMjAwdPHhQhw4dUkxMjGJiYmS32xUUFKSKFSuqXr16atiwoRo2bCh/f3+zywVKrOPHj+uvv/7SqVOnFBsbq5SUFAUFBalKlSqqUqWKmjVrpqZNmxLgAAAAAGUQAT4AAMiWmpqqhQsX6quvvtKmTZuUnJyc7zp+fn666qqr1K5dO3Xv3l39+vVT9erVi6FaoGRKT0/XokWL9OOPP2rdunWKjIzMd53g4GC1bdtWt9xyi4YNG6bQ0NBiqBRAWdajRw9t2LAh3+fZbDb5+/vL399fwcHBqlGjhurUqaOmTZuqRYsW6tSpk5o1a1YMFQMAUDoR4AMAAEnS4sWL9eijj+rMmTMerZeenq49e/Zoz549+vzzz+Xj46NHH31UH374YRFVCpRM6enpmjFjht555x2dPXvWo3VjY2O1du1arV27VuPHj9fAgQM1adIktWrVqmiKBVDsRo0apa+//jr76wYNGpSIlmiGYSglJUUpKSmKjY3VqVOnnJ5To0YN3XbbbRoxYoS6detmQpUAAJRc9MAHAKCMMwxDjzzyiG655RaPw3tXMjMzXf7xDpRlu3bt0tVXX63x48d7HN7nlJGRoUWLFqlNmza66667FB0d7aUqAaBoRERE6LPPPlP37t3Vpk0bLVmyxOySAAAoMRiBDwBAGTdmzBh9/vnnLh+77LLL1KtXL7Vs2VKhoaEKCgpSQkKCLl68qL///lu7du3Svn37lJqaWsxVAyXH//73P40ePVppaWkuH69YsaK6d++uVq1aKTQ0VKGhoSpXrpzi4uJ0/Phx7du3T5s2bVJcXJzDeoZh6JtvvtH999+vHj16FMMrAVDWNW/eXOXLl3danpaWposXLyo2Njbf9nt79uzRzTffrEGDBmnWrFm03QMAIB8E+AAAlGE///yzy/C+TZs2evvtt9WrV698J85MSkrSihUrtHDhQi1cuFCJiYlFVS5Q4nz++ecaM2aMDMNweqxjx456+eWX1atXL5eB2KXS09O1cuVKffLJJ/rll19cbg8Aitry5cvVsGHDPJ+TkpKivXv3avv27dqyZYsWLVqklJQUp+ctXbpUbdq00erVq3XllVcWUcUl06RJkzRp0iSzywAAWAQtdAAAKKMMw9CTTz7ptHzw4MHasmWLevfunW94L0kVKlTQ4MGD9b///U9hYWGaNm2arrjiiqIoGShRVqxYoUceecQpbK9UqZJ++OEHbd26VTfccEO+4b3072TRgwYN0rJly7Rz50516dKlqMoGgEIJCAhQx44d9dhjj+n777/XmTNn9Oabb7qcfPv06dPq0aOHjh8/bkKlAACUDAT4AACUUVu2bHGaHK9u3bqaPXu2/P39C7TN4OBgPfHEE5o6daoXKgRKroiICN19992y2+0Oy2vVqqUNGzbo9ttvL/C227Rpo99++01Tp06Vn59fYUsFgCJVrVo1/fe//9W+ffvUs2dPp8fPnTun22+/Pdc2YwAAlHUE+AAAlFG//PKL07JRo0apUqVKJlQDlC5PP/20IiMjHZaVL19eK1asUOvWrQu9fZvNpqefflorVqxQcHBwobcHAEWtdu3aWrNmje68806nx3bt2qVp06aZUBUAANZHD3wAAMqokydPOi277rrrTKjE2jIzM7V792798ccfioiIkM1mU/Xq1dW4cWN17tzZrfYnnrDb7fr99991/PhxnT17VtK/oxdbtmyptm3byte3eH59O3r0qA4ePKjIyEhFRkbK399f1atXV7169dSxY0cFBgYWSx0l0Z9//qlvv/3Waflrr72ma6+91qv76tWrV4HXTUtL0/bt23XmzBlFREQoMTFR1apVU40aNdSyZctia4X1999/a/fu3Tpz5oxSUlJUuXJlNW/eXJ06dVJQUJBb2zAMQ/v27dO+ffsUEREhu92umjVrqkWLFmrfvr1b7cAKKiMjQzt37tSBAwcUFRUlHx8f1a5dW40aNVLHjh1Vrlw5r+/Tbrdr9+7dOn78uCIjIxUbG6uqVasqNDRUTZs21dVXX+31fboSGRmp33//Xf/8848SEhIUHBysGjVqqEOHDmrQoEGR7DMzM1N79uzRiRMnFBkZqQsXLqhy5coKDQ1VkyZN1Lp1a/n4FO04tfT0dG3fvl0HDx5UVFSU/Pz8sr/37du3L5KfeWnh4+OjL7/8UgcOHNCff/7p8NjkyZM1ZsyYQl+UjIqK0s6dOxUREaHIyEjZ7XZVr15dNWvWVMeOHVWtWrVCbf9Sdrtdf//9t/744w9FRkYqLi5OdrtdFSpUUOXKlVW/fn01atRIl19+eZEfl3lJTk7W5s2bdebMGZ07d07lypVTrVq1dPXVV+vaa68t0nOkK3///bd27dqlsLAwpaamqlq1aqpTp466dOmiKlWqFGstAFAiGAAAoEzq16+fIcnhv5UrVxbb/o8fP+60/1mzZhVoW927d3fYTvfu3fNdZ/369U77X79+ffbjsbGxxsSJE42aNWs6PS/rv6CgIGPUqFHGqVOnClT3pSIjI41x48YZoaGhue6vSpUqxpNPPmlERERkrzdr1iyn5x0/frxANfzzzz/G2LFjjcaNG+dagyQjICDA6Nu3r7F8+XK3t/3pp586bScsLMytddPT041KlSo5rf/ggw+6vf+33nrLYV1fX18jNjbW7fU9MXr0aKdar7jiCsNutxfJ/jy1evVq4+abbzYqVqyY58+5cePGxvjx443w8PAC7adBgwYO27vnnnuyH8vIyDA+/fRTo1mzZrnuv2LFisa4ceOM6OjoXPcRFxdnvPLKK0bdunVz3U7NmjWNd955x0hPT/eo/vzOUefOnTOeeOIJIyQkJNd916hRwxg7dqzDe7YwduzYYQwbNsyoWrVqnj+7OnXqGA888IBx9OjRAu0nv3Pq+vXrjb59+xo+Pj651tCiRQvjm2++MTIzM73wyg3jt99+M+688858X3vVqlWN//znP8aBAwc83kd+59Pw8HDj8ccfN4KDg3Pdf0hIiPHEE08YUVFRbu0z5/ukIP9d+tnlTTmPg8J8vuS0a9cul6/l3XffLdD2YmNjjcmTJxtt27Y1bDZbrt8rm81mtGnTxvjoo4+M1NTUAte/c+dO4/7778/zWLj0v8qVKxt9+vQxpk2bZpw+fTrf7b/88stO2yiIP//807j99tuNoKCgPM8Xr776qpGQkFDo/edc5+WXX85+zG63G1999ZVx1VVX5VpLuXLljN69extbt24t0OsFgNKKAB8AgDLq5ptvdvrD6fPPPy+2/Vs5wN+wYUOegWDO/wIDA41FixYVqHbDMIy5c+ca1apVc3t/1apVM3755RfDMLwT4MfGxhqPPvqo4efn53Fw1KVLF+PkyZP57uPo0aNO63799ddu1bd582aX+27UqJHbr7Fv374O63bq1MntdT2RnJzsMhgvaCjlTceOHXN54S6//4KCgoxXX33V4wsQuQX4YWFhRocOHdzef7169Yx9+/Y5bX/z5s3GZZdd5vZ2OnToYFy8eNHt+vM6Ry1btizfIDnne3bu3Lkeff8uFRkZadx55515BpOu/vPz8zMee+wxIyUlxaP95XZOTUlJcXmBKq//+vXr5xAMeurIkSPGjTfe6PFx6+PjY9x///1GcnKy2/vK63z6448/5nmxxtXP3J0QsqwG+IZhGD179nTafuvWrT3aRmZmpvH+++979Bma9V+DBg2MtWvXerS/lJQU48EHH8zz4pU756L8FDbAt9vtxvPPP+/R53qDBg2MnTt3Fmr/OdfJCvBPnz5tdOrUyaPv0/PPP+/RawaA0owWOgWwa9cus0uAxdByAkBJVKtWLadlP/zwgx544AETqrGOpUuXavDgwUpPT3d7neTkZA0ZMkRLlizRDTfc4NH+Pv/8c40ZM0aGYbi9TnR0tG666SYtXLjQo325cvLkSQ0aNMiplYG7Nm3apPbt2+vnn39Wx44dc33e5ZdfrkaNGun48ePZy9asWaO77747332sWbPG5fLjx4/rn3/+UePGjfNcPzU1VZs2bXJY1qdPn3z3WxDr1q1TQkKCw7Ly5ctr1KhRRbI/d+3atUsDBgxQRESEx+smJiZq4sSJ2rdvn7755hsFBAQUuI6zZ8/q+uuvd5pAOy9nzpxRv379tGPHDtWvX1+StGrVKt16661KTk52ezvbtm3TDTfcoE2bNhWqFdWSJUs0ePBgZWRkuL1OdHS0RowYobi4OD344IMe7e+ff/7RDTfcoL///tvTUpWenq4PP/xQe/bs0aJFiwrVmiIlJUUDBgzQ+vXrPVpv1apVGjBggNatW+dxa5m1a9dq6NChiomJ8Wg96d9WO19++aX++OMPLV68WDVr1vR4G1k+/fRTPfLIIx6fp/v06aNNmzapVatWBd53afb44487HU979uxRWFiY6tatm+/6KSkpuueee/TDDz8UaP8nT55U//799fHHH7v1u09aWpoGDhyotWvXFmh/xSUzM1OjRo3S//73P4/WO3nypLp3765ff/3Vq/X8888/6tatm8LCwjxa74033pDNZtPrr7/u1XoAoCQiwAcAoIzq3LmzPv/8c4dla9as0fTp0zVu3DiTqjLX3r17NWHChOzwPjAwUL1791a3bt1Uq1Yt+fr66vTp01q1apXTH/AZGRkaPXq0Dhw44Hb/3gULFrgM78uVK6eePXuqb9++qlu3rtLT03X69GktXbpU27dvz97f8OHD9dRTTxX49Z48eVIdOnTQ+fPnnR5r3769rr/+el155ZWqUqWK0tLSFB4eri1btuiXX35Rampq9nPPnz+vgQMHavfu3Xn2ve7du7e++OKL7K/dDUFyC/AlafXq1XrooYfyXH/z5s1OQW9RBfiuws1WrVqpatWqRbI/dxw6dEg9evRwurAgSVdccYVuu+02NWnSRJUrV9a5c+e0fft2LV682On5CxYsUGpqqhYvXlygfskZGRm69dZbs8N7m82mrl27qm/fvqpfv778/f115swZrVixwunYOH/+vMaMGaNly5bp0KFDGjp0aPbPNDAwUP369ct+n9rtdh07dkw//vijDhw44LCdbdu2adq0aXrmmWc8rl/696LRuHHjssN7m82mzp07a8CAAapXr55sNptOnz6tX375RZs3b3Z4bxuGoTFjxqhatWoaMmSIW/uLiIhQly5dFB4e7vRYvXr1NHjwYDVv3lxVq1ZVRESE9u3bp4ULFyo6OtrhuRs3blSfPn20ZcsW+fv7F+i133fffQ7H95VXXqkbb7xRzZo1U9WqVRUbG6s9e/ZowYIFTueU3377TdOmTdPTTz/t9v6WLFmiIUOGOF1MLV++vHr16qUOHTqofv36Cg4OVkJCgk6cOKF169Zp48aNDs/fvn27br31Vv3222/y8/Pz+HX/8ssvGjt2bPbPMjg4WP369VPnzp1Vo0YNZWZm6sSJE1q6dKm2bdvmsG5iYqLuvfde7dixI9eLRi1atFBISIgk6dSpU7p48WL2Y35+fmrRokW+NVasWNHj12UFPXv2lI+PjzIzMx2Wb926VUOHDs1z3fT0dPXr18/p5y39e8G4Z8+euuaaa1S1alX5+voqKipKO3bs0PLlyx0mGM/IyNBDDz2kWrVq6aabbspzn1OmTHH5uVW/fn3169dPLVq0UM2aNRUQEKCkpCTFxcXp6NGj+vPPP7V161YlJibmuX1vGT9+vMvwPigoSAMGDFDnzp1Vq1YtJScn6+TJk1q2bJl27twp6d9j9tZbb9Xtt9/ulVri4+N14403Zof3WefMPn366LLLLlPFihUVGRmpzZs3a+HChUpJSXFYf8qUKbrpppvUoUMHr9QDACWVzfBkGAEkMQIfzhiBD6AkioyMVIMGDVyOYL3pppv07LPPqkuXLkW2/xMnTqhRo0YOy2bNmlWgkco9evTQhg0bsr92ZwTZr7/+qp49ezosCwgIyP7j8a677tJbb72l2rVru1x/w4YNGjx4sC5cuOCwfMqUKXruuefyrTkqKkotWrRwCBKkf4PzWbNm5Rra/Pbbb7rvvvt07NgxSf+Glzl/hsePH1fDhg3z3H9aWpq6dOmiHTt2OCwfNGiQ3n77bTVv3jzXdc+dO6dnnnlG33zzjcPydu3aaevWrbmOsp03b56GDRvmsOzAgQN5BlSJiYmqUqVKdohns9kcQtGhQ4dq/vz5ua4vSS+88ILeeOON7K+DgoJ08eLFAoV5+endu7fWrVvnsGzs2LGaPn261/fljtTUVHXo0EH79u1zWF61alV98MEH+s9//uNyvYsXL2r8+PGaPXu202Pvv/++Hn/88Xz33bBhQ4fJsv39/bMv/LRu3VozZ87M9XeolStXasiQIU6B12+//abHH39ce/bskSQNGzZM7733nsv3aWZmpqZMmaIXX3zRYXlwcLDCw8PznYjZ1Tnq0nNEs2bNNHv27FyDpe3bt2vUqFE6dOiQw/LQ0FAdPHhQ1atXz3P/hmFo4MCB+uWXXxyWBwYGavLkyXrsscdcvteSk5P18ssv691333UKRp944glNmzYtz/1KzufUS193rVq1NH369FwD1oSEBD366KOaM2eOw/KQkBCdPXvWrQmwjx8/rjZt2jiMvPf19dWTTz6pZ555RqGhobmuu3fvXo0ePdrpb7bx48fr3XffzXO/s2fP1r333uuwLOu122w2jR8/Xi+++GJ24J7T/PnzdffddzuFkHPnznU697kyatQoff3119lfN2jQwKO7Vbwt53Eguff54omrr77a6Q6wCRMmOJyzXXn88cf14YcfOixr0aKFpk2bpr59++Z6kTE5OVkffPCBJk6c6HBxKCQkRPv27dNll13mcr2UlBSFhoY6XNisUKGCZsyYoXvuuSffCWpTU1P122+/adasWQoLC3P6vuY0adIkvfLKKw7L3IluNmzYoJ49ezo9d+TIkXr//fdzPe/8+uuvGj16dJ6/W7iz/5zf90vPHR06dNDHH3+sNm3auFz3xIkTGjJkiHbv3u2wvH///lqxYkW++waA0sy8adABAICpQkNDNXbsWJePLVmyRF27dlW9evV03333aebMmdq3b59HLSNKoqw/Ml977TXNmTMn1/Be+vcigauRyF9++aVb+3r22WedwvuuXbvq119/zTPQ7tatmzZv3qwrrrhCkjxqIXKpSZMmOYX3b775ppYsWZJneC/9G+D973//08svv+ywfMeOHfrxxx9zXa93795O36+8RtdL/4YRl4YsgwYNchhBvG7dOqeQMqec++jWrVuRhPeSdOTIEadlbdu2LZJ9uePdd991Gd6vX78+1/BekqpUqaJZs2bp+eefd3rsv//9r8etECRlh/fdunXTb7/9lucAiP79+2vmzJlOy4cOHZod3j/zzDOaO3duru9THx8fvfDCCxo9erTD8tjY2AK3n8o6R7Rs2VKbNm3Kc1Ro+/bttXHjRrVs2dJheWRkpP773//mu6+5c+c6hfcBAQFasmSJnnzyyVwvlAUGBurtt9/WJ5984vTYBx98kD3S1hNZr7tx48b6/fff8xwdXbFiRc2ePVv9+/d3WB4TE6MFCxa4tb+RI0c6hPcVKlTQypUr9fbbb+cZ3kv/3vGyZcsW9e3b12H59OnTdfr0abf2f6ms8P7rr7/WO++8k2t4L0m33367y8+AS+88gqN27do5Lfvnn3/yXGflypVO4f0tt9yiPXv2qF+/fnneIRQYGKjnnntOy5Ytc/gciImJ0ZQpU3Jdb+3atU53JX388ce699578w3vpX8vYPbt21ffffed0/vaWwzD0MMPP+wUtI8fP17ffPNNnhcNe/TooU2bNqlp06aSCv67RU5Z545Bgwbp119/zTW8l/696Lt69WqndlerV6/WqVOnvFIPAJRUBPgAAJRhr776qjp16pTr42FhYZo1a5YefPBBtWrVSpUqVVKHDh302GOP6ccff3TZeqWkGzx4sNOI3dxcf/31TreZHz16NHsEW26ioqL03XffOSyrUqWKFixY4Nbo1Jo1a2rhwoUF7uN98eJFpxHhY8aMcStUvNSkSZOcQrI333wz1+dXr15d1157rcOy1atX57mPnOH7zTffrM6dO2d/feHCBafRepeKiYlxGolbVO1z7Ha7zp0757Tc1XwTxSE9PV0zZsxwWj579mxdc801bm1j8uTJTvM6pKam6qOPPipQTdWqVdP333/vVsuP4cOHO4XfWT38u3fvnuexdqlXXnnFKWArTIBWvnx5/fTTT6pWrVq+z61WrZp++uknlS9f3mH5t99+q6ioqDzXfe+995yWvf322+rdu7dbdT744IMaM2aMwzLDMFxu1x1+fn764Ycf8myTlcVms7ncz8qVK/Ndd/Xq1dq6davDsq+++kq9evVyu9by5ctr/vz5DoFlenp6gV/7k08+qbvuusut544YMULt27d3WLZhwwanUfn4V40aNZyWnTlzJs91Xn31VYevr7nmGs2fP9/pfZaXvn37Ol2EnjVrVq6/1+S8qBAYGKiRI0e6vb9LVahQoUDr5WfdunVOd/x07NhR77zzjlvr16pVS/PmzfN4ror8NGzY0O35U6pWrer0c8nMzMz3dwUAKO0I8AEAKMMCAgK0fPnyfPu+ZklJSdH27ds1ffp03X777apdu7Z69Oihr776qlSEEz4+Pnr77bc9WsfVKOb82u3NmjXLoYe89G8Ynt/I0ku1bNlSDz/8sNvPv9RHH33kMJKwYsWKeuuttwq0rYkTJzp8vXfv3jxbPuQMHzds2JDnnR05A/w+ffo4BfB5jeJfv3697HZ7njV4S2xsrNO+JOU5YrcoLViwwKl3+oABA9x+v2eZMWOGU6Dz+eefezTRc5Ynn3wyzztbcho8eLDL5W+88YZbo14lqU6dOg4XfSTledEnP+PGjcsepeqOpk2bOs0rkpqa6rI9UZbff//d6Txy9dVX69FHH/Wo1ilTpjhNXFvQi68jRozwqG1kixYtnEbbutOKNOe5qGvXrrrzzjvd3m+W4OBgp1ZPBbnzolKlSk6BYn5yfi5kZGRo//79Hu+7LHB1foyLi8v1+Zs2bdKWLVsclk2bNq1Ad1WNHz9elSpVyv46NTU114t78fHxDl8HBwcXajLsopBzXiPp3++NJ3OWtGrVyqmNVGG9/PLLbs8NJP3bGi3nZw5tjAGUdQT4AACUcSEhIVq0aJHmzJmjJk2aeLSuYRjasGGD7r//fl155ZX69ttvi6jK4tGrVy9dfvnlHq3jqoWGqzYql8oZEPj7++vuu+/2aL+S8p28NTc529zcfvvtqly5coG21blzZ6cAJq/evjnD9/j4eKeJH7NEREQ49Ea+/PLL1bBhQ6dR/3kF+Dkfq1Gjhtujzz2VW8sBT4ILb3I1YvGRRx7xeDuXX365UzuU6OjoAoXg9913n0fPb926tdOyZs2aOQXynm7nr7/+8mj9Sz3wwAMer/Pggw86LcvrLgBXP7uHHnrI7YsWWUJCQjR8+HCHZenp6fnOEeJKQV53zpHo+X3fL1y44DSHRM4WSJ4YOHCgw9cnT550mJfBHXfeeafH58ecr1vK/3OhrMp5gUnKu31Lzs+vRo0aeXR3xqUCAwOd5sLJ7fMr5x0358+f19GjRwu036KScxL1Fi1aqGPHjh5v5/777/dWSQoKCtKIESM8WqdKlSrZbQKz8P4BUNYR4AMAANlsNt111106dOiQlixZouHDh3scWJw6dUr/+c9/dO+99zqNLi8punfv7vE6NWrUUFBQkMOy2NjYXJ+fmZnp1IO6Z8+eBRql3bJlS49GAkv/ts/5448/HJZ5GoZeysfHx6mlRlaPcle6devm1OYgtwB+zZo1Dr18s4L76667ziH02bRpU66Bz9q1ax2+7tWrl0ejET2R2wR/RbW//GzevNnh66CgIKd2OO6644478t1+fq644gqPRt9LctmupWvXrh5tQ5LTpJsZGRlO/azd0axZM1155ZUer9e0aVOndkA7duzIdf4GV9/bIUOGeLxfyTs/u8DAQJehdH5yXhC12+15ft83btzo9D4qzPkp5yTEUt7nJ1cK8rng6kJwXp8LZZmr90Be58ycAXthjg/J+RjJ7fjIebHeMAwNGzasQPMqFIVjx445zaszYMCAAm2rY8eObrUIc3dbnrQ2ypLzPcT7B0BZZ617vgAAgKl8fX01aNAgDRo0SHa7XXv37tWmTZu0c+dO7d69W0eOHHHZIuRSs2fPVmJion744Ydiqtp7co74cldwcLASExOzv87rD80jR4443YrvSVuKnK677jqPRhNv3brVKTCZMmWKy17p7so5CjGv3t4VKlRQp06dHEKYNWvWuGxR4ap9jvTvRYOePXvqp59+kvRv24NNmzY5jcw/c+aM06i9oup/LynX+QsunYyzuCQmJjodF61bty5wb2NXE016OgLf0zt8JDm0t/D2dmJjY93qxX+pwrxX27RpowMHDmR/HR8fr7/++kvNmjVzem7O7229evUKPJfCddddJx8fH4f3vac/uwYNGhSoRYmru0/y+r7nduHCmxfB8pt7IKeCfC64ugBOAOmaq/Nj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+Khdu3YOy9avX1+gNhEHDx70eERwgwYN1KRJE4dl27dv9/rt/O7I2ZM+q4d9zgC/b9++uW7DVRudbdu2OQU6xRXgS9Jjjz3mNPr9r7/+0nvvvVdsNUhyakORkJCgFStWFGhb8+fPz3f7ZcGhQ4cKdMfMX3/9pQMHDjgsa9eunXx8XP855up7++OPP3q8X6lk/excvdfnzZunjIwME6opfjkvehf0gpvVJSQkuGy7FxIS4nL5pXKey9PS0vTDDz94sTrvu/LKK3X//fc7LEtKStKxY8e8to+goCBdffXVDsuWLl1aoIsbZek9BwAlBQE+AADwinXr1jktu/zyy3N9vs1mcxp9WpDJPj///HOP1zHbjTfe6PB1amqq/ve//3m8nYK+9ltuucXh68zMTL366qsF2lZh5AxiwsLCtHv3bm3ZsiV7WWhoqK699lq3t7FmzRqnyZMDAgLUpUsXL1TsnquvvlojR450Wv7iiy8WqqWBK+vWrdOePXtcPta/f3+nZZ9++qnH+zh+/LhT8F+9evVcJ5os7b744guP15k5c6bTspzngUu5+tl9/vnnHt/FERsbq++++85hmZ+fn3r27OnRdopL3bp1ne4OOH78eLG3oDJLzlHaCQkJJlVSdOx2u+69916XbaEmTpyY7wTLOT+/JGny5MlKS0vzWo1FoVmzZk7LCjOnhit33nmnw9dJSUn66KOPPNpGenq6y9ZGAABzEeADAFBGLVmyxGuTvx07dszlCLhBgwbluV7OoGbnzp0ejQTftWuX5s6d6/bzreLee+916un+yiuveNTL/+DBg/r4448LtP+nnnpKAQEBDsu+/fZbzZs3r0DbKyhXo+Jffvllh7YCvXr1chrNfqlu3bo5fC/XrVunVatWOTzn+uuvd3q9Re3dd99VaGiow7LU1FTdcMMNXuktbBiGpk6dqhtuuCHXEOi2225TnTp1HJYtXbpUy5cv92hf48aNcxqN+dBDD7mcx6IsmD59uo4ePer2848eParp06c7LPP3989zpHH79u3Vtm1bh2X79+/3+ALMCy+8oAsXLjgsu+OOO1z23reKF154wWnZ008/bcpdQsUt50SkMTExxT4BdlE6e/asevfu7fJuko4dO2rs2LH5bmPgwIFq1aqVw7Ljx4/r8ccf91aZRSI8PNxpWc7PiMK6//77Vb58eYdlr732mkd3DU2ePFkHDx70al0AgMIjwAcAoIxatmyZmjZtqnvvvbdQvU7Pnj2r2267TUlJSQ7LQ0ND821b4moE6tNPP+3Wfk+dOqU77rijwBPfmql69eoaMWKEw7Lo6GgNHTpUKSkp+a4fERGhIUOGFPi1165dW48++qjT8vvuu08LFiwo0DbtdrvmzZvnMnzLq47mzZs7LFu6dKnD13m1z5H+7fXeqVOn7K8vXryozZs3OzwnZ6ue4lCjRg19/fXXTm2izp49q27duumnn34q8LZ37dqlLl266Nlnn83zGPDz83MZiN1zzz1uBzQTJ07UsmXLHJYFBATokUce8azoUiQ1NVWDBw92K1i9ePGiBg8e7NTresSIEflOJDt+/HinZU8//bTT/A65+eqrr5wu8tlsNj355JNurW+W2267zeniRWxsrG688UanNkTuio+P19SpU/XNN994o8Qik7MFiiSPL7hZUXR0tN566y21atXK5fFbr149/fDDD25dFLTZbHrttdecln/66aeaMGFCgeca2bp1q9Pn8qWmTZuWPU+Lp+Li4pzuIgkODtZll11WoO3lJjQ0VGPGjHFYlpSUpD59+rj1e960adP0yiuveLUmAIB3EOADAFCGZWRkaPbs2WrevLk6duyoGTNmuBwl5kpSUpI+/fRTtW7d2mVbkKlTp+Y76nnw4MGqWrWqw7IlS5bogQcecLogcKmffvpJHTt2zO6ZX9yjq73hrbfecgrwfv31V/Xq1ctla4EsmzZtUpcuXbL/GA8MDCzQ/l9//XWnCSOTkpI0dOhQjR492u3evH/++acmTpyopk2batiwYdq3b59HdeR3kced3vXe2EZRuPHGG/Xxxx873UEQGxurIUOG6Prrr9fKlSvduhCTnp6uZcuWadCgQWrXrp1Dm6G8PPXUU06jVaOiotSjRw99//33ua4XExOj0aNHuwzK3nrrLaeR/WVF1rnmjz/+UJcuXfJs+7Vjxw517drV6fwYGhqqt956K999DR8+XAMGDHBYlpycrIEDB2rGjBm5BpUpKSmaMGGCHnjgARmG4fDYE088YdkJbC81d+5cp8+Gf/75Rx06dNDkyZPdaj2SmZmp9evXa8yYMbrsssv07LPP6ty5c0VVsld07NjRaV6Ep556SosWLSpRF6tTUlK0bds2TZ8+XcOGDVO9evX03HPPKTIy0um5jRo10vr161W/fn23tz9o0CCNGzfOafmbb76pnj176rfffnNrO+Hh4Zo+fbo6d+6szp07a/Hixbk+d8OGDerXr5+uuuoqvfHGG24PfDhw4ID69OmjkydPOiy/8847nUbLe8Mbb7yhxo0bOyw7c+aMWrVqpQkTJjjVnZaWpl9++UW9e/d2uGjYsWNHr9cGACg43/yfAgAAyoJt27Zp27ZtGjdunBo2bKgOHTqoRYsWql69uqpVqyabzaa4uDidPHlS+/fv19q1a5WYmOhyW3fccYfuueeefPcZEBCgiRMn6oknnnBY/sUXX2jZsmUaOnSoWrVqpUqVKunixYs6fPiwli9f7nA7+LBhwxQeHu72qFSrCA0N1aeffqrbb7/dIWTbunWrrrnmGvXq1Ut9+vRR3bp1lZGRoVOnTmnZsmXatm1b9vMrVqyop556qkAj5gICArRw4UJ17NhRp0+fdnjsyy+/1OzZs9W2bVt1795dDRs2VNWqVWW32xUTE6OIiAjt3btXO3fuVFhYWKG+D3369HFqL5KlSZMmatCggVvbePHFF10+VqVKFVMDywcffFABAQF64IEHnHo0b9myRTfccIMqVqyonj17qlWrVqpevbpCQ0Pl6+uruLg4HT9+XHv37tWmTZsK1C+5fPny+u6779S+fXuHftqRkZEaPny4XnnlFd16661q0qSJKlWqpPPnz2vbtm1avHixy8kPBw4c6DI4KyueffZZvffee0pISNDBgwfVsWNHdenSRTfeeGN2AHn69GmtWLFCGzdudArQbTabPvnkE7dbZ8yaNUutWrVyuLCamJiocePGaerUqRo8eLCaN2+ukJAQRUVFae/evVq4cKGioqKcttWmTRtNmTKlEK+++DRp0kQ//PCDBgwY4PC+SUxM1IsvvqgpU6aoS5cuuv7661W7dm2FhIQoKSlJMTExOn36tHbv3q3du3cXaHJwM9WuXVs33HCDw6j78+fP69Zbb1X58uVVv359BQUFOV0U/OKLL5zuWigqAwYMcBk8p6WlKTY2VrGxsbn+bpDTbbfdpi+//NKpdZA7pk2bpr/++ksrV650WP7bb7+pe/fuuvLKK9WjRw+1bNlSVatWlZ+fn2JiYnThwgUdOHBAu3bt0pEjRzwesX/gwAG98MILeuGFF9SwYUO1bt1a1157rWrWrKmQkJDsc/fRo0e1ceNGbd682ek8UK1atSIb6R4UFKR58+apT58+Dp8ZqampevPNN/Xmm28qJCREtWrVUnJyss6dO+d0h9Ajjzyi0NBQ/f7779nLcptwGwBQPAjwAQCAkxMnTujEiRMFWveee+7Rl19+6fbzx40bp6VLl2rNmjUOy7NGxuWlW7du+uqrr/KcDNLKhgwZok8++UQPP/ywwx/4GRkZWrVqlVMv90v5+vrqu+++86hvfk516tTR9u3bdeeddzqNWLTb7dkXdYpSjx495Ovr69RjXcq/fU6Wtm3bKiQkxGVY17NnT9ODh7vvvlstWrTQf/7zH5e9iBMSErRkyRItWbLEo+2WK1dO999/v9MI+5yaN2+u9evXa+DAgYqIiHB47PDhw3rzzTfd2t/gwYP17bff5jknQWnXqFEjffvttxo8eLDsdrsMw9DGjRu1cePGfNe12Wz69NNPNWTIELf3V6NGDW3atEk33HCD/v77b4fHTp06pffff9+t7XTp0kWLFy92mnvDynr37q2NGzdq6NChThcZExMTtXLlSqfwtjSYOnWqNmzY4BSCp6Wl5XpnVHFOdpvXHWLuuu666/Taa68V6rO7XLlyWrJkicaPH68ZM2Y4PX7kyBGPer8XRNbvSgsXLnR7neDgYP3000+qVatWkdXVtm1brV69Wv3793fZ6ismJibXi1vDhg3TBx98oEmTJjksr1y5chFUCgBwF5dRAQAoo+666y6NHDlSISEhXtle48aNtWjRIs2ePdup73defHx8tHjx4nwnvM1p5MiRWrlyZYFbyFjFQw89pG+//dapXUReqlSpokWLFummm24q9P5r1aqltWvX6vXXX/eoBleaN2+uO+64w6N1KleurHbt2rl8zN3WN+XKlVOPHj0KtY2i1rZtW+3fv1/vvPOOateuXaht+fv7a9iwYfrzzz/12WefufUebtu2rbZu3Vqg70dQUJBeeeUVzZ8/v0S2q/K2m2++WT///LNH586qVavq22+/1YMPPujx/ho3bqzNmzfrjjvu8PjiiZ+fn8aNG6fVq1cXaJSz2dq3b6/du3fr3nvvLdSkyTabTT169FDXrl29WF3RaNGihVavXq0mTZqYXYpX1apVS2PGjNFvv/2mnTt3euXCu5+fn6ZPn6758+eradOmhdpWjRo18pzbwxuBe5cuXbRlyxZ169at0NvKT7t27XTgwAENGzbM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IU7rZM1D0KZNG33++edau3atfvrpJ/3111+KiYnR6NGj9dBDD+X5PSysknhclmUE/kDJ4FveX8Pe+F4/TX5Qd911l2JjY/XII4+YXZbXJSYm6uLFi2aXAQvImgehbt26ZpcCAECZQIAPAB7o/cBE7V0+R2+//bYmT55sdjledWm4m5mZ6fDYqVOn9MQTT+jUqVOSJJvNpqCgIEVHR2vDhg3asGGDhg4dqueee85pu3a7XdOmTdP333+fvSwwMFB2u13Hjx/X8ePHtX79ev36668O6yUlJemFF17Qxo0bs5cFBQUpISFBe/bs0Z49e7R8+XK9//77bvXsr1q1qjp16qRNmzZp+fLluQb4hmFoxYoVkqQBAwY4Pf7ZZ5/piy++yP5+BQQEKCMjQ3///bf+/vtvLV68WO+//77TBZBLTZs2Td9++61sNpsqVapEH1l4BYE/YJ5yvr4a8tIXCqgYrEcffVSxsbF67rnnZLPZzC7Na8LCwswuARZy/vx5hYaGqnx5zusAABQ1AnwA8FBpHcX8+++/S/o3nK9Tp0728vj4eI0dO1Znz55Vu3bt9OCDD6ply5YqX768EhIStGjRIn322Wf68ccf1aBBAw0fPtxhux999FF2eH/zzTfrnnvuyQ4YL1y4oP379zv15ZekiRMnauPGjapfv74eeughde3aVUFBQUpNTdXvv/+u9957T/v379err76qd955x63XOHDgQG3atEkbN25UfHy8KlWq5PScffv2ZYcUOQP87777TjNnzlRQUJDuvfdeDRo0SNWrV5fdbtdff/2lDz/8UDt27NBTTz2l+fPnu2xDdPjwYe3evVt333237rrrLlWpUkVpaWmKjo526zUA3kLgD3iXj4+PBo1/T4GVq+j555/XxYsxeuutN0tFiB8bG6v4+Hizy4CFGIah8PBwtz9DAABAwRHgA4AH1s58VUEVAvXss8+aXYrXZE1iu2PHDklS165dHVrAfPXVV9nh/fTp0x1a1lSsWFEjR45UnTp19Mwzz+jLL7/U7bffnv2ckydP6ptvvpEk3X333Xrssccc9l21alX16NFDPXr0cFi+adMm/frrr6pWrZo+++wzh4nS/P391b17dzVr1kxDhw7Vr7/+qiNHjujKK6/M97V269ZNFStWVEJCglavXq3Bgwc7PWfZsmWSpFatWqlevXrZy2NiYvTx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6b4EAMDOeNUJADbUoUMHCnx8R2RkpFq1amX1GJahxAdwrhoaGlRZWXnqDvodO/TFjm/K+q/37D5ZIhotWigiuovadeqqdh37auCVP1ZEp64K7xSn8E5dFXpRx9P+nXPo6y+1e1uJZj062dO/PK+0bdu2k/997bXXnvXjg4ODT/vju3bt0oIFC/TRRx9p7969ampqUseOHTV48GAlJycrOjr6O5+zbNkyPfnkk+rYsaOWLVumf//73/rLX/6ijRs36vDhw4qKitK1116re++993sfFr9582b9+c9/1qeffiqn06kOHTroxhtv1D333NOM34Fv3tnx17/+Ve+//76+/PJLOZ1ORUREqH///kpKStJll132nc/ZvXu3br31VknSm2++qaamJv3lL3/Rhx9+qP379+uiiy7SsmXLmnV9AADgXXjFCQA2FBYWxi58nMIwDHXs2NHqMSxHiQ/g21wul77++uuTxfyJXfTlX+zQFzt2aPeur9TQ0HDy48Pad1R4p65q16mrfvCj63RVxziFd+6qiE5dFdYhRgEtz/2bpCUr/qqg4GDddtttJv7K/MPevXvVtWvXc/68xYsXKycn5+TXNjAwUIZhqKKiQhUVFXrzzTeVk5OjQYMGnTHj7bff1hNPPKGGhgaFhISosbFRlZWVWrBggf71r3/pz3/+sxwOx3c+b+nSpZoxY4aampokSSEhIdqzZ4/mz5+v1atX6/bbb//e2UtLSzV58mQdOHBA0jdn/wcHB2vv3r1asWKFVq5cqd/85jff+82ATZs2aebMmaqrq1NwcDD/BgEA4OP4lx4AbKpz587aunWr1WPAJjp06KDAwECrx7AFSnzAv82ZM0fvvrtKX1RU6MudFar/1hErIe0ivynjO3VV16FX6or/2z0f0amr2nWMVaug0+/mvhCbVyzULbfcotDQUNOzfdGll14qwzDkcrmUl5ennJwcxcbGNvvz16xZoxkzZqhly5b6+c9/rjvuuOPkbvudO3fqD3/4g959911lZGTor3/962l34h88eFBPPfWUbrnlFt17772Kjo6W0+nUm2++qWeeeUZffPGFXnrpJf36178+5fO2bt2qmTNnqqmpSQMHDtQjjzyiuLg4NTQ0aNWqVZo1a5b+9Kc/nXH23bt368EHH9SRI0dO7tjv3r27WrZsqaqqKr322muaP3++fve736lr16667rrrTpszc+ZMXXLJJZoyZYouvfTSk792AADgm3ilCQA21aZNG4WHh+vgwYNWjwKLtWzZ8rQFhD+jxAf8U11dnaZNm6a20bHqfc1I9brlm3L+xFn0QW08W6Lvr/hcu7ZuVP6MRz163XNlGIbVI5zUqVMn3XbbbVq8eLHKysp05513qmfPnurXr5969+6tPn36qFu3bqed+fjx48rNzZUkPfLII/rJT35yys/HxcVp1qxZmjRpkt5//30VFRVp8uTvHm3kdDp1yy236NFH//t1Cw4O1k9/+lNVVlaqqKhI77zzzncK/Hnz5qmxsVEXX3yx8vPzTx7v07JlSyUmJqpt27Z68MEHz/hrz8/P15EjR3TzzTfrqaeeOuXnIiIi9Otf/1qhoaF69tln9fzzz5+xwA8LC9O8efNOeYfAuXwTxC7sdF8CAGBn9nzKEgBA0je78Hlxg44dOyogIMDqMWwnOTlZL730kjb+4y96Y/q9J48z8KQTJf7ixYuVNGbMKUd1ADCfw+HQqDvukOFq0siJT+uapId06bW3Krr7ZR4v7yXp03cWKrRtW918880ev7Y3y8jI0L333qvWrVvL5XLp888/1+uvv67p06dr9OjRSkxM1DPPPHPymJkT1q5dq3379ikyMvLkefCnM3LkSEnSP//5zzN+zC9/+cvT/viJc/m/+uqrUx6ge+TIEf3rX/+S9M03j093Nv/gwYPVr1+/0+ZWV1dr9erVkvSdB/eebvZt27Z959d/wk9/+tPTHu/jbVjjAgDQPGwTAwAbCwoKUvv27bVv3z6rR4FFTtwDOD124gP+Z9LDD+uvryZoy/tvqu/133/euDu5XC5tXrFQo26//YwPWrULuxWlLVu21K9//WulpKTo/fff1yeffKItW7Zox44dOn78uKqqqrRgwQK99dZbysvLU9++fSVJJSUlkqTDhw/rpptuOmP+8ePHJUl79uw57c+HhYWpS5cup/25b/+be/jw4ZNf261bt578RvGVV155xmtfeeWV2rRp03d+fPPmzSc//4EHHjjj53/bnj17FBkZ+Z0f79+/f7M+HwAA+AZeXQKAzXXs2FEHDhxQY2Oj1aPAAp06dbJd8WI3lPiAf4mPj9dVV1+jdQuetbTA3/35Ru3buU1JSQWWzdBcdv13JCQkRDfffPPJdzDU19fr008/1auvvqoPPvhAhw4dUkZGhhYtWqSgoCDt379f0jcF/Zl2p39bfX39aX/8+3avf/sdb99+V1VVVdXJ/46Kijrj55/p507MLqlZs0s65R0A3xYREdGsz7c7u96XAADYDa8sAcDmTpx/XllZafUo8DCHw6Hw8HCrx/AKlPiAf0mbPEmjRo3SV599rC59fmjJDCXvvKqIyIt0ww03WHL9c+EtRWlQUJASEhKUkJCgJ554Qn//+9+1d+9e/fOf/9R11113cjPDVVddpYIC+3/j5NtOzB4UFKS1a9deUJan/31zF2+5LwEAsJpv/MsPAD4uKipKrVq1snoMeFhMTAwvbs8BZ+ID/uPWW29VXNdLtHbBs5Zcv6mpSZtXvqqf3nWXV/z77I3PUbn99v++u6KiokKSdNFFF0mSysrKPD7Pt3e9f9/Rht/eaf9tJ2avr6/XV199Ze5wXopvcgMA0DwU+ADgBVq0aKHOnTtbPQY8KCwsTKGhnn8go7ejxAf8Q0BAgB6e+JA2vfuaDn3t+TL0y03rdPDrrzRmTJLHr30+vLEo/fYxN4GBgZL+e/b7vn379Omnn3p0nt69e5/c+b5+/fozftzHH3982h/v16/fyW/Kv/POO+YP6IW88RtLAABYgQIfALxERESEQkJCrB4DHmAYxhkfroezo8QH/MM999yjkJAQ/fO1uR6/9qfvLFSnzjG6+uqrPX7t82GnorSyslI7d+4868f9/e9/P/nfvXv3liQNGTLk5E72OXPmnPGM+BOqq6svYNJThYaGatCgQZKkV1555bTn63/44YenfYCt9M067tprr5Ukvfzyy2f9PTBzdruy030JAICdUeADgJcwDEOxsbEcqeIHOnXqpKCgIKvH8GqU+IDvCw0N1f333aePF/9R9XU1HrtuY0ODPlv1usYkjfaas8jttAP/iy++0F133aWHHnpIf//737V79+6TP9fQ0KCtW7fqySefVFFRkSSpT58+uvzyyyV9c3781KlTZRiGtm7dql/84hf65z//qePHj5/MqKys1BtvvKFx48bp9ddfN3X2X//61woICFBFRYUmTpx48mifhoYGrVy5Uo888sj3vntu4sSJCgsLU21tre69914tXbpUNTX/vXcPHTqk4uJipaenKzMz09TZ7chO9yUAAHbGv5gA4EWCg4PVsWPHU17swrc4HA516NDB6jF8Ag+2BXzfgw8+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6f+fgxAfH292FJfgdDo1YsRIjRo1Uk1u76YHhn9siXn3edmn9fnw7tq9boUmTZqkZ599lnn3AKqElYrSw4cPa82aNZKkp59+Wr179z7n897e3vrLX/6iv/zlL+rVq5fy8vJMSImqYKXrEgAAK6PABwCLSkpKMjsCLCQlJUXVq1dnt/Zl5ObmqnefPlowf7469Rujtn1etkRJnp6cqFnP3aPs1MNavny57rjjDrMjAYAp9uzZU/LrNm3aXPbr/f0vfAP26NGjmjt3rn799VelpKSouLhYcXFxatmypbp3767Y2PPfdbVs2TKNHDlScXFxWrZsmXbu3KmZM2dq06ZNOn36tKKjo9WmTRv17dtXoaGhF820detWzZgxQ7///rvy8vIUExOjW2+9VX369CnFn4CUnZ2tzz//XD/88IMOHz6svLw8RURE6Prrr9ejjz6q66677rzHJCcnq3PnzpKkL7/8UsXFxZo5c6Z++eUXnThxQlFRUVq2bFmpnh8AALgWCnwAsKDMzExlZWWZHQMW4nQ6dezYMdWuXdvsKJbFYbUA4FpSUlJ05ZVXlvlxixcv1rhx41RYWChJ8vX1lc1mU2JiohITE/Xll19q3Lhxuummmy66xldffaURI0aosLBQwcHBKioqUlJSkubOnauff/5ZM2bMUGBg4HmPW7p0qcaMGaPi4mJJUnBwsI4dO6ZPPvlE3377rbp0ufT3nm3btun5559XWlqapD9m//v7+yslJUUJCQn6+uuv1a9fv0veDNiyZYvGjh2r3Nxc+fv7y9ubl/UAALgzL7MDAADOl5KSYnYEWFBaWpoKCgrMjmFJGzZsULPmLXTw6HE9OX2tZcr7Dctm6KOn2+uG6xpp/a+/UN4D8HgNGzYseWfUO++8o0OHDpXp8d99953GjBkjSerdu7eWLVumdevWae3atfriiy902223KScnRy+++KKOHz9+wTVOnTqlUaNG6e6779by5cv13Xff6YcfftCQIUPk7e2tAwcO6NNPPz3vcbt27dLYsWNVXFyspk2b6osvvtB3332nNWvWaMyYMUpLS9NHH3100ezJyckaOHCg0tLSdOutt2r27Nlat26dvv/+eyUkJKhv377y8vLSlClT9N133110nbFjx+qqq67Sp59+qrVr12rNmjWaPHlymf4cAQCA66DABwCLyc3NZfc9LsjpdCo1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Xr166YorrpD0x8G469evV0JCgiZOnFjyuLZt26pdu3b69ttvNXnyZB07dkzdunVT7dq1JUkFBQXavXu3Vq9eraVLl2ru3LmKiYkp0++1Y8eOeuutt5SZmakRI0ZIkq688kpdc801F/z6qKgoDRw4UBMnTtTatWvVv39/PfXUU7ruuutkt9vldDp16NAhrV27VkuXLlWfPn105513lilTZTP6uqSohxVR1ANVIyIiQidPnizVYfNwbTabTbVq1TI7BgAAHosCHwAuYuPyGUpJ3K03F31W5se64g58SapXr55at26t77//Xp988ok6d+6ssLAwTZkyRYMHD9aePXv0zjvv6J133lFISEjJTvOzLvTi7umnn1ZOTo4WLFigpUuXaunSpQoMDFRxcbHy8vIkScHBwec9bvTo0Ro1apQSEhK0cOFCLVy4UAEBAfLx8VF2dvY5B+yW5+3coaGhuuWWW/Ttt99qx44dki6++/6sRx55RPn5+ZoyZYo2bNigvn37ysfHR4GBgcrJyVFhYWGFMlW2ilyX+fn5WrhwIUU9LIOiHjCXzWZT7dq1tWPHDjmdTrPjoBLVqFGj1GMQAQCA8VyzYQKASlaQd0arp41Q10ce0Q033FDmx7vqDnxJevzxx/X9998rJSVFixYt0iOPPKL4+Hh9+umnWrlypb755hvt3LlTGRkZstvtio+P19VXX61WrVqpdevW561nt9v14osvqlOnTlq4cKE2bdqk9PR0+fn5qUaNGrruuuvUqVOn8x7n7++vsWPH6v7779eXX36pzZs36+TJk8rNzVW1atV01VVXqWXLlmrXrp2io6PL9Xu966679O2330r6YxzAHXfccdnH9OzZU+3atdOCBQu0fv16JScnKzs7W0FBQapZs6aaNWumtm3b6rrrritXpspUkeuyqKhI3bp1U3C1KMVd3YSiHlWGoh6wLn9/f8XFxZXpsHq4lsDAwDK/yxEAABjL5izldomNGzdWdhbAZTVt2tTU5589e7Yee+wxvf5jnrx92R1jhO8/naCvpw7Vzp07Va9evTI/Pjc3Vzt37qyEZED5xcbGKj4+vtyPDwgM1G1Pj9Ut3QYZFwr4P2Up6q9t1EiNGjWiqAcswOl0aufOnTpz5ozZUWAwm82mBg0aKDAw0OwoAAB4NHbgA8D/OJOVoR9mvKG+ff9ervJect0ROnBv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2),\n", + " Circle((x+2.5, y+0.6), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=0.2\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=1.8\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " \n", + " len=1.8\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 4.3, len, -0.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 4.1, len, -1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 3.8, len, -2.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(3.1, 2.6, len, 1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 2.2, len, -0.9, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(3.0, 1.2, len, 2.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 0.9, len, 1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 0.7, len, 0.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " Circle((5.8, 3.9), 0.5, fc=box_bg),\n", + " Circle((5.8, 2.4), 0.5, fc=box_bg),\n", + " Circle((5.8, 1.1), 0.5, fc=box_bg)\n", + " ])\n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " \n", + " plt.text(2.2,0.75, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(5.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(5.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(5.5,3.85, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(3.0, 5, 'Sundew Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a0ace820", + "metadata": {}, + "source": [ + "What difference did it make?\n", + "----------------------------------------\n", + "\n", + "\n", + "On a given linux server used to run both applications for benchmarking purposes:\n", + "\n", + "* The original (PDS) C-code was performing 10-20 product routings per second, \n", + "\n", + "* Sundew (the python replacement) was peaking at 150 messages per second with files,\n", + "\n", + "* Sundew was file system bound, and was also a message router, capable of routing over 400 messages/second.\n", + "\n", + "It feels safe to say it was approximately an order of magnitude improvement in performance.\n", + "\n", + "Note:: These tests were done in 2005, using, for example, hard disks for storage. Performance on current hardware is far superior. \n" + ] + }, + { + "cell_type": "markdown", + "id": "413563af", + "metadata": {}, + "source": [ + "Bonus\n", + "----------\n", + "\n", + "The original PDS application was surrounded by scripts and code to do many kinds of\n", + "name processing, and with the original built in C, this was done with shell scripts.\n", + "As a happy side-effect of implementing in Python, it became feasible to implement\n", + "a plugin architecture to customize processing in a much more efficient way.\n", + "\n", + "Instead of forking of processes, as required by additional functionality in PDS, we could define routines that would be called from the python code. These routines could be written in Python, and thus save enormous amounts of overhead from the fork/reap pattern.\n", + "\n", + "While plugin architectures can be implemented in C, they are much more daunting, and having operations people writing C is a high bar to customize processing.\n", + "\n", + "So a great deal of peripheral, or customized processing was accellerated by the use of Python as the implementation language.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89650e5c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Contribution/Philosophy/index.html b/Contribution/Philosophy/index.html new file mode 100644 index 000000000..02aa41fc0 --- /dev/null +++ b/Contribution/Philosophy/index.html @@ -0,0 +1,168 @@ + + + + + + + Sarracenia Design Philosophy — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sarracenia Design Philosophy

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/Release.html b/Contribution/Release.html new file mode 100644 index 000000000..65e26c3b0 --- /dev/null +++ b/Contribution/Release.html @@ -0,0 +1,763 @@ + + + + + + + Releasing MetPX-Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Releasing MetPX-Sarracenia

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+

Pre-Release Overview

+

MetPX-Sarracenia is distributed in a few different ways, and each has it’s own build process. +Packaged releases are always preferable to one off builds, because they are reproducible.

+

To publish a pre-release one needs to:

+
    +
  • starting with the development branch (for sr3) or v2_dev (for v2.)

  • +
  • run QA process on all operating systems looking for regressions on older 3.6-based ones.

    +
      +
    • github runs flow tests for ubuntu 20.04 and 22.04, review those results.

    • +
    • github runs unit tests (only work on newer python versions.), review those results.

    • +
    • find ubuntu 18.04 server. build local package, run flow tests.

    • +
    • find redhat 8 server. build package: python3 setup.py bdist_rpm, run flow tests

    • +
    • find redhat 9 server, build package: python3 -m build –no-isolation. run flow tests

    • +
    +
  • +
  • review debian/changelog and update it based on all merges to the branch since previous release.

  • +
  • Set the pre-release tag.

  • +
  • commit the above.

  • +
  • pypi.org

    +
      +
    • to ensure compatiblity with python3.6, update a python3.6 branch (for redhat 8 and/or ubuntu 18.)

    • +
    • use the python3.6 branch to release to pypi (because upward compatibility works, but not downward.)

    • +
    • upload the pre-release so that installation with pip succeeds.

    • +
    +
  • +
  • launchpad.org:

    + +
  • +
  • build redhat packages.

    +
      +
    • find redhat 8 server. build package: python3 setup.py bdist_rpm

    • +
    • find redhat 9 server, build package: python3 -m build –no-isolation

    • +
    +
  • +
  • on github: Draft a release.

    +
      +
    • create release notes as prompted.

    • +
    • copy the installation instructions from a previous release (for mostly ubuntu.)

    • +
    • attach: +- wheel built on python3.6 on ubuntu 18 (the uploaded to pypi.org) +- windows binary. +- redhat 8 and 9 rpms labelled as such.

    • +
    +
  • +
  • encourage testing of pre-release, wait some time for blockers, if any.

  • +
+
+
+

Stable Release Process

+

A Stable version is just a pre-release version that has been +re-tagged as stable after some period of waiting for issues +to arise. Since all the testing was done for the pre-release, +the stable release does not require any explicit testing.

+
    +
  • merge from pre-release to stable:

    +

    git checkout stable +git merge pre-release +# there will be conflicts here for debian/changelog and sarracenia/_version.py +# for changelog: +# - merge all the rcX changelogs into a single stable one. +# - ensure the version at the top is correct and tagged ‘unstable’ +# - edit the signature at the bottom for reflect who you are, and current date. +# for sarracenia/_version.py +# - fix it so it shows the correct stable version. +git tag -a v3.xx.yy -m “v3.xx.yy” +git push origin v3.xx.yy

    +
  • +
  • merge from pre-release_py36 to stable_py36:

    +

    git checkout stable_py36 +git merge pre-release_py36 +# same editing required as above. +git tag -a o3.xx.yy -m “o3.xx.yy” +git push origin v3.xx.yy

    +
  • +
  • go on Launchpad,

    +
    +
    +
    +
  • +
  • go on ubuntu 18.04, build bdist_wheel:

    +
    git checkout stable_py36
    +python3 setup.py bdist_wheel
    +
    +
    +
  • +
+

note that pip3 install wheel is needed, because the one from +ubuntu 18 is not compatible with the current pypi.org.

+
    +
  • go on redhat 8, build rpm:

    +

    git checkout stable_py36 +python3 setup.py bdist_rpm

    +
  • +
  • go on redhat 9, build rpm:

    +

    git checkout stable_py36 +rpmbuild –build-in-place -bb metpx-sr3.spec

    +
  • +
  • On github.com, create release.

    +
      +
    • copy/paste install procedure from a previous release, adjust

    • +
    • attach wheel build on ubuntu 18.

    • +
    • attach redhat 8 rpm

    • +
    • attach redhat 9 rpm

    • +
    • attach windows exe

    • +
    +
  • +
+
+
+

Details

+
+

Quality Assurance

+

The Quality Assurance (QA) process, occurs mainly on the development branch. +prior to accepting a release, and barring known exceptions,

+
    +
  • QA tests automatically triggerred by pushes to the development branch should all pass. +(All related github actions.) +tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow are included in “flow.yml”

  • +
  • build an ubuntu 18.04 vm and run the flow tests there to ensure that it works. +(installation method: cloning from development on github.) +tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow

  • +
  • build a redhat 8 vm and run the flow test there to ensure that it works. +(installation method: cloning from development on github.) +tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow

  • +
  • build a redhat 9 vm and run the flow test there to ensure that it works.

  • +
  • build a windows executable… test?

  • +
+

For extensive discussion see: https://github.com/MetPX/sarracenia/issues/139

+

Once the above are done, the pre-release process can proceed.

+
+
+

Versioning Scheme

+

Each release will be versioned as <version>.<YY>.<MM> <segment>

+

It is difficult to reconcile debian and python versioning conventions. +We use rcX for pre-releases which work in both contexts.

+

Where:

+
    +
  • Version is the application version. Currently, 2 and 3 exist.

  • +
  • YY is the last two digits of the year of the initial release in the series.

  • +
  • MM is a TWO digit month number i.e. for April: 04.

  • +
  • segment is what would be used within a series. +from pep0440: +X.YrcN # Release Candidate +X.Y # Final release +X.ypN #ack! patched release.

  • +
+

Currently, 3.00 is still stabilizing, so the year/month convention is not being applied. +Releases are currently 3.00.iircj +where:

+
+
    +
  • ii – incremental number of pre-releases of 3.00

  • +
  • j – beta increment.

  • +
+
+

The first alpha release of v2 from January 2016 would be versioned +as metpx-sarracenia-2.16.01a01. A sample v3 is v3.00.52rc2. At some point 3.00 +will be complete & solid enough that the we will resume the year/month convention.

+

Final versions have no suffix and are considered stable and supported. +Stable should receive bug-fixes if necessary from time to time.

+
+

Note

+

If you change default settings for exchanges / queues as +part of a new version, keep in mind that all components have to use +the same settings or the bind will fail, and they will not be able +to connect. If a new version declares different queue or exchange +settings, then the simplest means of upgrading (preserving data) is to +drain the queues prior to upgrading, for example by +setting, the access to the resource will not be granted by the server. +(??? perhaps there is a way to get access to a resource as is… no declare) +(??? should be investigated)

+

Changing the default requires the removal and recreation of the resource. +This has a major impact on processes…

+
+
+
+

Set the Version

+

This is done to start development on a version. It should be done on development +after every release.

+
    +
  • git checkout development

  • +
  • Edit sarracenia/_version.py manually and set the version number.

  • +
  • Edit CHANGES.rst to add a section for the version.

  • +
  • run dch to start the changelog for the current version. +* change unstable to UNRELEASED (maybe done automatically by dch.)

  • +
  • git commit -a

  • +
  • git push

  • +
+
+
+

Git Branches for Pre-release

+

Prior to releasing, ensure that all QA tests in the section above are passed. +When development for a version is complete. The following should occur:

+

A tag should be created to identify the end of the cycle:

+
git checkout development
+git tag -a v3.16.01rc1 -m "release 3.16.01rc1"
+git push
+git push origin v3.16.01rc1
+
+
+

Once the tag is in the development branch, promote it to stable:

+
git checkout pre-release
+git merge development
+git push
+
+
+

Once stable is updated on github, the docker images will be automatically upgraded, but +we then need to update the various distribution methods: PyPI, and Launchpad

+

Once package generation is complete, one should Set the Version +in development to the next logical increment to ensure no further development +occurs that is identified as the released version.

+
+
+

Build Python3.6 Compatbile Branch

+

Canonical, the company behind Ubuntu, provides Launchpad as a means of enabling third parties to build +packages for their operating system releases. It turns out that the newer OS versions have dependencies +that are not available on the old ones. So the development branch is configured to build on newer +releases, but an a separate branch must be created when creating releases for ubuntu bionic (18.04) and +focal (20.04.) The same branch can be used to build on redhat 8 (another distro that uses python 3.6)

+

Post python 3.7.?, the installatiion method changes from the obsolete setup.py to use pyproject.toml, +and the hatch python tools. Prior to that version, hatchling is not supported, so setup.py must be used. +However the presence of pyproject.toml fools the setup.py into thinking it can install it. To +get a correct installation one must:

+
    +
  • remove pyproject.toml (because setup.py gets confused.)

  • +
  • remove “pybuild-plugin-prproject” dep from debuan

  • +
+

in detail:

+
# on ubuntu 18.04 or redhat 8 (or some other release with python 3.6 )
+
+git checkout pre-release
+git branch -D pre-release_py36
+git branch stable_py36
+git checkout stable_py36
+vi debian/control
+# remove pybuild-plugin-pyproject from the "Build-Depends"
+git rm pyproject.toml
+# remove the new-style installer to force use of setup.py
+git commit -a -m "adjust for older os"
+
+
+

There might be a “–force” required at some point. Perhaps something along the lines of:

+
git push origin stable_py36 --force
+
+
+

Then proceed with Launchpad instructions.

+
+
+

PyPi

+

Because python packages are upward compatible, but not downward, build them on ubuntu 18.04 +(oldest supported python & OS version.) in order for pip installs to work on the widest number +of systems.

+

for local installation on a computer with a python 3.6 for testing and development:

+
python3 setup.py bdist_wheel
+
+
+

or… on newer systems, using build instead:

+
python3 -m build --no-isolation
+
+
+

Pypi does not distinguish between older and newer python releases. There is only one package +version for all supported versions. When uploading from a new OS, the versions in use on the +OS are inferred to be the minimum, and so installation on older operating systems may be blocked +by generated dependencies on overly modern versions.

+

So when uploading to pypi, always do so from the oldest operating system where it needs to work. +upward compatibility is more likely than downward.

+

Pypi Credentials go in ~/.pypirc. Sample Content:

+
[pypi]
+username: SupercomputingGCCA
+password: <get this from someone>
+
+
+

Assuming pypi upload credentials are in place, uploading a new release used to be a one liner:

+
python3 setup.py bdist_wheel upload
+
+
+

on older systems, or on (python >= 3.7) newer ones:

+
twine upload dist/metpx_sarracenia-2.22.6-py3-none-any.whl dist/metpx_sarracenia-2.22.6.tar.gz
+
+
+

Should always include source (the .tar.gz file) +Note that the CHANGES.rst file is in restructured text and is parsed by pypi.python.org +on upload.

+
+

Note

+

When uploading pre-release packages (alpha,beta, or RC) PYpi does not serve those to users by default. +For seamless upgrade, early testers need to do supply the --pre switch to pip:

+
pip3 install --upgrade --pre metpx-sarracenia
+
+
+

On occasion you may wish to install a specific version:

+
pip3 install --upgrade metpx-sarracenia==2.16.03a9
+
+
+

command line use of setup.py is deprecated. Replaced by build and twine.

+
+
+
+
+

Launchpad

+

Generalities about using Launchpad for MetPX-Sarracenia.

+
+

Repositories & Recipes

+

For Ubuntu operating systems, the launchpad.net site is the best way to provide packages that are fully integrated +( built against current patch levels of all dependencies (software components that Sarracenia relies +on to provide full functionality.)) Ideally, when running a server, a one should use one of the repositories, +and allow automated patching to upgrade them as needed.

+

Before every build of any package, it is important to update the git repo mirror on launchpad.

+ +

Wait until this completes.

+

Repositories:

+
    +
  • Daily https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-daily (living on dev… ) +should, in principle, be always ok, but regressions happen, and not all testing is done prior to every +commit to dev branches. +Recipes:

    +
      +
    • metpx-sr3-daily – automated daily build of sr3 packages happens from development branch.

    • +
    • sarracenia-daily – automated daily build of v2 packages happens from v2_dev branch

    • +
    +
  • +
  • Pre-Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-pre-release (for newest features.) +from development branch. Developers manually trigger builds here when it seems appropriate (testing out +code that is ready for release.)

    +
      +
    • metpx-sr3-pre-release – on demand build sr3 packages from pre-release branch.

    • +
    • metpx-sr3-pre-release-old – on demand build sr3 packages from pre-release_py36 branch.

    • +
    • metpx-sarracenia-pre-release – on demand build sr3 packages from v2_dev branch.

    • +
    +
  • +
  • Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx (for maximum stability) +from v2_stable branch. After testing in systems subscribed to pre-releases, Developers +merge from v2_dev branch into v2_stable one, and manually trigger a build.

    +
      +
    • metpx-sr3 – on demand build sr3 packages from stable branch.

    • +
    • metpx-sr3-old – on demand build sr3 packages from stable_py36 branch.

    • +
    • sarracenia-release – on deman build v2 packages from v2_stable branch.

    • +
    +
  • +
+

for more discussion see Which Version is stable

+
+
+

Automated Build

+
    +
  • Ensure the code mirror is updated by checking the Import details by checking this page for sarracenia

  • +
  • if the code is out of date, do Import Now , and wait a few minutes while it is updated.

  • +
  • once the repository is upto date, proceed with the build request.

  • +
  • Go to the sarracenia release recipe

  • +
  • Go to the sr3 release recipe

  • +
  • Click on the Request build(s) button to create a new release

  • +
  • for Sarrac, follow the procedure here

  • +
  • The built packages will be available in the metpx ppa

  • +
+
+
+

Daily Builds

+

Daily builds are configured +using this recipe for python +and this recipe for C and +are run once per day when changes to the repository occur. These packages are stored in the metpx-daily ppa. +One can also Request build(s) on demand if desired.

+
+

Manual Process

+

The process for manually publishing packages to Launchpad ( https://launchpad.net/~ssc-hpc-chp-spc ) +involves a more complex set of steps, and so the convenient script publish-to-launchpad.sh will +be the easiest way to do that. Currently the only supported releases are trusty and xenial. +So the command used is:

+
publish-to-launchpad.sh sarra-v2.15.12a1 trusty xenial
+
+
+

However, the steps below are a summary of what the script does:

+
    +
  • for each distribution (precise, trusty, etc) update debian/changelog to reflect the distribution

  • +
  • build the source package using:

    +
    debuild -S -uc -us
    +
    +
    +
  • +
  • sign the .changes and .dsc files:

    +
    debsign -k<key id> <.changes file>
    +
    +
    +
  • +
  • upload to launchpad:

    +
    dput ppa:ssc-hpc-chp-spc/metpx-<dist> <.changes file>
    +
    +
    +
  • +
+

Note: The GPG keys associated with the launchpad account must be configured +in order to do the last two steps.

+
+
+

Backporting a Dependency

+

Example:

+
backportpackage -k<key id> -s bionic -d xenial -u ppa:ssc-hpc-chp-spc/ubuntu/metpx-daily librabbitmq
+
+
+
+
+

Building an RPM

+

This process is currently a bit clumsy, but it can provide usable RPM packages. +Example of creating a multipass image for fedora to build with:

+
fractal% multipass launch -m 8g --name fed34 https://mirror.csclub.uwaterloo.ca/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.raw.xz
+Launched: fed34
+fractal%
+
+
+

Based on https://www.redhat.com/sysadmin/create-rpm-package … install build-dependencies:

+
sudo dnf install -y rpmdevtools rpmlint git
+git clone -b development https://github.com/MetPX/sarracenia sr3
+cd sr3
+
+
+

One can build a very limited sort of rpm package on an rpm based distro by +The names of the package for file magic data (to determin file types) has different names on +ubuntu vs. redhat. The last three lines of dependencies in pyproject.toml are about +“python-magic”, but on redhat/fedora >= 9, it needs to be “file-magic” instead:

+
# remove last three lines of dependencies in setup.py
+
+* on redhat <=8: vi setup.py ;  python3 setup.py bdist_rpm
+
+# might work, but might need some removals also.
+* on redhat >=9: vi pyproject.toml;  python3 -m build
+
+
+

“python-magic”, but on redhat, it needs to be “file-magic” instead:

+
vi pyproject.toml
+
+
+

using the normal (for Redhat) rpmbuild tool:

+
rpmbuild --build-in-place -bb metpx-sr3.spec
+
+
+

When doing this on the redhat 8, edit the metpx-sr3.spec and potentially pyproject.toml +to remove the other dependencies because there are no OS packages for: paramiko, +watchdog, xattr, & magic. Eventually, one will have removed enough that the rpm file +will be built.

+

One can check if the dependencies are there like so:

+
[ubuntu@fed39 sr3]$ rpm -qR /home/ubuntu/rpmbuild/RPMS/noarch/metpx-sr3-3.00.47-0.fc39.noarch.rpm
+
+/usr/bin/python3
+python(abi) = 3.12
+python3-appdirs
+python3-humanfriendly
+python3-humanize
+python3-jsonpickle
+python3-paramiko
+python3-psutil
+python3-xattr
+python3.12dist(appdirs)
+python3.12dist(humanfriendly)
+python3.12dist(humanize)
+python3.12dist(jsonpickle)
+python3.12dist(paramiko)
+python3.12dist(psutil) >= 5.3
+python3.12dist(watchdog)
+python3.12dist(xattr)
+rpmlib(CompressedFileNames) <= 3.0.4-1
+rpmlib(FileDigests) <= 4.6.0-1
+rpmlib(PartialHardlinkSets) <= 4.0.4-1
+rpmlib(PayloadFilesHavePrefix) <= 4.0-1
+rpmlib(PayloadIsZstd) <= 5.4.18-1
+
+[ubuntu@fed39 sr3]$
+
+
+

You can see all of the prefixed python3 dependencies required, as well as the recommended binary accellerator packages +are listed. Then if you install with dnf install, it will pull them all in. Unfortunately, this method does not allow +the specification of version of the python dependencies which are stripped out. on Fedora 34, this is not a problem, +as all versions are new enough. Such a package should install well.

+

After installation, one can supplement, installing missing dependencies using pip (or pip3.) +Can check how much sr3 is working using sr3 features and use pip to add more features +after the RPM is installed.

+
+
+
+
+

Building a Windows Installer

+

One can also build a Windows installer with that +script. +It needs to be run from a Linux OS (preferably Ubuntu 18) in the root directory of Sarracenia’s git. +find the python version in use:

+
fractal% python -V
+Python 3.10.12
+fractal%
+
+
+

So this is python 3.10. Only a single minor version will have the embedded package needed +by pynsist to build the executable, so look at:

+
https://www.python.org/downloads/windows/
+
+
+

Then go look on python.org, for the “right” version (for 3.10, it is 3.10.11 ) +Then, from the shell, run:

+
sudo apt install nsis
+pip3 install pynsist wheel
+./generate-win-installer.sh 3.10.11 2>&1 > log.txt
+
+
+

The final package will be generated into build/nsis directory.

+
+
+

github

+
    +
  • Click on Releases

  • +
  • Click on tags, pick the tag for the new release vXX.yy.zzrcw

  • +
  • Click on Pre-Release tag at the bottom if appropriate.

  • +
  • Click on Generate Release notes… Review.

  • +
  • copy/paste of Installation bit at the end from a previous release.

  • +
  • Save as Draft.

  • +
  • build packages locally or download from other sources. +drag and drop into the release.

  • +
  • Publish.

  • +
+

This will give us the ability to have old versions available. +launchpad.net doesn’t seem to keep old versions around.

+
+
+

Troubleshooting

+
+
+

ubuntu 18

+

trying to upload from ubuntu 18 vm:

+
buntu@canny-tick:~/sr3$ twine upload dist/metpx_sr3-3.0.53rc2-py3-none-any.whl
+/usr/lib/python3/dist-packages/requests/__init__.py:80: RequestsDependencyWarning: urllib3 (1.26.18) or chardet (3.0.4) doesn't match a supported version!
+  RequestsDependencyWarning)
+Uploading distributions to https://upload.pypi.org/legacy/
+Uploading metpx_sr3-3.0.53rc2-py3-none-any.whl
+100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 408k/408k [00:00<00:00, 120kB/s]
+HTTPError: 400 Client Error: '2.0' is not a valid metadata version. See https://packaging.python.org/specifications/core-metadata for more information. for url: https://upload.pypi.org/legacy/
+ubuntu@canny-tick:~/sr3$
+
+
+

I uploaded from redhat8 instead. used pip3 to install twine on redhat, and that was ok. This could be a result +of running the system provided python3-twine on ubuntu.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/deltas.html b/Contribution/deltas.html new file mode 100644 index 000000000..3b6559918 --- /dev/null +++ b/Contribution/deltas.html @@ -0,0 +1,351 @@ + + + + + + + Discussion of File Modification Propagation — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Pre-Draft

+
+

Discussion of File Modification Propagation

+

This was early thinking about how to deal with file updates. +The early versions of the protocol only concerned itself with entire files. +when the file sets are large enough, partial updates become very desirable. +Also, when the size of individual files is large enough, and when traversing +WAN links, one can obtain substantial practical advantage by sending sending +data using multiple streams, rather than just one.

+

So in v02 there is a header ‘parts’ which indicates the partitioning method +used for a given transfer, and the conclusion about the format/protocol +is now documented in the sr_post(7) man page. This file contains early +discussions & notes.

+
+
Algorithm used (regardless of tool):
    +
  • for each ´block´ (blocksize interesting) generate a signature.

  • +
  • when a subscriber reads an notification message, it includes the signature.

  • +
  • he compares the signatures on the file he already has, and updates it to match.

  • +
+
+
+

the zsync algorithm is the right idea, can perhaps use it directly.

+
+

What If Each Notification is for a Block, not a File ?

+

gedanken experiment… per block messages, rather than entire files ? +what if the messages we send are all per block?

+

Why is this really cool?

+
+
    +
  • It does the gridftp thing, splitting out single file transfers +into parallel streams.

  • +
  • For large files, the ddsr’s might have a whole bunch of part files, +instead of the complete ones, because the transfer is split over +multiple nodes, no problem, as long as later stages are subscribed +to all ddsr’s.

  • +
  • intervening switches do not need to store the largest file +that can be transferred, only some number of the largest chunks. +eliminates the maximum file size problem.

  • +
  • This also deals with files that are written over time, without waiting +until they are complete before hitting send.

  • +
  • for the client to do multi-threaded send, they just start up +any number of sr_senders listening to their own input exchange. +sharing the subscription, just like sr_subscribe (dd_subscribe) does.

  • +
  • for large files, you can see progress reports sources receive +confirmation of each switching layer receiving each chunk.

  • +
+
+

say we set a blocksize of 10MB, and we checksum that block, noting the offset, then +continue?

+

so v01.post: +blocksz sz-inblocks remainder blocknum flags chksum base-url relative-path flow ….

+

see logging.txt for a description of ‘flow’, user settable, with a default.

+

flags says whether the chksum is for the name or the body. (if checksum is for name, +then cannot use blocking chksums.) … says name checksums should only be used for smallish files.

+

The blocksz establishes the multiplier for the sz and the blocknum. the remainder +is the last bit of the file after the last block boundary.

+

So you calculating the checksum for each block you send off a message with the block,

+

this way, for large files, the transfer can be split over a large number of nodes. +but then re-assembly is a bit of a puzzle. will each node of ddsr have only +fractional (aka sparse) large files? as long as the sr_sub is to both ddsr’s, it should +get everything. what happens with sparse files?

+

https://administratosphere.wordpress.com/2008/05/23/sparse-files-what-why-and-how/

+

that´s it’s OK… +it would work, on linux, but it’s a bit strange, and would like cause confusion in +practice. Besides, how do we know when we are done?

+

— Re-assembly — +How about the following. when sr_subscribe(dd_subscribe) writes files, it writes to a file +suffixed .sr§1 when it receives a file, and there is a .sr§ it checks the size +of the file, if the current part is contiguous, it just appends (via lseek & write) +the data to the .sr§ file. If not, it creates a separate .sr§<blocknum>

+
+
+

.sr§ suffix

+

but that means they advertise the parts… hmm… the names now mean something, +We use the Section Character instead of part. to avoid that, pick a name that +is more unusual that .part something like .sr§partnum (using utf-8 interesting +to see what url-encoding will do to that.) It is good to use UTF special +characters, because no-one else uses them, so unlikely to clash.

+

what is someone advertises a .sr§ file? what does it mean? do we need to +detect it?

+

Then it looks in the directory to see if a .sr§<blocknum +1> exists, and appends +it if it does, and loops until all contiguous parts are appended (the corresponding +files deleted.)

+

NOTE: Do not use append writing .sr§, but always lseek and write. This prevents +race conditions from causing havoc. If there are multiple sr_subscribe (dd_subscribes) writing +the file they will just both write the same data multiple times (worst case.)

+

anyways when you run out of contiguous parts, you stop.

+

if the last contiguous block received includes the end of the file, then +do the file completion logic.

+
+
+

How to Select Chunksize

+
+
    +
  • source choice?

  • +
  • we impose our own on ddsr?

  • +
+
+

a default to 10*1024*1024=10485760 bytes, with override possible.

+

might want the validation part to impose a minimum chunksize +on posted files, in addition to the file path.

+

we set a minimum, say 10MB, then if that is less than 5% of the file, +then use 5%, until we get to a maximum chunksize… say 500MB. +override with size 0 (no chunking one long send.)

+
+
what is the overhead to send a block?
    +
  • there are no fixed width fields in the messages, are all variable length ASCII.

  • +
  • estimate that an avereage notification is 100 bytes,

  • +
  • log message is perhaps 120 bytes.

  • +
+
+
for each block transferred.

notification goes in one direction, +at least one log message per scope hope goes through in the other direction.

+

if we say two hops + client delivery (a third hop)

+

so a block will have on the order of 100+4*120 = 580 bytes.

+
+
+

It accepted that there is substantial protocol overhead on small files.

+

However, one would hope the overhead would get more reasonable for bigger files, +but that is limited by the blocksize. +Aesthetically, need to choose a size that dwarfs the overhead.

+
+
+

if we do blockwise cksums, path from v00

+

compatibility… upgrading… +v00.notify alerts boil down to:

+

v01.post could be:

+
+
+
filesz 1 0 0 …
    +
  • the blocksize is the length of the entire file, 1 block ithe sz

  • +
  • no remainder.

  • +
  • 0th block (the first one, zero origin counting)

  • +
+
+
+
+

or we take the convention that a blocksize of zero means no blocking… +in which chase it would be:

+
+
+
0 1 filesz 0 …
    +
  • store the sz as the remainder.

  • +
  • disable blocking for that file.

  • +
+
+
+
+

if there is validation on the blocking size, needs to be a way to deal with it.

+
+
+

Digression about ZSync

+

zsync is available in repositories and zsync(1) is the existing download client. +zsyncmake(1) builds the signatures, with a programmable block size.

+

It looks ike zsync is usable as is?

+
+
downside: portability.
+
need zsync on Windows and mac for downloads, dependency a pain.

there is a Windows binary, made once in 2011… hmm… +have not seein it on Mac OS either… sigh…

+
+
+
+
+

we send the signatures in the notification messages, rather than posting on the site. +If we set the blocksize high, then for files < 1 block, there is no signature.

+

should sr_sarra post the signature to the site, for zsync compatibility?

+

Do not want to be forking zsyncmake for every product… +even if we do not use zsync itself, might want to be compatible… so use +a third party format and have a comparable. 1st implementation would do +forking, 2nd version might replicate the algorithm internally.

+

perhaps we have a threshold, if the file is less than a megabyte, we just send +the new one. The intent is not to replicate source trees, but large data sets.

+
+
    +
  • for most cases (when writing a new file) we do not want extra overhead.

  • +
  • target is large files that change, for small ones, transfer again, is not a big deal.

  • +
  • want to minimize signature size (as will travel with notifications.)

  • +
  • so set a block size to really large.

  • +
+
+

Perhaps build the zsync client into sr_subscribe (dd_subscribe), but use zsync make on the server side ? +or when the file is big enough, forking a zsync is no big deal? but mac & win…

+
+
+

Server/Protocol Considerations

+
+
HTTP:

– uses byte range feature of HTTP. +– FIXME: find samples from other email.

+
+
in SFTP/python/paramiko…

– there is readv( … ) which allows to read subsets of a file. +– the read command in SFTP PROTOCOL spec has offset as a standard argument of read

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/index.html b/Contribution/index.html new file mode 100644 index 000000000..eef979140 --- /dev/null +++ b/Contribution/index.html @@ -0,0 +1,170 @@ + + + + + + + Contributing — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Contributing

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/man_page_template.html b/Contribution/man_page_template.html new file mode 100644 index 000000000..8811637a5 --- /dev/null +++ b/Contribution/man_page_template.html @@ -0,0 +1,257 @@ + + + + + + + SR3_TITLE — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3_TITLE

+
+

sr_title

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr_title foreground|start|stop|restart|reload|status configfile +sr_title cleanup|declare|setup configfile

+
+
+

DESCRIPTION

+

sr_title Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do +eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit +esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat +cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id +est laborum. sr_subscribe(7) +with some limitations.

+
+
    +
  • doesn’t download data, only circulates notification messages.

  • +
  • runs as only a single instance (no multiple instances).

  • +
  • does not support any plugins.

  • +
  • does not support vip for high availability.

  • +
  • different regular expression library: POSIX vs. python.

  • +
  • does not support regex for the strip command (no non-greedy regex).

  • +
+
+

It can therefore act as a drop-in replacement for:

+
+

sr_report(1) - process report notification messages.

+

sr_shovel(8) - process shovel notification messages.

+

sr_winnow(8) - process winnow notification messages.

+
+

The sr_cpump command takes two arguments: an action start|stop|restart|reload|status… (self described) +followed by a configuration file.

+

The foreground action is used when debugging a configuration, when the user wants to +run the program and its configfile interactively… The foreground instance +is not concerned by other actions. The user would stop using the foreground instance +by simply <ctrl-c> on linux or use other means to send SIGINT or SIGTERM to the process.

+

The actions cleanup, declare, setup can be used to manage resources on +the rabbitmq server. The resources are either queues or exchanges. declare creates +the resources. setup creates and additionally does the bindings of queues.

+

The actions add, remove, edit, list, enable, disable act +on configurations.

+
+
+

CONFIGURATION

+

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore +et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid +ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur?”

+
+
+

ENVIRONMENT VARIABLES

+

If the SR_CONFIG_EXAMPLES variable is set, then the add directive can be used +to copy examples into the user’s directory for use and/or customization.

+

An entry in the ~/.config/sarra/default.conf (created via sr_subscribe edit default.conf ) +could be used to set the variable:

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

The value should be available from the output of a list command from the python +implementation.

+
+
+

SEE ALSO

+

sr3_report(7) - the format of report messages.

+

sr3_report(1) - process report messages.

+

sr3_post(1) - post notification messages of specific files.

+

sr3_post(7) - the format of notification messages.

+

sr3_subscribe(1) - the download client.

+

sr3_watch(1) - the directory watching daemon.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/mqtt_issues.html b/Contribution/mqtt_issues.html new file mode 100644 index 000000000..78a603017 --- /dev/null +++ b/Contribution/mqtt_issues.html @@ -0,0 +1,189 @@ + + + + + + + MQTT Implementation Notes — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

MQTT Implementation Notes

+
+

v3 vs. v5

+
    +
  • version 3 has resends sent on a timed basis, every few seconds (perhaps as much as 20 seconds.) +If you ever have a backlog, these retransmits will be a storm of ever increasing traffic.

  • +
  • version 3 does not have shared subscriptions, can only use one process per subscription. +load balancing more difficult.

  • +
+
+
+

Shared Subscriptions

+
    +
  • once you join a group, you are there until the session is dead, even if you disconnect, +it will pile 1/n messages in your queue.

  • +
+
+
+

Back Pressure

+
    +
  1. paho client is async,

  2. +
  3. best practice is to have very light-weight on_message handlers.

  4. +
  5. paho client acknowledges in the library around the time on_message is called.

  6. +
+

If you have an application that is falling behind… say it is slow at processing +but since the reception is async, all this means is you will get an ever bigger +queue of messages on the local host. Ideally, it would let the broker know that +things are going poorly and the broker would send less data there.

+

Methods:

+
    +
  1. v5: Receive Maximum … limit the number of messages in flight between broker and client. +implemented. works.

  2. +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/on_part_assembly.html b/Contribution/on_part_assembly.html new file mode 100644 index 000000000..4ba1ccb5b --- /dev/null +++ b/Contribution/on_part_assembly.html @@ -0,0 +1,217 @@ + + + + + + + File Re-assembling — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

File Re-assembling

+
+

Components

+

sr_watch: You can use sr3_watch to watch a directory for incoming partition files (.Part) from sr_subscribe or sr_sender, both have the ability to send a file in partitions. In the config file for sr3_watch the important parameters to include are:

+
+
    +
  • path <path of directory to watch>

  • +
  • on_part /usr/lib/python3/dist-packages/sarra/plugins/part_file_assemble.py

  • +
  • accept *.Part

  • +
  • accept_unmatch False # Makes it only acccept the pattern above

  • +
+
+

Part_File_Assemble (plugin): This plugin is an on_part plugin which triggers the assembly code in sr_file

+

sr_file: Contains the reassembly code… The algorithm is described below

+
+
+

Algorithm

+

After being triggered by a downloaded part file:

+
+
    +
  • if the target_file doesn’t exist:

    +
    +
      +
    • if the downloaded part file was the first partition (Part 0):

      +
      +
        +
      • create a new empty target_file

      • +
      +
      +
    • +
    +
    +
  • +
  • find which partition number needs to be inserted next (i)

  • +
  • while i < total blocks:

    +
    +
      +
    • file_insert_part()

      +
      +
        +
      • inserts the part file into target file and computes checksum of the inserted portion

      • +
      +
      +
    • +
    • verify insertion by comparing checksums of partition file and inserted block in the file

    • +
    • delete file if okay, otherwise retry

    • +
    • trigger on_file

    • +
    +
    +
  • +
+
+
+
+

Testing

+

Create an sr3_watch config file according to the template above +Start the process by typing the following command: `sr_watch foreground path/to/config_file.cfg`

+

Then create a subcriber config file and include `inplace off` so the file will be downloaded in parts +Start the subscriber by typring `sr_subscribe foreground path/to/config_file.cfg`

+

Now, you must send post messages of the file for the subscriber +for example: `./sr_post.py -pb amqp://tsource@localhost/ -pbu sftp://<user>@localhost/ -p /home/<user>/test_file -px xs_tsource  --blocksize 12M`

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Contribution/v03.html b/Contribution/v03.html new file mode 100644 index 000000000..a75343de0 --- /dev/null +++ b/Contribution/v03.html @@ -0,0 +1,953 @@ + + + + + + + Version 3 Refactor — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Version 3 Refactor

+
+

Summary

+

This document is aimed at developers who need to work with both v2 code and +the refactor that was originally called v3, but eventually was called sr3. +For developers familiar with v2, the document can serve as a bit of a map +to the source code of sr3, which has now stablizied enough that it substantially +passes the flow_tests. Sr3 is perhaps not really usable yet, but the direction +is well established and further development is now described using the issue +tracker (look at the v3only label.) So this document is now essentially +historical. If someone does not know v2, this document will not help as it is +exclusively about mapping v2 to sr3. Reading the balance of v03 documentation +should be more helpful.

+

Abstract Goals of sr3:

+
    +
  • configuration compatibility (upward compatible from v2.) including all plugins.

  • +
  • multi-protocol support. +ability to put in urls for mqtt, or different amqp libraries, perhaps others.

  • +
  • internally represent things in v03 notification messages, have something build +v02 ones for compatibility, but operate in v03.

  • +
  • less code, simpler code. +more readable, elegant, pythonic code. +make maintenance easier.

  • +
+
+

goals of opportunity

+
+
    +
  • add stuff to make it work as an API?

  • +
  • potentially new plugin api to allow groups (of notification messages and/or files.)

  • +
  • Finish off log rotation.

  • +
  • Assume python >= 3.4 remove old cruft.

  • +
  • Assume ubuntu >= 18.04 remove old cruft.

  • +
  • Assume systemd, remove sysv integration.

  • +
  • have options adopt camelCase where possible.

  • +
  • fully async, multi-sources and sinks.

  • +
+
+
+
+

State of the Code

+

As of 2021/08/24, the sr3 code passes all the same flow tests that v2 does +on one laptop (except dynamic in sr3 #407). It runs those same tests using the same configurations, so compatibiliy +goal is achieved. Sr3 accept mqtt broker urls, and an issue is created #389 for amqp v1. +Sr3 is being used to feed a WMO experimental feed, albeit with the need +to restart it regularly ( issue #388 )

+

The new sr3 code has 4000 fewer lines than v2, and includes mqtt.py (extra broker protocol) +as well as a module implementing a compatibility layer with v2 plugins. For example, the +new configuration routine is 30% shorter and more consistent in sr3 than in v2. +The code is also much more pythonic, as the API is much more +natural to work with multiple API levels that can be learned by consulting jupyter notebooks.

+

v2 code:

+
fractal% find -name '*.py' | grep -v .pybuild | grep -v debian | grep -v plugins | xargs wc -l
+ 133 ./sr_winnow.py
+ 544 ./sr_sftp.py
+  47 ./sr_tailf.py
+ 365 ./sr_cache.py
+ 164 ./sr_xattr.py
+1136 ./sr_message.py
+  51 ./sr_checksum.py
+ 129 ./pyads.py
+ 306 ./sr_http.py
+2204 ./sr_subscribe.py
+ 403 ./sr_consumer.py
+1636 ./sr_post.py
+ 265 ./sr1.py
+  54 ./sr_log2save.py
+ 206 ./sr_sarra.py
+ 286 ./sr_rabbit.py
+ 567 ./sr_file.py
+  28 ./__init__.py
+ 107 ./sr_report.py
+  74 ./sr_watch.py
+ 126 ./sr_shovel.py
+ 505 ./sr_retry.py
+ 956 ./sr_util.py
+ 355 ./sr_sender.py
+ 368 ./sr_cfg2.py
+1119 ./sr.py
+ 753 ./sr_poll.py
+ 729 ./sr_audit.py
+ 308 ./sr_credentials.py
+ 988 ./sr_instances.py
+ 608 ./sr_amqp.py
+ 455 ./sr_ftp.py
+3062 ./sr_config.py
+  33 ./sum/checksum_s.py
+  34 ./sum/checksum_d.py
+  34 ./sum/__init__.py
+  26 ./sum/checksum_0.py
+  30 ./sum/checksum_n.py
+  29 ./sum/checksum_a.py
+19223 total
+fractal%
+
+
+

sr3 code:

+
2157 ./config.py
+ 342 ./credentials.py
+ 384 ./diskqueue.py
+ 183 ./filemetadata.py
+ 768 ./flowcb/gather/file.py
+  53 ./flowcb/gather/message.py
+   7 ./flowcb/housekeeping/__init__.py
+ 130 ./flowcb/housekeeping/resources.py
+ 250 ./flowcb/__init__.py
+ 145 ./flowcb/log.py
+  24 ./flowcb/nodupe/data.py
+ 345 ./flowcb/nodupe/__init__.py
+  24 ./flowcb/nodupe/name.py
+ 454 ./flowcb/poll/__init__.py
+  14 ./flowcb/post/__init__.py
+  55 ./flowcb/post/message.py
+ 117 ./flowcb/retry.py
+ 461 ./flowcb/v2wrapper.py
+1617 ./flow/__init__.py
+  80 ./flow/poll.py
+  34 ./flow/post.py
+  18 ./flow/report.py
+  29 ./flow/sarra.py
+  27 ./flow/sender.py
+  16 ./flow/shovel.py
+  29 ./flow/subscribe.py
+  35 ./flow/watch.py
+  16 ./flow/winnow.py
+ 793 ./__init__.py
+ 226 ./instance.py
+  36 ./identity/arbitrary.py
+  93 ./identity/__init__.py
+  33 ./identity/md5name.py
+  24 ./identity/md5.py
+  17 ./identity/random.py
+  24 ./identity/sha512.py
+  17 ./moth/amq1.py
+ 585 ./moth/amqp.py
+ 313 ./moth/__init__.py
+ 548 ./moth/mqtt.py
+  16 ./moth/pika.py
+ 135 ./pyads.py
+ 349 ./rabbitmq_admin.py
+  26 ./sr_flow.py
+  52 ./sr_post.py
+2066 ./sr.py
+  50 ./sr_tailf.py
+ 383 ./transfer/file.py
+ 514 ./transfer/ftp.py
+ 361 ./transfer/https.py
+ 437 ./transfer/__init__.py
+ 607 ./transfer/sftp.py
+15519 total
+
+
+
+
+
+

V02 Plugin Pain Points

+

Writing plugins should be a straight-forward activity for people with a rudimentary +knowledge of Python, and some understanding of the task at hand. In version 2, +writing plugins is a lot harder than it should be.

+
    +
  • syntax error, v2 gives basically a binary response, either reading in the plugin worked +or it didn’t… it is very unfriendly compared to normal python.

  • +
  • when a setting is put in a config file, it’s value is [ value ], and not value (It’s in a list.)

  • +
  • weird scoping issue of import (import in main does not carry over to on_message, need to import +in the main body of the routine as well as in the python file.)

  • +
  • What the heck is self, what the heck is parent? These arguments to plugins are not obvious. +self usually refers to the caller, not the self in a normal class, and parent is the flow, +so no state can be stored in self, and all must be stored in parent. Parent is kind of +a catch all for settings and dynamic values in one pile.

  • +
  • bizarre use of python logger API… self.logger? wha?

  • +
  • inability to call from python code (no API.)

  • +
  • inability to add notification messages within a plugin (can only process the message you have.)

  • +
  • inability to process groups of notification messages at a time (say for concurrent sends or +downloads, rather than just one at time.

  • +
  • poor handling of message acknowledgements. v02 just ackowledges the previous message +when a new one is received.

  • +
  • lack of clarity about options, versus working variables, because they are in the same namespace +in a plugin, if you find self.setting==True … is that because the application set it somewhere, +or because an option was set by a client… is it a setting or a variable?

  • +
  • making changes to notification messages is a bit complicated, because they evolved over different message formats.

  • +
+
+

Changes Done to Address Pain Points

+
    +
  • use importlib from python, much more standard way to register plugins. +now syntax errors will be picked up just like any other python module being imported, +with a reasonable error message.

  • +
  • no strange decoration at end of plugins (self.plugin = , etc… just plain python.)

  • +
  • The strange choice of parent as a place for storing settings is puzzling to people. +parent instance variable becomes options, self.parent becomes self.o

  • +
  • plural event callbacks replace singular ones:

    +
      +
    • after_accept(self,worklist) replaces on_message(self,parent)

    • +
    • after_work(self,worklist) replaces on_part/on_file(self,parent)

    • +
    +
  • +
  • notification messages are just python dictionaries. fields defined by json.loads( v03 payload format ) +notification messages only contain the actual fields, no settings or other things… +plain data.

  • +
  • callbacks move notification messages between worklists. A worklist is just a list of notification messages. There are four:

    +
      +
    • worklist.incoming – notification messages yet to be processed.

    • +
    • worklist.rejected – notification message which are not to be further processed.

    • +
    • worklist.ok – notification messages which have been successfully processed.

    • +
    • worklist.retry – notification messages for which processing was attempted, but it failed.

    • +
    +

    could add others… significant number of applications for something like deferred

    +
  • +
  • acknowledgements done more pro-actively, as soon as a message is processed +(for rejected or failed notification messages, this is much sooner than in v2.)

  • +
  • add scoping mechanism to define plugin properties.

  • +
  • properties fed to __init__ of the plugin, parent is gone from the plugins, they should +just refer to self.o for the options/settings they need. (self.o clearly separates options +from working data.)

  • +
  • command-line parsing using python standard argParse library. Means that keywords no longer work +with a single -. Settling on standard use of – for word based options, and - for abbrevs. +examples: Good: –config, and -c, BAD: -config –c .

  • +
+
+
+
+

Ship of Theseus

+

It might be that the re-factoring inherent in v03 results in a +Ship of Theseus, where it works the same way as v02, but all +the parts are different… obviously a concern/risk… +might be a feature.

+

Now that we are a good way throught the process, a +mapping of source code transcriptions between +the two versions, is clear:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Version 2 file

Version 3 file

sr_config.py

config.py

sr_instances.py

sr.py for most mgmt. +instance.py single proc

sr_consumer.py

+

sr_amqp.py

+

sr_message.py

+

moth/__init__.py

+

moth/amqp.py

+

sr_checksum.py

+

sum/*

+
+
identity/

__init__.py +*

+
+
+

sr_cache.py

flowcb/nodupe.py

sr_retry.py

flowcb/retry.py

+

diskqueue.py

+

sr_post.py

flowcb/gather/file.py +flow/post.py

sr_poll.py

+
flowcb/poll/

__init__.py

+
+
+

flow/poll.py

+

sr_util.py/sr_proto

+

sr_util.py/sr_transport

+

sr_file.py

+

sr_ftp.py

+

sr_http.py

+

sr_sftp.py

+

transfer/__init__.py +* transfer.Protocol

+

flow/__init__.py

+

transfer/file.py

+

transfer/ftp.py

+

transfer/http.py

+

transfer/sftp.py

+

plugins/

flowcb/ (sr3 ones) +plugins/ still there +for v2 ones.

overall flow

flow/__init__.py

sr_poll.py

+

sr_post.py

+

sr_subscribe.py

+

sr_shovel.py

+

sr_report.py

+

sr_sarra.py

+

sr_sender.py

+

sr_watch.py

+

sr_winnow.py

+

sr_flow.py +as entry point…

+

but generally just use +sr.py as single one.

+
+
+

Mappings

+

v2->sr3 instance variables:

+
self.user_cache_dir --> self.o.cfg_run_dir
+
+
+

Changes needed in v2 plugins:

+
from sarra.sr_util import --> from sarracenia import
+
+
+
+
+

Dictionaries or Members for Properties?

+

There seems to be a tension between using class members and dictionaries +for settings. Members seem more convenient, but harder to manipulate, +though we have equivalent idioms. Argparse returns options as their own +members of this parsing object. There is a hierarchy to reconcile:

+
    +
  • protocol defaults

  • +
  • consumer defaults

  • +
  • component defaults

  • +
  • configuration settings (overrides)

  • +
  • command line options (overrides)

  • +
+

resolving them to apply overrides, mais more sense as operations +on dictionaries, printing, saving loading, again makes more sense +as dictionaries. In code, members are slightly shorter, and perhaps +more idiomatic:

+
hasattr(cfg,'member') vs. 'member' in cfg (dictionary)
+
+
+

What makes more sense… Does it make any practical difference? +not sure… need to keep the members for places where +callbacks are called, but can use properties elsewhere, if desired.

+
+
+
+

Known Problems (Solved in sr3)

+
    +
  • passing of logs around is really odd. We didn’t understand what +python logging objects were. Need to use them in the normal way. +new modules are built that way…

    +

    In new modules, use the logging.getLogger( __name__ ) convention, but +often the name does not match the actual source file… why? +e.g. a log message from config.py parsing shows up like:

    +
    2020-08-13 ...  [INFO] sarra.sr_credentials parse_file ... msg text...
    +
    +
    +

    why is it labelled sr_credentials? no idea.

    +
  • +
  • this weird try/except thing for importing modules… tried removing +it but it broke parsing of checksums… sigh… have to spend time +on specifically that problem. On new modules ( sarra.config, +sarra.tmpc.*, sr.py ) using normal imports. likely need to +refactor how checksum plugin mechanism works then try again.

    +

    totally refactored now. Identity class is normal, and separate from flowcb.

    +
  • +
+
+
+

Concrete Plan (Done)

+

Replace sarra/sr_config with sarra/sr_cfg2. The new sr_cfg2 uses argparse +and a simpler model for config file parsing. This became config.py

+

make sr.py accept operations on subsets, so it becomes the unique entry point. +internalize implementation of all management stuff, declare etc…

+

HMPC - Topic Message Protocol Client… a generalization of the message +passing library with a simplified API. abstracts the protocol differences +away. (This later became the Moth module.)

+

The method of testing is to make modifications and check them against the +sr_insects development branch. In general, an un-modified sr_insects tests should +work, but since the logs change, there is logic being added on that branch +to parse v2 and sr3 versions in the same way. Thus the development branch tests +are compatible with both stable and work-in-progress versions.

+

To get each component working, practice with individual unit tests, and then +get to static-flow tests. Can also do flakey_broker. The work is only going +that far as all the components are converted. Once full conversion is achieved, +then will look at dynamic_flow.

+

Purpose is not a finished product, but a product with sufficient and +appropriate structure so that tasks can be delegated with reasonable hope of success.

+
+
+

Done

+

The functionality of sr_amqp.py is completely reproduced in moth/amqp.py +All the important logic is preserved, but it is transcribed into new classes. +Should have identical failure recovery behaviour. But it doesn’t. We have +static flow test passing, but the flakey broker, which tests such recovery, +is currently broken. (2022/03 all good now!)

+

sr_cfg2.py was still a stub, it has a lot of features and options, but +it isn’t clear how to expand it to all of them. the thing about instances +inheriting from configure… it is odd, but hard to see how changing that +will not break everything, plugin-wise… thinking about having defaults +distributed to the classes that use the settings, and having something +that brings them together, instead of one massive config thing. +renamed to config.py (aka: sarra.config) and exercising it with +sr.py. It is now a complete replacement.

+

Replaced the sr_consumer class with a new class that implements the +General Algorithm describe in Concepts <Concepts.rst#the-general-algorithm> +This happenned and became the Flow Module, and the General Algorithm got +renamed the Flow Algorithm. yes, that is now flow/ class hierarchy. +The main logic is in __init__, and actual components are sub-classes.

+

Thinking about just removing sr_ the prefix from classes for replacements, +since they are in sarra directory anyways. so have an internal class +sarra/instances, sarra/sarra <- replace consumer… This happenned +and became a place holder for progress, meaning that files with sr_ +prefix in the name, that are not entry-points, indicate v2 code that +has not yet been retired/replaced.

+

Added configuration selection to sr.py (e.g. subscribe/*) and +setup, and cleanup options.

+

add/remove/enable/disable/edit (in sr.py) done.

+

‘log’ dropped for now… (which log ?)

+

added list, show, and built prototype shovel… required +a instance (sets state files and logs) and then calls flow… +flow/run() is visibly the general algorithm, +shovel is a sub-class of flow.

+

Got a skeleton for v2 plugins working (v2wrapper.py) implemented +import-based and group oriented sr3 plugin framework. ( #213 )

+

cache (now called noDupe) working.

+

re-wrote how the sr3 callbacks work to use worklists, and then re-cast +cache and retry v2plugins as sr3 callbacks themselves.

+

renamed message queue abstract class from tmpc to moth +(what does a Sarracenia eat?)

+

With shovel and winnow replaced by new implementations, it passes +the dynamic flow test, including the Retry module ported to sr3, and +a number of v2 modules used as-is.

+

Completed an initial version of the sr3_post component now (in sr3: flowcb.gather.file.File) +Now working on sr_poll, which will take a while because it involve refactoring: sr_file, sr_http, +sr_ftp, sr_sftp into the transfer module

+

Mostly done sr_subscribe, which, in the old version, is a base class for all other components, +but in sr3 is just the first component that actually downloads data. So encountering all +issues with data download, and flowcb that do interesting things. Mostly done, but +flowcb not quite working.

+

sr_sarra was straightforward once sr_subscribe was done.

+

re-implemented Transfer get to have conventional return value as the number of bytes +transferred, and if they differ, that signals an issue.

+

sr_sender send now done, involved a lot more thinking about how to set new_ fields +in notification messages. but once that was done, was able to remove both the sender and sr_subscribe +(the parent class of most components) and allowed removal of sr_cache, sr_consumer, sr_file, +sr_ftp, sr_http, sr_message, sr_retry, and sr_sftp, sum/*, sr_util.

+

That’s the end of the most difficult part.

+

There was one commit to reformat the entire codebase to PEP style using yapf3. +Now I have the yapf3 pre-commit hook that reformats changes so that the entire codebase +remains yapf3 formatted.

+

Also have written message rate limiting into core, so now have message_rate_min, and message_rate_max +settings that replace/deprecate v2 post_rate_limit plugin.

+
+

Worries Addressed

+

This section contains issues that were taken care of. They were a bother for a while, +so noting down what the solution was.

+
    +
  • logging using __name__ sometimes ends up claiming to be from the wrong file. +example:

    +
    2020-08-16 01:31:52,628 [INFO] sarra.sr_credentials set_newMessageFields FIXME new_dir=/home/peter/sarra_devdocroot/download
    +
    +
    +

    set_newMessageFields is in config.py not sr_credentials… why it is doing that? +Likely wait until all legacy code is replaced before tackling this. +if this doesn’t get fixed, then make it a bug report.

    +

    fixed: note… the problem was that the logger declaration must be AFTER all +imports. Concretely:

    +
    logger = logging.getLogger( __name__ )
    +
    +
    +

    must be placed after all imports.

    +
  • +
  • sr_audit ? what to do. Removed.

  • +
  • all non entry_point sr_*.py files can be removed. +remove sum sub-directory. sr_util.py

  • +
+
+
+

Accel Overhaul

+

plugin compatiblity under review… decided to re-write the accel_* plugins for sr3, and +change the API because the v2 one has fundamental deficiencies:

+
    +
  • the do_get api deals with failure by raising an exception… there is no checking +of return codes on built-in routines… It is possiby taken care of by try/except, +but would prefer for a normal program flow to be able to trace and +report when an i/o failure happens (keep try/except to as small a scale as we can.)

  • +
  • there is a highly… idiosyncratic nature of the do_get, for example in the v2 accel_scp, +where it calls do_get, and then decides not to run and falls through to the built-in +one. This logic is rarely helpful, difficult to explain, and confusing to diagnose +in practice.

  • +
+

Have re-written accel_wget, and accel_scp to the new api… working through static-flow +to test them. There is also logic to spot v2 invocations of them, and replace with sr3 +in the configuration. And the first attempt was quite convoluted… was not happy. +2nd attempt also… working on a third one.

+

Re-wrote again, just adding getAccelerated() to the Transfer API, so it is built-in +instead of being a plugin. Any Transfer class can specify an accelerator and it +will be triggered by accel_threshold. https and sftp/scp accelerators are implemented.

+
+
+
+

DoneTodo

+

Items from the TODO list that have been addressed.

+
    +
  • migrate sr_xattr.py to sarra/xattr.py (now called sarracenia/filemetadata.py)

  • +
  • fix flakey_broker test to pass. (done!)

  • +
  • update documentation… change everything to use sr3 entry point, yes done. +(See transition point below.)

  • +
  • consider transition, life with both versions… should sr.py –> sr3.py ? Yes. Done +should we have a separate debian package with transition entry points +(sr_subscribe and friends only included in compat package, and all) +interactivity natively only happens through sr3? +now called metpx-sr3

  • +
  • perhaps move the whole plugin thing up a level (get rid of directory) +so Plugin becomes a class instantiated in sarra/__init__.py… puts +plugins and built-in code on a more even level… for example how +do plugin transfer protocols work? thinking… This is sort of done +now: plugin became flowcb. Identity is removed from the hierarchy. +Class extension is now a separate kind of plugin (via import)

  • +
  • change default topic_prefix to v03.post done 2021/02

  • +
  • change default topic_prefix to v03 done 2021/03

  • +
  • change topic_prefix to topicPrefix done 2021/03

  • +
  • Adjust Programmer’s Guide to reflect new API. done 2021/02

  • +
  • log incoherency between ‘info’ and logging.INFO prevents proper log control. +FIXED 2021/02.

  • +
  • missing accelerators: sftp.putAcc, ftp.putAc, ftp.getAc, file.getAc,

  • +
  • migrate sr_credentials.py to sarracenia/credentials.py.

  • +
  • remove post from v03 topic trees. Done!

  • +
  • cleanup entry points: sr_audit, sr_tailf, sr_log2save,

  • +
  • test with dynamic-flow.

  • +
  • MQTT Support (Done!)

  • +
+
+
+

BUGS/Concerns/Issues

+

migrated to github issues with v3only tag.

+
+
+

After Parity: True Improvements

+
+
+

TODO

+

At this point am able to report existing problems as issues with the v03only tag. +so below is the things leftover after refactor:

+
    +
  • added “missing defaults” message, examine list, and see if we should set them all. +check_undeclared_options missing defaults: {‘discard’, ‘exchangeSplit’, +‘pipe’, ‘post_total_maxlag’, ‘exchangeSuffix’, ‘destination’, ‘inplace’, +‘report_exchange’, ‘post_exchangeSplit’, ‘set_passwords’, ‘declare_exchange’, +‘sanity_log_dead’, ‘report_daemons’, ‘realpathFilter’, ‘reconnect’, +‘post_exchangeSuffix’, ‘save’, ‘cache_stat’, ‘declare_queue’, +‘bind_queue’, ‘dry_run’, ‘sourceFromExchange’, ‘retry_mode’, ‘poll_without_vip’, ‘header’} +#405

  • +
  • #369 … clean shutdown

  • +
  • figure out an AsyncAPI implementation for subscription at least. #401

  • +
  • get partitioned file transfers working again. #396 +on_part_assembly.rst

  • +
  • convert existing poll to poll0 ? old poll. #394

  • +
  • alarm_set truncates to integers… hmm.. use setitimer instead? #397

  • +
  • outlet option is missing. #398

  • +
  • vhost support needed. #384

  • +
  • sr_poll active/passive bug #29

  • +
  • realpathFilter is used by CMOI. Seems to be disappeared in sr3. It’s there in the C version. #399

  • +
  • port rest of v02 plugins to v03 equivalents and add mappings in config.py, #400 +so that we have barely any v2’s left.

  • +
  • transfer/sftp.py remove file_index from implementation ( #367 ) depend on NoDupe.py

  • +
  • full async mode for MQP’s. requires publish_retry functionality. +(again in future plans above.) #392

  • +
  • once full async mode available, allow multiple gathers and publishes. +(again in future plans above.) #392

  • +
  • #33 add hostname to default queue.

  • +
  • #348 add statehost to .cache directory tree.

  • +
+
+
+

Not Baked/Thinking

+

Structural code things that are not settled, may change. +Probably need to be settled before having anyone else dive in.

+
    +
  • scopable properties for internal classes, like they exist for plugins. #402 +I think this is done. Would have to document somewhere, +testing and demoing at the same time.

  • +
  • took the code required to implement set_newMessageFields (now called +sarracenia.Message.updateFieldsAccept) verbatim from v2. +It is pretty hairy… perhaps turn into a plugin, to get it out of the +main code? Don’t think it will ever go away. It is fairly ugly, but +very useful and heavily used in existing configs. probably OK.

  • +
  • changing recovery model, so that all retry/logic is in main loop, #392 +and moth just returns immediately. Point being could have multiple +gathers for multiple upstreams, and get notification messages from whichever is +live… also end up with a single loop that way… cleaner. +likely equivalent to async mode mentioned above.

  • +
  • gather as a way of separating having multiple input brokers. #392 +so could avoid needing a winnow, but just having a subscriber connect to +multiple upstreams directly. +likely equivalent to async, and multi-gather.

  • +
  • think about API by sub-classing flow… and having it auto-integrate +with sr3 entry point… hmm… likely look at this when updating +Programmer’s Guide.

  • +
  • more worklists? rename failed -> retry or deferred. Add a new failed +where failed represents a permanent failure. and the other represents +to be retried later.

  • +
  • MQTT issues

  • +
+
+
+

FIXME/Deferred

+

The point of the main sr3 work is to get a re-factor done to the point where +the code is understandable to new coders, so that tasks can be assigned. +This section includes a mix of tasks that can hopefully be assigned,

+

FIXME are things left to the side that need to be seen to.

+
    +
  • RELEASE BLOCKER hairy. #403 +watch does not batch things. It just dumps an entire tree. +This will need to be re-wored before release into an iterator style approach. +so if you start in a tree with a million files, it will scan the entire million +and present them as a single in memory worklist. This will have performance +problems. want to incrementally proceed though lists one ‘prefetch’ batch +at a time.

    +

    There is an interim fix to pretend it does batching properly, but the memory +impact and delay to producing the first file is still there, but at least +returns one batch at a time.

    +
  • +
  • RELEASE BLOCKER logs of sr_poll and watch tend to get humungous way too quickly. #389

  • +
  • try out jsonfile for building notification messages to post. can build json incrementally, #402 +so you do not need to delete the _deleteOnPost elements (can just skip over them)

  • +
  • um… add the protocols. mqtt and qpid-proton (amq1) #389

  • +
  • make sure stop actually works… seeing strays after tests… but changing too much +to really know. need to check. It does!

  • +
  • We gave up on partitioned sending as a retrenchment for the refactor. It will come in a +later version.

  • +
  • reporting features mostly removed.

  • +
+
+
+

Transition

+

Do not know if straightforward (Replacement) upgrade is a good approach. Will it be possible to test sarra +sufficiently such that upgrades of entire pumps are possible? or will incremental (parallel) upgrades +be required?

+

It depends on whether sr3 will work as a drop-in replacement or not. There is some incompatibility +we know will happen with do_* plugins. If that is sufficiently well documented and easily +dealt with, then it might not be a problem. On the other hand, if there are subtle +problems, then a parallel approach might be needed.

+
+

Replacement

+

The package has the same name as v2 ones (metpx-sarracenia) differing only in version number. +Installing the new replaces the old completely. This requires that the new version be equal +or better than the old in all aspects, or that installation be confined to test machines +until that point is reached.

+

This takes longer to get initial installation, but has much clearer demarcation (you know +when you are done.)

+
+
+

Parallel

+

Name the package metpx-sarra3 and have the python class directory be sarra3 (instead of sarra.) +(also ~/.config/sr3 and ~/.cache/sr3. likely the .cache files must be different because +retry files have different formats? validate. ) So one can copy configurations from old to +new and run both versions in parallel. The central entry point would be sr3 (rather than +sr), and to avoid confusion the other entry points (sr_subscribe etc…) would be omitted +so that v2 code would work unchanged. Might require some tweaks to have the sr3 classes +ignore instances from the other versions.

+

This is similar to python2 to python3 transition. Allows deployment of sr3 without having +to convert entirely to it. Allows running some components, and building maturity slowly +while others are not ready. It facilitates A:B testing, running the same configuration +with one version or the other without having the install or use a different machine, +facilitating verification of compatibility.

+
+
+

Conclusion

+

Have implemented Parallel model, with APPNAME=sr3 ( ~/.config/sr3, ~/.cache/sr3 ) +sr3_ prefix replacing sr_ for all commands, and changing the sarra Python class to +the full sarracenia name to avoid clashing python classes.

+
+
+
+

Incompatibilities

+

There are not supposed to be any. This is a running list of things to fix or document. +breaking changes:

+
    +
  • in sr3, use – for full word options, like –config, or –broker. In v2 you could use -config and -broker, +but that will end badly in sr3. In the old command line parser, -config, and –config were the same, which +was idiosyncratic. The new +command line option parser is built on ArgParse, and interprets a single - as prefix a single option where the +the subsequent letters are and argument. Example

    +

    -config hoho.conf -> in v2 refers to loading the hoho.conf file as a configuration.

    +

    in sr3, it will be interpreted as -c (config) load the onfig.conf gile, and hoho.conf is part of some subsequent option.

    +
  • +
  • loglevel none -> loglevel notset (now passing loglevel setting directly to python logging module, none isn’t defined.)

  • +
  • log messages and output in interactive, will be completely different.

  • +
  • dropped settings: use_amqplib, use_pika… replaced by separate per protocol implementation libraries. amqp uses the ‘amqp’ library which is neither of the above. ( commit 02fad37b89c2f51420e62f2f883a3828d2056de1 )

  • +
  • dropping on_watch plugins. afaict, no-one uses them. The way v03 works it would be an after_accept for a watch. +makes more sense that way anyways.

  • +
  • plugins that access internals of sr_retry need to be rewritten, as the class is now plugin/retry.py. +the way to queue something for retry in current plugins is to append them to the failed queue. +This is only an issue in the flow tests of sr_insects.

  • +
  • do_download and do_send were 1st pass at schemed plugins, I think they should be deprecated/replaced +by do_get and do_put. unclear whether there is a need for these anymore (download and send plugins are +at wrong level of abstraction)

  • +
  • do_download, do_send, do_get, do_put are schemed downloads… that is, rather than stacking so that +all are called, they are registered for particular protocols. in v2, for example accel_* plugins would +register the “download” scheme. an on_message entry point would alter the scheme so that the do_* routine +would be invoked. In v2, the calling signature for all plugins is the same (self, parent) but for +these do_get and do_put cases, that is quite counter productive. so instead have a calling signature +identical to built-in protocol get/put… src_file, dst_file, src_offset, dst_offset, len ) +Resolution: just implement new Transfer classes, does not naturally fit in flowcb.

  • +
  • In v2, mirror default settings used to be False in all components except sr_sarra. +but the mirror setting was not honoured in shovel, and winnow (bug #358) +this bug is corrected in sr3, but then you notice that the default is wrong.

    +

    In sr3, the default for mirror is changed to True for all flows except subscribe, which +is the least surprising behaviour given the default to False in v2.

    +
  • +
  • in v2, download does not check the length of a file while it is downloading. +in sr3, it does. as an example, when using sftp as a poll, ls will list the size of a symbolic link. +When it downloads, it gets the actual file, and not the symlink, so the size is different.

    +

    Example from flow test:

    +
    2021-04-03 10:13:07,310 [ERROR] sarracenia.transfer read_writelocal util/writelocal mismatched file length writing FCAS31_KWBC_031412___39224.slink. Message said to expect 135 bytes.  Got 114 bytes.
    +
    +
    +

    the file is 114 bytes, by the link path is 135 bytes… +both v2 and sr3 download the file and not the link, but sr3 produces this error message. +Thinking about this one… is it a bug in poll?

    +
  • +
  • In v2, if you delete a file, and then re-create it, an event will be created. +In sr3, if you do the same, the old entry will be in the nodupe cache, and the event will be suppressed. +I have noticed this difference, but not sure which version’s behaviour is correct. +it could be fixed, if we decide the old behaviour is right.

  • +
+
+
+

Features

+
    +
  • All the components are now derived from the flow class, and run the general algorithm already +designed as the basis of v2, but never implemented as such.

  • +
  • The extension API is now vanilla python with no magic settings. just standard classes, using standard import mechanism. +debugging should be much simpler now as the interpreter will provide much better error messages on startup. +The v2 style plugins are now called flow callbacks, and there are a number of classes (identity, moth, +transfer, perhaps flow) that permit extension by straightforward sub-classing. This should make it much +easier to add additional protocols for transport and messages, as well checksum algorithms for new data types.

  • +
  • sarra.moth class abstracts away AMQP, so messaging protocol becomes pluggable.

  • +
  • use the sarracenia/ prefix (already present) to replace sr_ prefix on modules.

  • +
  • API access to flows. (so can build entirely new programs in python by subclassing.)

  • +
  • properties/options for classes are now hierarchical, so can set debug to specific classes within app.

  • +
  • sr3 ability to select multiple components and configurations to operate on.

  • +
  • sr3 list examples is now used to display examples separate from the installed ones.

  • +
  • sr3 show is now used to display the parsed configuration.

  • +
  • notification messages are acknowledged more quickly, should help with throughput.

  • +
  • FlowCB plugin entry_points are now based on groups of notification messages, rather than individual ones, allowing people +to organize concurrent work.

  • +
  • identity (checksums) are now plugins.

  • +
  • gather (inlet? sources of notification messages) are now plugins.

  • +
  • added typing to options settings, so plugins can declare: size, duration, string, or list.

  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/CommandLineGuide.html b/Explanation/CommandLineGuide.html new file mode 100644 index 000000000..e20d600bf --- /dev/null +++ b/Explanation/CommandLineGuide.html @@ -0,0 +1,2634 @@ + + + + + + + Command Line Guide — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Command Line Guide

+
+

SR3 - Everything

+

sr3 is a command line tool to manage Sarracenia +configurations, individually or in groups. For the current user, it reads on all +of the configuration files, state files, and consults the process table to determine +the state of all components. It then makes the change requested.

+
+

sr3 options action [ component/configuration … ]

+
+

sr3 components are used to publish to and download files from websites or file servers +that provide sr_post(7) protocol notifications. Such sites +publish notification messages for each file as soon as it is available. Clients connect to a +broker (often the same as the server itself) and subscribe to the notifications. +The sr_post notifications provide true push notices for web-accessible folders (WAF), +and are far more efficient than either periodic polling of directories, or ATOM/RSS style +notifications. Sr_subscribe can be configured to post notification messages after they are downloaded, +to make them available to consumers for further processing or transfers.

+

sr3 can also be used for purposes other than downloading, (such as for +supplying to an external program) specifying the -n (equal to: download off) will +suppress the download behaviour and only post the URL on standard output. The standard +output can be piped to other processes in classic UNIX text filter style.

+

The components of sarracenia are groups of defaults on the main algorithm, +to reduce the size of individual components. The components are:

+
+
    +
  • cpump - copy notification messages from one pump another second one (a C implementation of shovel.)

  • +
  • flow - generic flow with no default behaviours. Good basis for building user defined components.

  • +
  • poll - poll a non-sarracenia web or file server to create notification messages for processing.

  • +
  • post & watch - create notification messages for files for processing.

  • +
  • sarra _ - download file from a remote server to the local one, and re-post them for others.

  • +
  • sender - send files from a local server to a remote one.

  • +
  • shovel - copy notification messages, only, not files.

  • +
  • watch - create notification messages for each new file that arrives in a directory, or at a set path.

  • +
  • winnow - copy notification messages, suppressing duplicates.

  • +
+
+

All of these components accept the same options, with the same effects. +There is also sr3_cpump(1) which is a C version that implements a +subset of the options here, but where they are implemented, they have the same effect.

+

The sr3 command usually takes two arguments: an action followed by a list +of configuration files. When any component is invoked, an operation and a +configuration file are specified. If the configuration is omitted, it means to +apply the action to all configurations. The action is one of:

+
+
    +
  • foreground: run a single instance in the foreground logging to stderr

  • +
  • restart: stop and then start the configuration.

  • +
  • sanity: looks for instances which have crashed or gotten stuck and restarts them.

  • +
  • start: start the configuration running

  • +
  • status: check if the configuration is running.

  • +
  • stop: stop the configuration from running

  • +
+
+

The remaining actions manage the resources (exchanges, queues) used by the component on +the broker, or manage the configurations.

+
+
    +
  • cleanup: deletes the component’s resources on the server.

  • +
  • declare: creates the component’s resources on the server.

  • +
  • add: copy to the list of available configurations.

  • +
  • list: list all the configurations available.

  • +
  • list plugins: list all the plugins available.

  • +
  • list examples: list all the examples available.

  • +
  • show view an interpreted version of a configuration file.

  • +
  • edit: modify an existing configuration.

  • +
  • remove: remove a configuration.

  • +
  • disable: mark a configuration as ineligible to run.

  • +
  • enable: mark a configuration as eligible to run.

  • +
  • convert: converts v2 configs to a v3 config.

  • +
+
+

For example: sr3 foreground subscribe/dd runs the subscribe component with +the dd configuration as a single foreground instance.

+

The foreground action is used when building a configuration or for debugging. +The foreground instance will run regardless of other instances which are currently +running. Should instances be running, it shares the same notification message queue with them. +A user stop the foreground instance by simply using <ctrl-c> on linux +or use other means to kill the process.

+

After a configuration has been refined, start to launch the component as a background +service (daemon or fleet of daemons whose number is controlled by the instances option.) +If multiple configurations and components need to be run together, the entire fleet +can be similarly controlled using the sr3(1) command.

+

To have components run all the time, on Linux one can use systemd integration, +as described in the Admin Guide On Windows, one can configure a service, +as described in the Windows user manual

+

The actions cleanup, declare, can be used to manage resources on +the rabbitmq server. The resources are either queues or exchanges. declare creates +the resources.

+

The add, remove, list, edit, enable & disable actions are used to manage the list +of configurations. One can see all of the configurations available using the list +action. to view available plugins use list plugins. Using the edit option, +one can work on a particular configuration. A disabled configuration will not be +started or restarted by the start, +foreground, or restart actions. It can be used to set aside a configuration +temporarily.

+

The convert action is used to translate configuration files written with Sarracenia version 2 +options into Sarracenia version 3 options. The v2 configuration file must be placed in the +~/.config/sarra/component/v2_config.conf directory and the translated version will be placed in +the ~/.config/sr3/component/v3_config.conf directory. For example, one would invoke this action +with sr3 convert component/config.

+
+

ACTIONS

+
+

declare

+

Call the corresponding function for each configuration:

+
$ sr3 declare
+  declare: 2020-09-06 23:22:18,043 [INFO] root declare looking at cpost/pelle_dd1_f04
+  2020-09-06 23:22:18,048 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,049 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan01 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,049 [INFO] root declare looking at cpost/veille_f34
+  2020-09-06 23:22:18,053 [INFO] sarra.moth.amqp __putSetup exchange declared: xcpublic (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,053 [INFO] root declare looking at cpost/pelle_dd2_f05
+  ...
+  2020-09-06 23:22:18,106 [INFO] root declare looking at cpost/pelle_dd2_f05
+  2020-09-06 23:22:18,106 [INFO] root declare looking at cpump/xvan_f14
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f14.23011811.49631644 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup um..: pfx: v03, exchange: xcvan00, values: #
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_cpump.xvan_f14.23011811.49631644 with v03.# to xcvan00 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,111 [INFO] root declare looking at cpump/xvan_f15
+  2020-09-06 23:22:18,115 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f15.50074940.98161482 (as: amqp://tfeed@localhost/)
+
+
+

Declares the queues and exchanges related to each configuration. +One can also invoke it with --users, so that it will declare users as well as exchanges and queues:

+
$ sr3 --users declare
+  ...
+  2020-09-06 23:28:56,211 [INFO] sarra.rabbitmq_admin add_user permission user 'ender' role source  configure='^q_ender.*|^xs_ender.*' write='^q_ender.*|^xs_ender.*' read='^q_ender.*|^x[lrs]_ender.*|^x.*public$'
+  ...
+
+
+

Providing a flow/flows will declare only the users that are specified in the flow(s):

+
$ sr3 --users declare subscribe/dd_amis
+  ...
+  declare: 2024-05-17 20:02:18,548 434920 [INFO] sarracenia.rabbitmq_admin add_user permission user 'tfeed@localhost' role feeder  configure=.* write=.* read=.*
+  ...
+
+
+
+
+

dump

+

print the three data structure used by sr. There are three lists:

+
    +
  • processes thought to be related to sr.

  • +
  • configurations present

  • +
  • contents of the state files.

  • +
+

dump is used for debugging or to get more detail than provided by status:

+
Running Processes
+     4238: name:sr_poll.py cmdline:['/usr/bin/python3', '/home/peter/src/sarracenia/sarra/sr_poll.py', '--no', '1', 'start', 'pulse']
+     .
+     .
+     .
+Configs
+   cpost
+       veille_f34 : {'status': 'running', 'instances': 1}
+
+States
+   cpost
+       veille_f34 : {'instance_pids': {1: 4251}, 'queue_name': None, 'instances_expected': 0, 'has_state': False, 'missing_instances': []}
+
+Missing
+
+
+

It is quite long, and so a bit too much information to look at in a raw state. +Usually used in conjunction with linux filters, such as grep. +for example:

+
$ sr3 dump  | grep stopped
+  WMO_mesh_post : {'status': 'stopped', 'instances': 0}
+  shim_f63 : {'status': 'stopped', 'instances': 0}
+  test2_f61 : {'status': 'stopped', 'instances': 0}
+
+$ sr3 dump  | grep disabled
+  amqp_f30.conf : {'status': 'disabled', 'instances': 5}
+
+
+

provides easy method to determine which configurations are in a particular state. +Another example, if sr3 status reports sender/tsource2send_f50 as being partial, then +one can use dump to get more detail:

+
$ sr3 dump | grep sender/tsource2send_f50
+  49308: name:sr3_sender.py cmdline:['/usr/bin/python3', '/usr/lib/python3/dist-packages/sarracenia/instance.py', '--no', '1', 'start', 'sender/tsource2send_f50']
+  q_tsource.sr_sender.tsource2send_f50.58710892.12372870: ['sender/tsource2send_f50']
+
+
+
+
+

foreground

+

run a single instance of a single configuration as an interactive process logging to the current stderr/terminal output. +for debugging.

+

list

+

shows the user the configuration files present:

+
$ sr3 list
+  User Configurations: (from: /home/peter/.config/sarra )
+  cpost/pelle_dd1_f04.conf         cpost/pelle_dd2_f05.conf         cpost/veille_f34.conf
+  cpump/xvan_f14.conf              cpump/xvan_f15.conf              poll/f62.conf
+  post/shim_f63.conf               post/t_dd1_f00.conf              post/t_dd2_f00.conf
+  post/test2_f61.conf              sarra/download_f20.conf          sender/tsource2send_f50.conf
+  shovel/rabbitmqtt_f22.conf       subscribe/amqp_f30.conf          subscribe/cclean_f91.conf
+  subscribe/cdnld_f21.conf         subscribe/cfile_f44.conf         subscribe/cp_f61.conf
+  subscribe/ftp_f70.conf           subscribe/q_f71.conf             subscribe/rabbitmqtt_f31.conf
+  subscribe/u_sftp_f60.conf        watch/f40.conf                   admin.conf
+  credentials.conf                 default.conf
+  logs are in: /home/peter/.cache/sarra/log
+
+
+

The last line says which directory the log files are in.

+

Also list examples shows included configuration templates available as starting points with the add action:

+
$ sr3 list examples
+  Sample Configurations: (from: /home/peter/Sarracenia/development/sarra/examples )
+  cpump/cno_trouble_f00.inc        poll/aws-nexrad.conf             poll/pollingest.conf
+  poll/pollnoaa.conf               poll/pollsoapshc.conf            poll/pollusgs.conf
+  poll/pulse.conf                  post/WMO_mesh_post.conf          sarra/wmo_mesh.conf
+  sender/ec2collab.conf            sender/pitcher_push.conf         shovel/no_trouble_f00.inc
+  subscribe/WMO_Sketch_2mqtt.conf  subscribe/WMO_Sketch_2v3.conf    subscribe/WMO_mesh_CMC.conf
+  subscribe/WMO_mesh_Peer.conf     subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf
+  subscribe/dd_all.conf            subscribe/dd_amis.conf           subscribe/dd_aqhi.conf
+  subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf       subscribe/dd_cmml.conf
+  subscribe/dd_gdps.conf           subscribe/dd_ping.conf           subscribe/dd_radar.conf
+  subscribe/dd_rdps.conf           subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+  subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf
+  subscribe/hpfx_amis.conf         subscribe/local_sub.conf         subscribe/pitcher_pull.conf
+  subscribe/sci2ec.conf            subscribe/subnoaa.conf           subscribe/subsoapshc.conf
+  subscribe/subusgs.conf           watch/master.conf                watch/pitcher_client.conf
+  watch/pitcher_server.conf        watch/sci2ec.conf
+
+
+$ sr3 add dd_all.conf
+  add: 2021-01-24 18:04:57,018 [INFO] sarracenia.sr add copying: /usr/lib/python3/dist-packages/sarracenia/examples/subscribe/dd_all.conf to /home/peter/.config/sr3/subscribe/dd_all.conf
+$ sr3 edit dd_all.conf
+
+
+

The add, remove, list, edit, enable & disable actions are used to manage the list +of configurations. One can see all of the configurations available using the list +action. to view available plugins use list plugins. Using the edit option, +one can work on a particular configuration. A disabled sets a configuration aside +(by adding .off to the name) so that it will not be started or restarted by +the start, foreground, or restart actions.

+
+
+

show

+

View all configuration settings (the result of all parsing… what the flow components actually see):

+
% sr3 show subscribe/q_f71
+2022-03-20 15:30:32,507 1084652 [INFO] sarracenia.config parse_file download_f20.conf:35 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept"
+2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file tsource2send_f50.conf:26 obsolete v2:"on_message msg_rawlog" converted to sr3:"logEvents after_accept"
+2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file rabbitmqtt_f22.conf:6 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept"
+
+Config of subscribe/q_f71:
+{'_Config__admin': 'amqp://bunnymaster@localhost/ None True True False False None None',
+ '_Config__broker': 'amqp://tsource@localhost/ None True True False False None None',
+ '_Config__post_broker': None,
+ 'accelThreshold': 0,
+ 'acceptSizeWrong': False,
+ 'acceptUnmatched': False,
+ 'admin': 'amqp://bunnymaster@localhost/ None True True False False None None',
+ 'attempts': 3,
+ 'auto_delete': False,
+ 'baseDir': None,
+ 'baseUrl_relPath': False,
+ 'batch': 1,
+ 'bindings': [('xs_tsource_poll', ['v03', 'post'], ['#'])],
+ 'broker': 'amqp://tsource@localhost/ None True True False False None None',
+ 'bufsize': 1048576,
+ 'byteRateMax': None,
+ 'cfg_run_dir': '/home/peter/.cache/sr3/subscribe/q_f71',
+ 'component': 'subscribe',
+ 'config': 'q_f71',
+ 'currentDir': None,
+ 'debug': False,
+ 'declared_exchanges': [],
+ 'declared_users': {'anonymous': 'subscriber', 'eggmeister': 'subscriber', 'ender': 'source', 'tfeed': 'feeder', 'tsource': 'source', 'tsub': 'subscriber'},
+ 'delete': False,
+ 'destfn_script': None,
+ 'directory': '//home/peter/sarra_devdocroot/recd_by_srpoll_test1',
+ 'discard': False,
+ 'documentRoot': None,
+ 'download': True,
+ 'durable': True,
+ 'env_declared': ['FLOWBROKER', 'MQP', 'SFTPUSER', 'TESTDOCROOT'],
+ 'exchange': 'xs_tsource_poll',
+ 'exchangeDeclare': True,
+ 'exchangeSuffix': 'poll',
+ 'expire': 1800.0,
+ 'feeder': ParseResult(scheme='amqp', netloc='tfeed@localhost', path='/', params='', query='', fragment=''),
+ 'fileEvents': {'create', 'link', 'modify', 'delete', 'mkdir', 'rmdir' },
+ 'file_total_interval': '0',
+ 'filename': None,
+ 'fixed_headers': {},
+ 'flatten': '/',
+ 'hostdir': 'fractal',
+ 'hostname': 'fractal',
+ 'housekeeping': 300,
+ 'imports': [],
+ 'inflight': None,
+ 'inline': False,
+ 'inlineByteMax': 4096,
+ 'inlineEncoding': 'guess',
+ 'inlineOnly': False,
+ 'instances': 1,
+ 'identity_arbitrary_value': None,
+ 'identity_method': 'sha512',
+ 'logEvents': {'after_work', 'after_accept', 'on_housekeeping'},
+ 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s',
+ 'logLevel': 'info',
+ 'logReject': False,
+ 'logRotateCount': 5,
+ 'logRotateInterval': 86400.0,
+ 'logStdout': True,
+ 'log_flowcb_needed': False,
+ 'masks': ['accept .* into //home/peter/sarra_devdocroot/recd_by_srpoll_test1 with mirror:True strip:.*sent_by_tsource2send/'],
+ 'messageAgeMax': 0,
+ 'messageCountMax': 0,
+ 'messageDebugDump': False,
+ 'messageRateMax': 0,
+ 'messageRateMin': 0,
+ 'message_strategy': {'failure_duration': '5m', 'reset': True, 'stubborn': True},
+ 'message_ttl': 0,
+ 'mirror': True,
+ 'msg_total_interval': '0',
+ 'fileAgeMax': 0,
+ 'nodupe_ttl': 0,
+ 'overwrite': True,
+ 'permCopy': True,
+ 'permDefault': 0,
+ 'permDirDefault': 509,
+ 'permLog': 384,
+ 'plugins_early': [],
+ 'plugins_late': ['sarracenia.flowcb.log.Log'],
+ 'post_baseDir': None,
+ 'post_baseUrl': None,
+ 'post_broker': None,
+ 'post_documentRoot': None,
+ 'post_exchanges': [],
+ 'post_topicPrefix': ['v03', 'post'],
+ 'prefetch': 25,
+ 'pstrip': '.*sent_by_tsource2send/',
+ 'queueBind': True,
+ 'queueDeclare': True,
+ 'queueName': 'q_tsource_subscribe.q_f71.76359618.62916076',
+ 'queue_filename': '/home/peter/.cache/sr3/subscribe/q_f71/subscribe.q_f71.tsource.qname',
+ 'randid': 'cedf',
+ 'randomize': False,
+ 'realpathPost': False,
+ 'rename': None,
+ 'report': False,
+ 'reset': False,
+ 'resolved_qname': 'q_tsource_subscribe.q_f71.76359618.62916076',
+ 'retry_ttl': 1800.0,
+ 'settings': {},
+ 'sleep': 0.1,
+ 'statehost': False,
+ 'strip': 0,
+ 'subtopic': [],
+ 'timeCopy': True,
+ 'timeout': 300,
+ 'timezone': 'UTC',
+ 'tls_rigour': 'normal',
+ 'topicPrefix': ['v03', 'post'],
+ 'undeclared': ['msg_total_interval', 'file_total_interval'],
+ 'users': False,
+ 'v2plugin_options': [],
+ 'v2plugins': {'plugin': ['msg_total_save', 'file_total_save']},
+ 'vhost': '/',
+ 'vip': []}
+
+%
+
+
+
+
+

convert

+

Converting a config: both formats are accepted, as well as include files:

+
$ sr3 convert poll/sftp_f62
+  2022-06-14 15:00:00,762 1093345 [INFO] root convert converting poll/sftp_f62 from v2 to v3
+
+$ sr3 convert poll/sftp_f62.conf
+  2022-06-14 15:01:11,766 1093467 [INFO] root convert converting poll/sftp_f62.conf from v2 to v3
+
+$ sr3 convert shovel/no_trouble_f00.inc
+  2022-06-14 15:03:29,918 1093655 [INFO] root convert converting shovel/no_trouble_f00.inc from v2 to v3
+
+
+

To overwrite an existing sr3 configuration use the –wololo option. +When overwriting multiple sr3 configurations, one must also use –dangerWillRobinson=n in the +normal way… where n is the number of configurations to convert.

+
+
+

start

+

launch all configured components:

+
$ sr3 start
+  gathering global state: procs, configs, state files, logs, analysis - Done.
+  starting...Done
+
+
+
+
+

stop

+

stop all processes:

+
$ sr3 stop
+  gathering global state: procs, configs, state files, logs, analysis - Done.
+  stopping........Done
+  Waiting 1 sec. to check if 93 processes stopped (try: 0)
+  All stopped after try 0
+
+
+
+
+

status

+

Sample OK status (sr is running):

+
fractal% sr3 status
+status:
+Component/Config                         Processes   Connection        Lag                              Rates
+                                         State   Run Retry  msg data   Queued  LagMax LagAvg  Last  %rej     pubsub messages   RxData     TxData
+                                         -----   --- -----  --- ----   ------  ------ ------  ----  ----     ------ --------   ------     ------
+cpost/veille_f34                         run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/pelle_dd1_f04                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   31.3%  0 Bytes/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/pelle_dd2_f05                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   31.3%  0 Bytes/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/xvan_f14                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/xvan_f15                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+poll/f62                                 run     1/1     0 100%   0%      0    0.08s    0.04s  1.4s  0.0%  2.0 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+post/shim_f63                            stop    0/0          -          -         -     -     -          -        -
+post/test2_f61                           stop    0/0     0 100%   0%      0    0.02s    0.01s  0.4s  0.0%  8.1 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+sarra/download_f20                       run     3/3     0 100%  10%      0   13.17s    5.63s  1.8s  0.0%  5.4 KiB/s   4 msgs/s  1.7 KiB/s  0 Bytes/s
+sender/tsource2send_f50                  run   10/10     0 100%   9%      0    1.37s    1.08s  1.9s  0.0%  8.1 KiB/s   5 msgs/s  0 Bytes/s  1.7 KiB/s
+shovel/pclean_f90                        run     3/3   136 100%   0%      0    0.00s    0.00s  0.6s  0.0%  4.0 KiB/s   5 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/pclean_f92                        run     3/3     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/rabbitmqtt_f22                    run     3/3     0 100%   0%      0    0.89s    0.67s  1.5s  0.0%  8.1 KiB/s   5 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/t_dd1_f00                         run     3/3     0 100%   0%    124   23.15s    4.50s  0.1s 55.0%  3.9 KiB/s   9 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/t_dd2_f00                         run     3/3     0 100%   0%     83   11.82s    3.50s  0.1s 49.2%  3.6 KiB/s   8 msgs/s  0 Bytes/s  0 Bytes/s
+subscribe/amqp_f30                       run     3/3     0 100%  12%      0   18.79s    9.22s  0.1s  0.0%  3.3 KiB/s   4 msgs/s  1.9 KiB/s  0 Bytes/s
+subscribe/cclean_f91                     run     3/3   145 100%   0%      1    0.00s    0.00s  0.4s  0.0%  2.3 KiB/s   6 msgs/s  0 Bytes/s  0 Bytes/s
+subscribe/cdnld_f21                      run     3/3     0 100%  17%     12    7.20s    2.81s  0.7s  0.0%  2.3 KiB/s   3 msgs/s  1.7 KiB/s  0 Bytes/s
+subscribe/cfile_f44                      run     3/3     0 100%   6%      1    3.32s    0.32s  0.4s  0.0%  2.3 KiB/s   6 msgs/s  1.7 KiB/s  0 Bytes/s
+subscribe/cp_f61                         run     3/3     0 100%   3%      0    6.42s    3.49s  1.6s  0.0%  4.2 KiB/s   6 msgs/s 635 Bytes/s  0 Bytes/s
+subscribe/ftp_f70                        run     3/3     0 100%   8%      0    1.18s    0.83s  0.2s  0.0%  1.8 KiB/s   3 msgs/s  1.8 KiB/s  0 Bytes/s
+subscribe/q_f71                          run     3/3     0 100%   0%      0    1.62s    0.57s  0.0s  0.0%  1.2 KiB/s   3 msgs/s  1.2 KiB/s  0 Bytes/s
+subscribe/rabbitmqtt_f31                 run     3/3     0 100%  11%      0    4.27s    1.95s  1.2s  0.0%  4.2 KiB/s   6 msgs/s 637 Bytes/s  0 Bytes/s
+subscribe/u_sftp_f60                     run     3/3     0 100%   1%      0    2.69s    2.23s  1.3s  0.7%  4.2 KiB/s   6 msgs/s 644 Bytes/s  0 Bytes/s
+watch/f40                                run     1/1     0 100%   0%      0    0.10s    0.05s  1.9s  0.0%  4.2 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+winnow/t00_f10                           run     1/1     0 100%   0%      0   12.31s    4.33s  3.5s 50.0%  3.2 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s
+winnow/t01_f10                           run     1/1     0 100%   0%      0   11.59s    3.76s  0.1s 50.5%  4.2 KiB/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+      Total Running Configs:  25 ( Processes: 64 missing: 0 stray: 0 )
+                     Memory: uss:2.4 GiB rss:3.3 GiB vms:6.2 GiB
+                   CPU Time: User:39.62s System:4.42s
+       Pub/Sub Received: 103 msgs/s (80.6 KiB/s), Sent:  63 msgs/s (32.8 KiB/s) Queued: 221 Retry: 281, Mean lag: 2.32s
+          Data Received: 32 Files/s (11.9 KiB/s), Sent: 5 Files/s (1.7 KiB/s)
+
+
+

Full sample:

+
fractal% sr3 --full status
+status:
+Component/Config                         Processes   Connection        Lag                              Rates                                        Counters (per housekeeping)                                                    Data Counters                                           Memory                             CPU Time
+                                         State   Run Retry  msg data   Queued  LagMax LagAvg  Last  %rej     pubsub messages   RxData     TxData       subBytes   Accepted   Rejected  Malformed   pubBytes    pubMsgs     pubMal     rxData    rxFiles     txData    txFiles    Since       uss        rss        vms       user     system
+                                         -----   --- -----  --- ----   ------  ------ ------  ----  ----     ------ --------   ------     ------        -------   --------   --------  ---------    -------     ------      -----      -----    -------     ------  -------      -----        ---        ---        ---       ----     ------
+cpost/veille_f34                         run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    2.4 MiB    5.9 MiB   15.2 MiB        0.03       0.08
+cpump/pelle_dd1_f04                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   83.5%  0 Bytes/s  11 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes    1.4 Kim    1.1 Kim     0 msgs    0 Bytes   230 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    3.9 MiB    6.8 MiB   17.2 MiB        0.12       0.11
+cpump/pelle_dd2_f05                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   83.5%  0 Bytes/s  11 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes    1.4 Kim    1.1 Kim     0 msgs    0 Bytes   230 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    3.9 MiB    7.0 MiB   17.2 MiB        0.17       0.06
+cpump/xvan_f14                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   12.90s    3.0 MiB    4.7 MiB   16.1 MiB        0.01       0.01
+cpump/xvan_f15                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes      1 msg     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files    8.63s    3.0 MiB    4.6 MiB   16.1 MiB        0.01       0.02
+poll/f62                                 run     1/1     0 100%   0%      0    0.16s    0.05s  0.8s  0.0%  1.3 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs    4.4 Kim     0 msgs  151.1 KiB   420 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.56s   45.1 MiB   59.0 MiB  229.3 MiB        2.62       0.32
+post/shim_f63                            stop    0/0          -          -         -     -     -          -        -        -       -          -          -          -          -          -          -          -          -          -          -    0 Bytes    0 Bytes    0 Bytes        0.00       0.00
+post/test2_f61                           stop    0/0     0 100%   0%      0    0.03s    0.02s  0.5s  0.0% 11.5 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    6.7 KiB    14 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files    0.58s    0 Bytes    0 Bytes    0 Bytes        0.00       0.00
+sarra/download_f20                       run     3/3     0 100%   0%      0   10.17s    2.87s  1.1s  0.0%  1.1 KiB/s   0 msgs/s  1.1 KiB/s  0 Bytes/s   27.1 KiB    47 msgs     0 msgs     0 msgs   36.0 KiB    47 msgs     0 msgs   65.6 KiB   47 Files    0 Bytes    0 Files   57.57s  132.5 MiB  192.4 MiB  280.1 MiB        2.72       0.40
+sender/tsource2send_f50                  run   10/10     0 100%   9%      0    1.37s    0.52s  1.3s  0.0%  5.5 KiB/s   3 msgs/s  0 Bytes/s  1.3 KiB/s  326.0 KiB   421 msgs     0 msgs     0 msgs  326.6 KiB   421 msgs     0 msgs    0 Bytes    0 Files  152.7 KiB  421 Files  118.97s  425.6 MiB  562.8 MiB    2.2 GiB        7.80       0.93
+shovel/pclean_f90                        run     3/3   310 100%   0%      0   82.03s   75.72s  0.7s  0.0%  5.6 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s  111.4 KiB   120 msgs     0 msgs     0 msgs   99.9 KiB   111 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   38.02s  127.4 MiB  169.0 MiB  249.6 MiB        2.37       0.27
+shovel/pclean_f92                        run     3/3     0 100%   0%      0   82.49s   76.06s 19.1s  0.0%  1.7 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s   99.9 KiB   111 msgs     0 msgs     0 msgs  103.0 KiB   111 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.28s  126.4 MiB  168.5 MiB  249.2 MiB        2.04       0.21
+shovel/rabbitmqtt_f22                    run     3/3     0 100%   0%      0    1.25s    0.54s  1.3s  0.0%  5.5 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s  326.0 KiB   421 msgs     0 msgs     0 msgs  326.0 KiB   421 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.77s  126.0 MiB  167.6 MiB  248.7 MiB        2.12       0.20
+shovel/t_dd1_f00                         run     3/3     0 100%   0%      3   23.15s    3.06s  0.1s 82.3%  3.4 KiB/s  14 msgs/s  0 Bytes/s  0 Bytes/s  231.6 KiB    1.6 Kim    1.3 Kim     0 msgs  168.4 KiB   293 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.19s  127.3 MiB  171.0 MiB  250.1 MiB        2.77       0.32
+shovel/t_dd2_f00                         run     3/3     0 100%   0%      0   11.82s    2.61s  0.2s 82.2%  3.3 KiB/s  13 msgs/s  0 Bytes/s  0 Bytes/s  225.7 KiB    1.6 Kim    1.3 Kim     0 msgs  166.6 KiB   290 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.33s  127.3 MiB  170.6 MiB  250.1 MiB        2.74       0.39
+subscribe/amqp_f30                       run     3/3     0 100%  39%      0   18.79s    6.44s  0.7s  0.0%  1.5 KiB/s   2 msgs/s  1.2 KiB/s  0 Bytes/s  181.0 KiB   237 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  146.8 KiB  237 Files    0 Bytes    0 Files  117.95s  126.2 MiB  168.0 MiB  248.9 MiB        2.20       0.27
+subscribe/cclean_f91                     run     3/3   213 100%   0%      0    0.00s    0.00s  0.5s 17.4%  2.7 KiB/s   6 msgs/s  0 Bytes/s  0 Bytes/s   35.5 KiB    92 msgs    16 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   13.37s  126.1 MiB  167.9 MiB  248.8 MiB        2.53       0.36
+subscribe/cdnld_f21                      run     3/3     0 100%  41%      0   10.43s    3.13s  0.1s  0.0%  1.4 KiB/s   2 msgs/s  1.4 KiB/s  0 Bytes/s  167.1 KiB   262 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  162.7 KiB  262 Files    0 Bytes    0 Files  117.90s  131.6 MiB  190.7 MiB  277.6 MiB        3.39       0.42
+subscribe/cfile_f44                      run     3/3     0 100%  40%      0    3.32s    0.30s  0.0s  0.0%  1.5 KiB/s   4 msgs/s  1.4 KiB/s  0 Bytes/s  178.3 KiB   509 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  161.8 KiB  509 Files    0 Bytes    0 Files  118.09s  130.9 MiB  188.5 MiB  278.0 MiB        2.62       0.30
+subscribe/cp_f61                         run     3/3     0 100%  12%      0    6.42s    2.09s  0.1s  0.0%  2.8 KiB/s   3 msgs/s 597 Bytes/s  0 Bytes/s  326.6 KiB   421 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  123 Files    0 Bytes    0 Files  118.23s  125.9 MiB  166.8 MiB  248.9 MiB        2.48       0.31
+subscribe/ftp_f70                        run     3/3     0 100%  39%      0    1.75s    0.77s  0.1s  0.0%  1.3 KiB/s   2 msgs/s  1.4 KiB/s  0 Bytes/s  158.4 KiB   340 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  159.8 KiB  340 Files    0 Bytes    0 Files  118.04s  126.1 MiB  167.4 MiB  248.8 MiB        2.22       0.36
+subscribe/q_f71                          run     3/3     0 100%  31%      0    3.12s    1.18s  5.7s  0.0%  1.2 KiB/s   3 msgs/s  1.1 KiB/s  0 Bytes/s  142.7 KiB   396 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  124.6 KiB  396 Files    0 Bytes    0 Files  118.49s  135.0 MiB  191.9 MiB  928.8 MiB        4.30       0.68
+subscribe/rabbitmqtt_f31                 run     3/3     0 100%   8%      0    4.27s    1.10s  1.1s  0.0%  2.8 KiB/s   3 msgs/s 598 Bytes/s  0 Bytes/s  326.0 KiB   421 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  123 Files    0 Bytes    0 Files  118.15s  126.2 MiB  167.9 MiB  248.9 MiB        2.22       0.27
+subscribe/u_sftp_f60                     run     3/3     0 100%   9%      0    2.69s    1.71s  1.2s  0.2%  2.8 KiB/s   3 msgs/s 599 Bytes/s  0 Bytes/s  326.6 KiB   421 msgs      1 msg     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  122 Files    0 Bytes    0 Files  117.93s  126.4 MiB  167.8 MiB  249.2 MiB        2.05       0.33
+watch/f40                                run     1/1     0 100%   0%      0    0.06s    0.02s  1.6s  0.0%  3.0 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs    74 msgs     0 msgs  169.5 KiB   203 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   57.21s   46.3 MiB   65.3 MiB  311.8 MiB        1.95       0.14
+winnow/t00_f10                           run     1/1     0 100%   0%      0    6.38s    3.47s  0.2s 51.7%  1.7 KiB/s   2 msgs/s  0 Bytes/s  0 Bytes/s   66.8 KiB   116 msgs    60 msgs     0 msgs   32.3 KiB    56 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   57.79s   42.5 MiB   56.2 MiB   83.3 MiB        0.77       0.12
+winnow/t01_f10                           run     1/1     0 100%   0%      0    9.75s    2.53s  0.1s 51.3%  1.8 KiB/s   2 msgs/s  0 Bytes/s  0 Bytes/s   67.2 KiB   117 msgs    60 msgs     0 msgs   32.8 KiB    57 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   56.37s   42.3 MiB   55.7 MiB   83.2 MiB        0.81       0.12
+      Total Running Configs:  25 ( Processes: 64 missing: 0 stray: 0 )
+                     Memory: uss:2.5 GiB rss:3.4 GiB vms:7.4 GiB
+                   CPU Time: User:53.06s System:7.00s
+       Pub/Sub Received: 99 msgs/s (63.2 KiB/s), Sent:  53 msgs/s (29.3 KiB/s) Queued: 3 Retry: 523, Mean lag: 4.54s
+          Data Received: 18 Files/s (9.3 KiB/s), Sent: 3 Files/s (1.3 KiB/s)
+
+
+

The first row of output gives categories for the following line:

+
    +
  • Processes: report on the number of instances and, and whether any are missing.

  • +
  • Connection: indicators of health of connectivity with remote.

  • +
  • Lag: the degree of delay being experienced, or how old data is by the time downloads are complete.

  • +
  • Last: indicates last successful transfer (for components that move files) or last message received/posted.

  • +
  • Rates: the speed of transfers, various measures.

  • +
  • Counters: the basis for rate calculations, reset each housekeeping interval.

  • +
  • Memory: the memory usage of the processes in the configuration.

  • +
  • Cpu Time: the cpu time usage of the processes in the configuration.

  • +
+

The last three categories will only be listed when the –full option is provided.

+

The second row of output gives detailed headings within each category:

+

The configurations are listed on the left. For each configuraion, the state +will be:

+
    +
  • stop: no processes are running.

  • +
  • run: all processes are running.

  • +
  • part: some processes are running.

  • +
  • disa: disabled, configured not to run.

  • +
+

The next columns to the right give more information, detailing how many processes are Running, out of the number expected. +For example, 3/3 means 3 processes or instances found of the 3 expected to be found. +The Expected entry lists how many processes should be running based on the configuration, and whether it is stopped +or not.

+

The Retry column is the number of notification messages stored in the local retry queue, indicating what channels are having +processing difficulties.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Heading

Purpose

State

General health/status of a given configuration: stop|run|disa|part|wVip|fore

Run

Number of processes or instances running compared to expected. 3/10 3 processes running of 10 expected.

Retry

The number of messages in retry queues, indicating problems with transfers.

msg

The percentage of time connected to a message broker, to subscribe to or publish messages.

data

The percentrage of time connected to data sources or sinks to transfer data.

LagQueued

The number of messages queued on the (remote) broker, not yet picked up.

LagMax

The maximum age of messages on receipt (prior to download), so delay upstream.

LagAvg

The average age of messages on receipt (prior to download), so delay upstream.

Last

Elapsed time since the last successful file (or message) transfer.

pubsub

The rate of the pub/sub messages being downloaded in bytes/second

messages

The rate of the pub/sub messages being downloaded in messages/second

%rej

The percent of downloaded messages rejected (indicator of health of subscription tuning.)

RxData

The amount of data being downloaded (not messages, but file data.)

TxData

The amount of data being sent (not messages, but file data.)

subBytes

Counter of the bytes of pub/sub messages being received.

Accepted

Counter of number of messages accepted

Rejected

Counter of number of messages rejected

Malformed

Counter of messages which were discarded because they could not be understood.

pubBytes

Counter of bytes of messages being published

pubMsgs

Counter of messages being published

pubMal

Counter of message which failed to publish.

rxData

Counter of bytes of files being downloaded.

rxFiles

Counter of files being downloaded.

txData

Counter of bytes of files being sent.

txFiles

Counter of files being sent.

Since

How many seconds since the last counter reset (basis for rate calculations.)

uss

unique set size (memory usage of instances.) Actual physical unique memory used by processes.

rss

resident set size (memory usage of instances) Actual physical memory used, including shared.

vms

virtual memory size of all shared and physical and swap allocated together.

user

user cpu time used

system

system cpu time used.

+

At the bottom of the listing is a cumulation of these values.

+
+
+
+
+

Message Gathering

+

Most Metpx Sarracenia components loop on gathering and/or reception of sarracenia +notification messages. Usually, the notification messages are sr_post(7) +notification messages, announcing the availability of a file by publishing its URL, +but there are also report messages which can be processed using the +same tools. AMQP messages are published to an exchange +on a broker (AMQP server). The exchange delivers notification messages to queues. To receive +notification messages, one must provide the credentials to connect to the broker. Once +connected, a consumer needs to create a queue to hold pending notification messages. +The consumer must then bind the queue to one or more exchanges so that they put +notification messages in its queue.

+

Once the bindings are set, the program can receive notification messages. When a notification message is received, +further filtering is possible using regular expressions onto the AMQP messages. +After a notification message passes this selection process, and other internal validation, the +component can run an after_accept plugin script to perform additional notification message +processing. If this plugin returns False, the notification message is discarded. If True, +processing continues.

+

The following sections explains all the options to set this “consuming” part of +sarracenia programs.

+
+

Setting the Broker

+

broker [amqp|mqtt]{s}://<user>:<password>@<brokerhost>[:port]/<vhost>

+

A URI is used to configure a connection to a notification message pump, either +an MQTT or an AMQP broker. Some Sarracenia components set a reasonable default for +that option. provide the normal user,host,port of connections. In most configuration files, +the password is missing. The password is normally only included in the credentials.conf file.

+

Sarracenia work has not used vhosts, so vhost should almost always be /.

+

for more info on the AMQP URI format: ( https://www.rabbitmq.com/uri-spec.html )

+

either in the default.conf or each specific configuration file. +The broker option tell each component which broker to contact.

+

broker [amqp|mqtt]{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

+
+
::

(default: None and it is mandatory to set it )

+
+
+

Once connected to an AMQP broker, the user needs to bind a queue +to exchanges and topics to determine the notification messages of interest.

+
+
+

Creating the Queue

+

Once connected to an AMQP broker, the user needs to create a queue. +Common settings for the queue on broker :

+
    +
  • queue <name> (default: q_<brokerUser>.<programName>.<configName>)

  • +
  • expire <duration> (default: 5m == five minutes. RECOMMEND OVERRIDING)

  • +
  • message_ttl <duration> (default: None)

  • +
  • prefetch <N> (default: 1)

  • +
+

Usually components guess reasonable defaults for all these values +and users do not need to set them. For less usual cases, the user +may need to override the defaults. The queue is where the notifications +are held on the server for each subscriber.

+
+

[ queue|queue_name|qn <name>]

+

By default, components create a queue name that should be unique. The +default queue_name components create follows the following convention:

+
+

q_<brokerUser>.<programName>.<configName>.<random>.<random>

+
+

Where:

+
    +
  • brokerUser is the username used to connect to the broker (often: anonymous )

  • +
  • programName is the component using the queue (e.g. sr_subscribe ),

  • +
  • configName is the configuration file used to tune component behaviour.

  • +
  • random is just a series of characters chosen to avoid clashes from multiple +people using the same configurations

  • +
+

Users can override the default provided that it starts with q_<brokerUser>.

+

When multiple instances are used, they will all use the same queue, for trivial +multi-tasking. If multiple computers have a shared home file system, then the +queue_name is written to:

+
+

~/.cache/sarra/<programName>/<configName>/<programName>_<configName>_<brokerUser>.qname

+
+

Instances started on any node with access to the same shared file will use the +same queue. Some may want use the queue_name option as a more explicit method +of sharing work across multiple nodes.

+
+
+
+

AMQP QUEUE BINDINGS

+

Once one has a queue, it must be bound to an exchange. +Users almost always need to set these options. Once a queue exists +on the broker, it must be bound to an exchange. Bindings define which +notification messages (URL notifications) the program receives. The root of the topic +tree is fixed to indicate the protocol version and type of the +notification message (but developers can override it with the topicPrefix +option.)

+

These options define which notification messages (URL notifications) the program receives:

+
+
    +
  • exchange <name> (default: xpublic)

  • +
  • exchangeSuffix <name> (default: None)

  • +
  • topicPrefix <amqp pattern> (default: v03 – developer option)

  • +
  • subtopic <amqp pattern> (no default, must appear after exchange)

  • +
+
+
+

subtopic <amqp pattern> (default: #)

+

Within an exchange’s postings, the subtopic setting narrows the product selection. +To give a correct value to the subtopic, +one has the choice of filtering using subtopic with only AMQP’s limited wildcarding and +length limited to 255 encoded bytes, or the more powerful regular expression +based accept/reject mechanisms described below. The difference being that the +AMQP filtering is applied by the broker itself, saving the notices from being delivered +to the client at all. The accept/reject patterns apply to notification messages sent by the +broker to the subscriber. In other words, accept/reject are client side filters, +whereas subtopic is server side filtering.

+

It is best practice to use server side filtering to reduce the number of notification messages sent +to the client to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all.

+

topicPrefix is primarily of interest during protocol version transitions, +where one wishes to specify a non-default protocol version of notification messages to +subscribe to.

+

Usually, the user specifies one exchange, and several subtopic options. +Subtopic is what is normally used to indicate notification messages of interest. +To use the subtopic to filter the products, match the subtopic string with +the relative path of the product.

+

For example, consuming from DD, to give a correct value to subtopic, one can +browse the our website http://dd.weather.gc.ca and write down all directories +of interest. For each directory tree of interest, write a subtopic +option as follow:

+
+

subtopic directory1.*.subdirectory3.*.subdirectory5.#

+
+
where:
+      *                matches a single directory name
+      #                matches any remaining tree of directories.
+
+
+
+
note:

When directories have these wild-cards, or spaces in their names, they +will be URL-encoded ( ‘#’ becomes %23 ) +When directories have periods in their name, this will change +the topic hierarchy.

+
+
FIXME:

hash marks are URL substituted, but did not see code for other values. +Review whether asterisks in directory names in topics should be URL-encoded. +Review whether periods in directory names in topics should be URL-encoded.

+
+
+
+
+

One can use multiple bindings to multiple exchanges as follows:

+
exchange A
+subtopic directory1.*.directory2.#
+
+exchange B
+subtopic *.directory4.#
+
+
+

Will declare two separate bindings to two different exchanges, and two different file trees. +While default binding is to bind to everything, some brokers might not permit +clients to set bindings, or one might want to use existing bindings. +One can turn off queue binding as follows:

+
subtopic None
+
+
+

(False, or off will also work.)

+
+
+
+

Client-side Filtering

+

We have selected our notification messages through exchange, subtopic and +perhaps patterned subtopic with AMQP’s limited wildcarding which +is all done by the broker (server-side). The broker puts the +corresponding notification messages in our queue. The subscribed component +downloads these notification messages. Once the notification message is downloaded, Sarracenia +clients apply more flexible client side filtering using regular expressions.

+
+

Brief Introduction to Regular Expressions

+

Regular expressions are a very powerful way of expressing pattern matches. +They provide extreme flexibility, but in these examples we will only use a +very trivial subset: The . is a wildcard matching any single character. If it +is followed by an occurrence count, it indicates how many letters will match +the pattern. The * (asterisk) character, means any number of occurrences. +So:

+
+
    +
  • .* means any sequence of characters of any length. In other words, match anything.

  • +
  • cap.* means any sequence of characters that starts with cap.

  • +
  • .*CAP.* means any sequence of characters with CAP somewhere in it.

  • +
  • .*cap means any sequence of characters that ends with CAP. In the case +where multiple portions of the string could match, the longest one is selected.

  • +
  • .*?cap same as above, but non-greedy, meaning the shortest match is chosen.

  • +
+
+

Please consult various internet resources for more information on the full +variety of matching possible with regular expressions:

+
+
+
+
+

accept, reject and accept_unmatch

+
    +
  • accept <regexp pattern> (optional)

  • +
  • reject <regexp pattern> (optional)

  • +
  • acceptUnmatched <boolean> (default: True)

  • +
  • baseUrl_relPath <boolean> (default: False)

  • +
+

The accept and reject options process regular expressions (regexp). +The regexp is applied to the the notification message’s URL for a match.

+

If the notification message’s URL of a file matches a reject pattern, the notification message +is acknowledged as consumed to the broker and skipped.

+

One that matches an accept pattern is processed by the component.

+

In many configurations, accept and reject options are mixed +with the directory option. They then relate accepted notification messages +to the directory value they are specified under.

+

After all accept / reject options are processed, normally +the notification message is acknowledged as consumed and skipped. To override that +default, set accept_unmatch to True. The accept/reject +settings are interpreted in order. Each option is processed orderly +from top to bottom. For example:

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+

In sequence #1, all files ending in ‘gif’ are rejected. In sequence #2, the accept .* (which +accepts everything) is encountered before the reject statement, so the reject has no effect.

+

It is best practice to use server side filtering to reduce the number of notification messages sent +to the component to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all. More details on how +to apply the directives follow:

+

Normally the relative path (appended to the base directory) for files which are downloaded +will be set according to the relPath header included in the notification message. if baseUrl_relPath +is set, however, the notification message’s relPath will be prepended with the sub-directories from +the notification message’s baseUrl field.

+
+
+
+

NAMING QUEUES

+

While in most common cases, a good value is generated by the application, in some cases +there may be a need to override those choices with an explicit user specification. +To do that, one needs to be aware of the rules for naming queues:

+
    +
  1. queue names start with q_

  2. +
  3. this is followed by <amqpUserName> (the owner/user of the queue’s broker username)

  4. +
  5. followed by a second underscore ( _ )

  6. +
  7. followed by a string of the user’s choice.

  8. +
+

The total length of the queue name is limited to 255 bytes of UTF-8 characters.

+
+
+
+

POSTING

+

Just as many components consumer a stream of notification messages, many components +(often the same ones) also product an output stream of notification messages. To make files +available to subscribers, a poster publishes the notification messages to an AMQP or +MQTT server, also called a broker. The post_broker option sets all the +credential information to connect to the output AMQP broker.

+

post_broker [amqp|mqtt]{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

+

Once connected to the source AMQP broker, the program builds notifications after +the download of a file has occurred. To build the notification and send it to +the next hop broker, the user sets these options :

+
    +
  • post_baseDir <path> (optional)

  • +
  • post_topicPrefix <pfx> (default: ‘v03’)

  • +
  • post_exchange <name> (default: xpublic)

  • +
  • post_baseUrl <url> (MANDATORY)

  • +
+

FIXME: Examples of what these are for, what they do…

+
+

NAMING EXCHANGES

+
    +
  1. Exchange names start with x

  2. +
  3. Exchanges that end in public are accessible (for reading) by any authenticated user.

  4. +
  5. Users are permitted to create exchanges with the pattern: xs_<amqpUserName>_<whatever> such exchanges can be written to only by that user.

  6. +
  7. The system (sr_audit or administrators) create the xr_<amqpUserName> exchange as a place to send reports for a given user. It is only readable by that user.

  8. +
  9. Administrative users (admin or feeder roles) can post or subscribe anywhere.

  10. +
+

For example, xpublic does not have xs_ and a username pattern, so it can only be posted to by admin or feeder users. +Since it ends in public, any user can bind to it to subscribe to notification messages posted. +Users can create exchanges such as xs_<amqpUserName>_public which can be written to by that user (by rule 3), +and read by others (by rule 2.) A description of the conventional flow of notification messages through exchanges on a pump. +Subscribers usually bind to the xpublic exchange to get the main data feed. This is the default in sr_subscribe.

+

Another example, a user named Alice will have at least two exchanges:

+
+
    +
  • xs_Alice the exhange where Alice posts her file notifications and report messages (via many tools).

  • +
  • xr_Alice the exchange where Alice reads her report messages from (via sr_shovel).

  • +
  • Alice can create a new exchange by just posting to it (with sr3_post or sr_cpost) if it meets the naming rules.

  • +
+
+

Usually an sr_sarra run by a pump administrator will read from an exchange such as xs_Alice_mydata, +retrieve the data corresponding to Alice´s post notification message, and make it available on the pump, +by re-announcing it on the xpublic exchange.

+
+
+
+

POLLING

+

Polling is doing the same job as a post, except for files on a remote server. +In the case of a poll, the post will have its url built from the pollUrl +option, with the product’s path (path/”matched file”). There is one +post per file. The file’s size is taken from the directory “ls”… but its +checksum cannot be determined, so the default identity method is “cod”, asking +clients to calculate the identity Checksum On Download.

+

To set when to poll, use the scheduled_interval or scheduled_hour and scheduled_minute +settings. for example:

+
scheduled_interval 30m
+
+
+

to poll the remote resources every thirty minutes. Alternatively:

+
scheduled_hour 1,13,19
+scheduled_minute 27
+
+
+

specifies that poll be run at 1:27, 13:27, and 19:27 each day.

+

By default, sr_poll sends its post notification message to the broker with default exchange +(the prefix xs_ followed by the broker username). The post_broker is mandatory. +It can be given incomplete if it is well defined in the credentials.conf file.

+

Refer to sr3_post(1) - to understand the complete notification process. +Refer to sr_post(7) - to understand the complete notification format.

+
+
These options set what files the user wants to be notified for and where

it will be placed, and under which name.

+
+
+
    +
  • path <path> (default: .)

  • +
  • accept <regexp pattern> [rename=] (must be set)

  • +
  • reject <regexp pattern> (optional)

  • +
  • permDefault <integer> (default: 0o400)

  • +
  • fileAgeMax <duration> (default 30d)

  • +
+

nodupe_fileAgeMax should be less than nodupe_ttl when using duplicate suppression, +to avoid re-ingesting of files that have aged out of the nodupe cache.

+

The option path defines where to get the files on the server. +Combined with accept / reject options, the user can select the +files of interest and their directories of residence.

+

The accept and reject options use regular expressions (regexp) to match URL. +These options are processed sequentially. +The URL of a file that matches a reject pattern is not published. +Files matching an accept pattern are published.

+

The path can have some patterns. These supported patterns concern date/time . +They are fixed…

+

${YYYY} current year +${MM} current month +${JJJ} current julian +${YYYYMMDD} current date

+

${YYYY-1D} current year - 1 day +${MM-1D} current month - 1 day +${JJJ-1D} current julian - 1 day +${YYYYMMDD-1D} current date - 1 day

+
ex.   path /mylocaldirectory/myradars
+      path /mylocaldirectory/mygribs
+      path /mylocaldirectory/${YYYYMMDD}/mydailies
+
+      accept    .*RADAR.*
+      reject    .*Reg.*
+      accept    .*GRIB.*
+      accept    .*observations.*
+
+
+

The permDefault option allows users to specify a linux-style numeric octal +permission mask:

+
permDefault 040
+
+
+

means that a file will not be posted unless the group has read permission +(on an ls output that looks like: —r—–, like a chmod 040 <file> command). +The permDefault options specifies a mask, that is the permissions must be +at least what is specified.

+

As with all other components, the vip option can be used to indicate +that a poll should be active on only a single node in a cluster. Note that +other nodes participating in the poll, when they don’t have the vip, +will subscribe to the output of the poll to keep their duplicate suppression +caches current.

+

files that are more than fileAgeMax are ignored. However, this +can be modified to any specified time limit in the configurations by using +the option fileAgeMax <duration>. By default in components +other than poll, it is disabled by being set to zero (0). As it is a +duration option, units are in seconds by default, but minutes, hours, +days, and weeks, are available. In the poll component, fileAgeMax +defaults to 30 days.

+
+

Advanced Polling

+

The built-in Poll lists remote directories and parses the lines returned building +paramiko.SFTPAttributes structures (similar to os.stat) for each file listed. +There is a wide variety of customization available because resources to poll +are so disparate:

+
    +
  • one can implement a sarracenia.flowcb callback with a poll routine +to support such services, replacing the default poller.

  • +
  • Some servers have non-standard results when listing files, so one can +subclass a sarracenia.flowcb.poll callback with the on_line +entry point to normalize their responses and still be able to use the +builtin polling flow.

  • +
  • There are many http servers that provide disparately formatted +listings of files, so that sometimes rather than reformatting individual +lines, a means of overriding the parsing of an entire page is needed. +The on_html_page entry point in sarracenia.flowcb.poll can be +modified by subclassing as well.

  • +
  • There are other servers that provide different services, not covered +by the default poll. One can implement additional sarracenia.transfer +classes to add understanding of them to poll.

  • +
+

The output of a poll is a list of notification messages built from the file names +and SFTPAttributes records, which can then be filtered by elements +after gather in the algorithm.

+
+
+
+

COMPONENTS

+

All the components do some combination of polling, consuming, and posting. +with variations that accomplish either forwarding of notification messages or +data transfers. The components all apply the same single algorithm, +just starting from different default settings to match common use +cases.

+
+

CPUMP

+

cpump* is an implementation of the shovel component in C. +On an individual basis, it should be faster than a single python downloader, +with some limitations.

+
+
    +
  • doesn’t download data, only circulates posts. (shovel, not subscribe)

  • +
  • runs as only a single instance (no multiple instances).

  • +
  • does not support any plugins.

  • +
  • does not support vip for high availability.

  • +
  • different regular expression library: POSIX vs. python.

  • +
  • does not support regex for the strip command (no non-greedy regex).

  • +
+
+

It can therefore usually, but not always, act as a drop-in replacement for shovel and winnow

+

The C implementation may be easier to make available in specialized environments, +such as High Performance Computing, as it has far fewer dependencies than the python version. +It also uses far less memory for a given role. Normally the python version +is recommended, but there are some cases where use of the C implementation is sensible.

+

sr_cpump connects to a broker (often the same as the posting broker) +and subscribes to the notifications of interest. If _suppress_duplicates_ is active, +on reception of a post, it looks up the notification message’s integity field in its cache. If it is +found, the file has already come through, so the notification is ignored. If not, then +the file is new, and the sum is added to the cache and the notification is posted.

+
+
+

FLOW

+

Flow is the parent class from which all of the other components except cpost and cpump are built. +Flow has no built-in behaviour. Settings can make it act like any other python component, +or it can be used to build user defined components. Typically used with the flowMain option +to run a user defined flow subclass.

+
+
+

POLL

+

poll is a component that connects to a remote server to +check in various directories for some files. When a file is +present, modified or created in the remote directory, the program will +notify about the new product.

+

The notification protocol is defined here sr_post(7)

+

poll connects to a broker. Every scheduled_interval seconds (or can used +combination of scheduled_hour and scheduled_minute) , it connects to +a pollUrl (sftp, ftp, ftps). For each of the directory defined, it lists +the contents. Polling is only intended to be used for recently modified +files. The fileAgeMax option eliminates files that are too old +from consideration. When a file is found that matches a pattern given +by accept, poll builds a notification message for that product.

+

The notification message is then checked in the duplicate cache (time limited by +nodupe_ttl option.) to prevent posting of files which have already +been seen.

+

poll can be used to acquire remote files in conjunction with an sarra +subscribed to the posted notifications, to download and repost them from a data pump.

+

The pollUrl option specify what is needed to connect to the remote server

+

pollUrl protocol://<user>@<server>[:port]

+
+
::

(default: None and it is mandatory to set it )

+
+
+

The pollUrl should be set with the minimum required information… +sr_poll uses pollUrl setting not only when polling, but also +in the sr3_post notification messages produced.

+

For example, the user can set :

+

pollUrl ftp://myself@myserver

+

And complete the needed information in the credentials file with the line :

+

ftp://myself:mypassword@myserver:2121 passive,binary

+

Poll gathers information about remote files, to build notification messages about them. +The gather method that is built-in uses sarracenia.transfer protocols, +currently implemented are sftp, ftp, and http.

+
+

Repeated Scans and VIP

+

When multiple servers are being co-operating to poll a remote server, +the vip setting is used to decide which server will actually poll. +All servers participating subscribe to where poll is posting, +and use the results to fill in the duplicate suppression cache, so +that if the VIP moves, the alternate servers have current indications +of what was posted.

+
+
+
+

POST or WATCH

+

sr3_post posts the availability of a file by creating an notification message. +In contrast to most other sarracenia components that act as daemons, +sr3_post is a one shot invocation which posts and exits. +To make files available to subscribers, sr3_post sends the notification messages +to an AMQP or MQTT server, also called a broker.

+

There are many options for detection changes in directories, for +a detailed discussion of options in Sarracenia, see DetectFileReady.rst

+

This manual page is primarily concerned with the python implementation, +but there is also an implementation in C, which works nearly identically. +Differences:

+
+
    +
  • plugins are not supported in the C implementation.

  • +
  • C implementation uses POSIX regular expressions, python3 grammar is slightly different.

  • +
  • when the sleep option ( used only in the C implementation) is set to > 0, +it transforms sr_cpost into a daemon that works like watch.

  • +
+
+

The watch component is used to monitor directories for new files. +It is equivalent to post (or cpost) with the sleep option set to >0.

+

The [-pbu|–post_baseUrl url,url,…] option specifies the location +subscribers will download the file from. There is usually one post per file. +Format of argument to the post_baseUrl option:

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+file:
+
+
+

When several urls are given as a comma separated list to post_baseUrl, the +url´s provided are used round-robin style, to provide a coarse form of load balancing.

+

The [-p|–path path1 path2 .. pathN] option specifies the path of the files +to be announced. There is usually one post per file. +Format of argument to the path option:

+
/absolute_path_to_the/filename
+or
+relative_path_to_the/filename
+
+
+

The -pipe option can be specified to have sr3_post read path names from standard +input as well.

+

An example invocation of sr3_post:

+
sr3_post -pb amqp://broker.com -pbu sftp://stanley@mysftpserver.com/ -p /data/shared/products/foo
+
+
+

By default, sr3_post reads the file /data/shared/products/foo and calculates its checksum. +It then builds a notification message, logs into broker.com as user ‘guest’ (default credentials) +and sends the post to defaults vhost ‘/’ and default exchange. The default exchange +is the prefix xs_ followed by the broker username, hence defaulting to ‘xs_guest’. +A subscriber can download the file /data/shared/products/foo by authenticating as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials. +The output of the command is as follows

+
[INFO] Published xs_guest v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' sum=d,82edc8eb735fd99598a1fe04541f558d parts=1,4574,1,0,0
+
+
+

In MetPX-Sarracenia, each post is published under a certain topic. +The log line starts with ‘[INFO]’, followed by the topic of the +post. Topics in AMQP are fields separated by dot. Note that MQTT topics use +a slash (/) as the topic separator. The complete topic starts with +a topicPrefix (see option), version v03, +followed by a subtopic (see option) here the default, the file path separated with dots +data.shared.products.foo.

+

The second field in the log line is the notification message notice. It consists of a time +stamp 20150813161959.854, and the source URL of the file in the last 2 fields.

+

The rest of the information is stored in AMQP message headers, consisting of key=value pairs. +The sum=d,82edc8eb735fd99598a1fe04541f558d header gives file fingerprint (or checksum +) information. Here, d means md5 checksum performed on the data, and 82edc8eb735fd99598a1fe04541f558d +is the checksum value. The parts=1,4574,1,0,0 state that the file is available in 1 part of 4574 bytes +(the filesize.) The remaining 1,0,0 is not used for transfers of files with only one part.

+

Another example:

+
sr3_post -pb amqp://broker.com -pbd /data/web/public_data -pbu http://dd.weather.gc.ca/ -p bulletins/alphanumeric/SACN32_CWAO_123456
+
+
+

By default, sr3_post reads the file /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concatenating the post_baseDir and relative path of the source url to obtain the local file path) +and calculates its checksum. It then builds a notification message, logs into broker.com as user ‘guest’ +(default credentials) and sends the post to defaults vhost ‘/’ and exchange ‘xs_guest’.

+

A subscriber can download the file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca.

+
+

File Partitioning

+

use of the blocksize option has no effect in sr3. It is used to do file partitioning, +and it will become effective again in the future, with the same semantics.

+
+
+
+

SARRA

+

sarra is a program that Subscribes to file notifications, +Acquires the files and ReAnnounces them at their new locations. +The notification protocol is defined here sr_post(7)

+

sarra connects to a broker (often the same as the remote file server +itself) and subscribes to the notifications of interest. It uses the notification +information to download the file on the local server it’s running on. +It then posts a notification for the downloaded files on a broker (usually on the local server).

+

sarra can be used to acquire files from sr3_post(1) +or watch or to reproduce a web-accessible folders (WAF), +that announce its products.

+

The sr_sarra is an sr_subscribe(1) with the following presets:

+
mirror True
+
+
+
+

Specific Consuming Requirements

+

If the notification messages are posted directly from a source, the exchange used is ‘xs_<brokerSourceUsername>’. +To protect against malicious users, administrators should set sourceFromExchange to True. +Such notification messages may not contain a source nor an origin cluster fields +or a malicious user may set the values incorrectly.

+
    +
  • sourceFromExchange <boolean> (default: False)

  • +
+

Upon reception, the program will set these values in the parent class (here +cluster is the value of option cluster taken from default.conf):

+

msg[‘source’] = <brokerUser> +msg[‘from_cluster’] = cluster

+

overriding any values present in the notification message. This setting +should always be used when ingesting data from a +user exchange.

+
+
+
+

SENDER

+

sender is a component derived from subscribe +used to send local files to a remote server using a file transfer protocol, primarily SFTP. +sender is a standard consumer, using all the normal AMQP settings for brokers, exchanges, +queues, and all the standard client side filtering with accept, reject, and after_accept.

+

Often, a broker will announce files using a remote protocol such as HTTP, +but for the sender it is actually a local file. In such cases, one will +see a notification message: ERROR: The file to send is not local. +An after_accept plugin will convert the web url into a local file one:

+
baseDir /var/httpd/www
+flowcb sarracenia.flowcb.tolocalfile.ToLocalFile
+
+
+

This after_accept plugin is part of the default settings for senders, but one +still needs to specify baseDir for it to function.

+

If a post_broker is set, sender checks if the clustername given +by the to option if found in one of the notification message’s destination clusters. +If not, the notification message is skipped.

+
+

SETUP 1 : PUMP TO PUMP REPLICATION

+

For pump replication, mirror is set to True (default).

+

baseDir supplies the directory path that, when combined with the relative +one in the selected notification gives the absolute path of the file to be sent. +The default is None which means that the path in the notification is the absolute one.

+

In a subscriber, the baseDir represents the prefix to the relative path on the upstream +server, and is used as a pattern to be replaced in the currently selected base directory +(from a baseDir or directory option) in the notification message fields: ‘fileOp’, +which are used when mirroring symbolic links, or files that are renamed.

+

The sendTo defines the protocol and server to be used to deliver the products. +Its form is a partial url, for example: ftp://myuser@myhost +The program uses the file ~/.conf/sarra/credentials.conf to get the remaining details +(password and connection options). Supported protocol are ftp, ftps and sftp. Should the +user need to implement another sending mechanism, he would provide the plugin script +through option do_send.

+

On the remote site, the post_baseDir serves the same purpose as the +baseDir on this server. The default is None which means that the delivered path +is the absolute one.

+

Now we are ready to send the product… for example, if the selected notification looks like this :

+

20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product

+

sr_sender performs the following pseudo-delivery:

+

Sends local file [baseDir]/relative/path/to/IMPORTANT_product +to sendTo/[post_baseDir]/relative/path/to/IMPORTANT_product +(kbytes_ps is greater than 0, the process attempts to respect +this delivery speed… ftp,ftps,or sftp)

+

At this point, a pump-to-pump setup needs to send the remote notification… +(If the post_broker is not set, there will be no posting… just products replication)

+

The selected notification contains all the right information +(topic and header attributes) except for url field in the +notice… in our example : http://this.pump.com/

+

By default, sr_sender puts the sendTo in that field. +The user can overwrite this by specifying the option post_baseUrl. For example:

+

post_baseUrl http://remote.apache.com

+

The user can provide an on_post script. Just before the notification message is +published on the post_broker and post_exchange, the +on_post script is called… with the sr_sender class instance as an argument. +The script can perform whatever you want… if it returns False, the notification message will not +be published. If True, the program will continue processing from there.

+

FIXME: Missing example configuration.

+
+
+

DESTINATION SETUP 2 : METPX-SUNDEW LIKE DISSEMINATION

+

In this type of usage, we would not usually repost… but if the +post_broker and post_exchange (url,**on_post**) are set, +the product will be announced (with its possibly new location and new name). +Let’s reintroduce the options in a different order +with some new ones to ease explanation.

+

There are 2 differences with the previous case : +the directory, and the filename options.

+

The baseDir is the same, and so are the +sendTo and the post_baseDir options.

+

The directory option defines another “relative path” for the product +at its sendTo. It is tagged to the accept options defined after it. +If another sequence of directory/accept follows in the configuration file, +the second directory is tagged to the following accepts and so on.

+

The accept/reject patterns apply to notification message notice url as above. +Here is an example, here some ordered configuration options :

+
directory /my/new/important_location
+
+accept .*IMPORTANT.*
+
+directory /my/new/location/for_others
+
+accept .*
+
+
+

If the notification selected is, as above, this :

+

20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product

+

It was selected by the first accept option. The remote relative path becomes +/my/new/important_location … and sr_sender performs the following pseudo-delivery:

+

sends local file [baseDir]/relative/path/to/IMPORTANT_product +to sendTo/[post_baseDir]/my/new/important_location/IMPORTANT_product

+

Usually this way of using sr_sender would not require posting of the product. +But if post_broker and post_exchange are provided, and url , as above, is set to +http://remote.apache.com, then sr_sender would reconstruct :

+

Topic: v03.my.new.important_location.IMPORTANT_product

+

Notice: 20150813161959.854 http://remote.apache.com/ my/new/important_location/IMPORTANT_product

+
+
+
+

SHOVEL

+

shovel copies notification messages on one broker (given by the broker option) to +another (given by the post_broker option.) subject to filtering +by (exchange, subtopic, and optionally, accept/reject.)

+

The topicPrefix option must to be set to:

+
+
+
+

shovel is a flow with the following presets:

+
no-download True
+suppress_duplicates off
+
+
+
+
+

SUBSCRIBE

+

Subscribe is the normal downloading flow component, that will connect to a broker, download +the configured files, and then forward the notification messages with an altered baseUrl.

+
+
+

WATCH

+

Watches a directory and publishes posts when files in the directory change +( added, modified, or deleted). Its arguments are very similar to sr3_post. +In the MetPX-Sarracenia suite, the main goal is to post the availability and readiness +of one’s files. Subscribers use sr_subscribe to consume the post and download the files.

+

Posts are sent to an AMQP server, also called a broker, specified with the option [ -pb|–post_broker broker_url ].

+

The [-post_baseUrl|–pbu|–url url] option specifies the protocol, credentials, host and port to which subscribers +will connect to get the file.

+

Format of argument to the url option:

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+file:
+
+
+

The [-p|–path path] option tells watch what to look for. +If the path specifies a directory, watches creates a post for any time +a file in that directory is created, modified or deleted. +If the path specifies a file, watch watches only that file. +In the notification message, it is specified with the path of the product. +There is usually one post per file.

+

FIXME: in version 3 does it work at all without a configuration? +perhaps we should just create the file if it isn’t there?

+

An example of an execution of watch checking a file:

+
sr3 --post_baseUrl sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo --post_broker amqp://broker.com start watch/myflow
+
+
+

Here, watch checks events on the file /data/shared/products/foo. +Default events settings reports if the file is modified or deleted. +When the file gets modified, watch reads the file /data/shared/products/foo +and calculates its checksum. It then builds a notification message, logs into broker.com as user ‘guest’ (default credentials) +and sends the post to defaults vhost ‘/’ and post_exchange ‘xs_stanley’ (default exchange)

+

A subscriber can download the file /data/shared/products/foo by logging as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials.

+

The output of the command is as follows

+
[INFO] v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo'
+      source=guest parts=1,256,1,0,0 sum=d,fc473c7a2801babbd3818260f50859de
+
+
+

In MetPX-Sarracenia, each post is published under a certain topic. +After the ‘[INFO]’ the next information gives the fBtopic* of the +post. Topics in AMQP are fields separated by dot. In MetPX-Sarracenia +it is made of a topicPrefix by default : version v03 , +followed by the subtopic by default : the file path separated with dots, here, data.shared.products.foo

+

After the topic hierarchy comes the body of the notification. It consists of a time 20150813161959.854 , +and the source url of the file in the last 2 fields.

+

The remaining line gives informations that are placed in the amqp message header. +Here it consists of source=guest , which is the amqp user, parts=1,256,0,0,1 , +which suggests to download the file in 1 part of 256 bytes (the actual filesize), trailing 1,0,0 +gives the number of block, the remaining in bytes and the current +block. sum=d,fc473c7a2801babbd3818260f50859de mentions checksum information,

+

here, d means md5 checksum performed on the data, and fc473c7a2801babbd3818260f50859de +is the checksum value. When the event on a file is a deletion, sum=R,0 R stands for remove.

+

Another example watching a file:

+
sr3 --post_baseDir /data/web/public_data --post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric/SACN32_CWAO_123456 -post_broker amqp://broker.com start watch/myflow
+
+
+

By default, watch checks the file /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concatenating the baseDir and relative path of the source url to obtain the local file path). +If the file changes, it calculates its checksum. It then builds a notification message, logs into broker.com as user ‘guest’ +(default credentials) and sends the post to defaults vhost ‘/’ and post_exchange ‘sx_guest’ (default post_exchange)

+

A subscriber can download the file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca.

+

An example checking a directory:

+
sr3 -post_baseDir /data/web/public_data -post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric --post_broker amqp://broker.com start watch/myflow
+
+
+

Here, watch checks for file creation(modification) in /data/web/public_data/bulletins/alphanumeric +(concatenating the baseDir and relative path of the source url to obtain the directory path). +If the file SACN32_CWAO_123456 is being created in that directory, watch calculates its checksum. +It then builds a notification message, logs into broker.com as user ‘guest’

+

A subscriber can download the created/modified file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca.

+
+
+

WINNOW

+

the winnow component subscribes to file notification messages and reposts them, suppressing redundant +ones. How to decide which ones are redundant varies by use case. In the most straight-forward case, +the messages have Identity header stores a file’s fingerprint as described in the sr_post(7) man page, +and header is used exclusively. There are many other use cases, though. discussed in the following section +on Duplicate Suppression

+

winnow has the following options forced:

+
no-download True
+suppress_duplicates on
+accept_unmatch True
+
+
+

The suppress_duplicates lifetime can be adjusted, but it is always on. +Other components can use the same duplicate suppression logic. The use of winnow components +is purely to make it easier for analysts to understand a configuration at a glance.

+

winnow connects to a broker (often the same as the posting broker) +and subscribes to the notifications of interest. On reception of a notification, +it looks up its sum in its cache. If it is found, the file has already come through, +so the notification is ignored. If not, then the file is new, and the sum is added +to the cache and the notification is posted.

+

winnow can be used to trim notification messages produced by post, sr3_post, +sr3_cpost, poll or watch etc… It is used when there are multiple sources of the same data, so that clients +only download the source data once, from the first source that posted it.

+
+
+
+

Configurations

+

If one has a ready made configuration called q_f71.conf, it can be +added to the list of known ones with:

+
subscribe add q_f71.conf
+
+
+

In this case, xvan_f14 is included with examples provided, so add finds it in the examples +directory and copies into the active configuration one. +Each configuration file manages the consumers for a single queue on +the broker. To view the available configurations, use:

+
$ subscribe list
+
+  configuration examples: ( /usr/lib/python3/dist-packages/sarra/examples/subscribe )
+            all.conf     all_but_cap.conf            amis.conf            aqhi.conf             cap.conf      cclean_f91.conf
+      cdnld_f21.conf       cfile_f44.conf        citypage.conf       clean_f90.conf            cmml.conf cscn22_bulletins.conf
+        ftp_f70.conf            gdps.conf         ninjo-a.conf           q_f71.conf           radar.conf            rdps.conf
+           swob.conf           t_f30.conf      u_sftp_f60.conf
+
+  user plugins: ( /home/peter/.config/sarra/plugins )
+        destfn_am.py         destfn_nz.py       msg_tarpush.py
+
+  general: ( /home/peter/.config/sarra )
+          admin.conf     credentials.conf         default.conf
+
+  user configurations: ( /home/peter/.config/sarra/subscribe )
+     cclean_f91.conf       cdnld_f21.conf       cfile_f44.conf       clean_f90.conf         ftp_f70.conf           q_f71.conf
+          t_f30.conf      u_sftp_f60.conf
+
+
+

one can then modify it using:

+
$ subscribe edit q_f71.conf
+
+
+

(The edit action uses the EDITOR environment variable, if present.) +Once satisfied, one can start the the configuration running:

+
$ subscibe foreground q_f71.conf
+
+
+

What goes into the files? See next section:

+
    +
  • The forward slash (/) as the path separator in Sarracenia configuration files on all +operating systems. Use of the backslash character as a path separator (as used in the +cmd shell on Windows) may not work properly. When files are read on Windows, the path +separator is immediately converted to the forward slash, so all pattern matching, +in accept, reject, strip etc… directives should use forward slashes when a path +separator is needed.

  • +
+

Example:

+
directory A
+accept X
+
+
+

Places files matching X in directory A.

+
+
vs::

accept X +directory A

+
+
+

Places files matching X in the current working directory, and the directory A setting +does nothing in relation to X.

+

To provide non-functional descriptions of configurations, or comments, use lines that begin with a #.

+

All options are case sensitive. Debug is not the same as debug nor DEBUG. +Those are three different options (two of which do not exist and will have no effect, +but should generate an ´unknown option warning´).

+

Options and command line arguments are equivalent. Every command line argument +has a corresponding long version starting with ‘–’. For example -u has the +long form –url. One can also specify this option in a configuration file. +To do so, use the long form without the ‘–’, and put its value separated by a space. +The following are all equivalent:

+
+
    +
  • url <url>

  • +
  • -u <url>

  • +
  • –url <url>

  • +
+
+

Settings are interpreted in order. Each file is read from top to bottom. +For example:

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+
+

Note

+

FIXME: does this match only files ending in ‘gif’ or should we add a $ to it? +will it match something like .gif2 ? is there an assumed .* at the end?

+
+

In sequence #1, all files ending in ‘gif’ are rejected. In sequence #2, the +accept .* (which accepts everything) is encountered before the reject statement, +so the reject has no effect. Some options have global scope, rather than being +interpreted in order. for thoses cases, a second declaration overrides the first.

+

Options to be reused in different config files can be grouped in an include file:

+
+
    +
  • –include <includeConfigPath>

  • +
+
+

The includeConfigPath would normally reside under the same config dir of its +master configs. If a URL is supplied as an includeConfigPATH, then a remote +configuraiton will be downloaded and cached (used until an update on the server +is detected.) See Remote Configurations for details.

+

Environment variables, and some built-in variables can also be put on the +right hand side to be evaluated, surrounded by ${..} The built-in variables are:

+
+
    +
  • ${BROKER_USER} - the user name for authenticating to the broker (e.g. anonymous)

  • +
  • ${PROGRAM} - the name of the component (subscribe, shovel, etc…)

  • +
  • ${CONFIG} - the name of the configuration file being run.

  • +
  • ${HOSTNAME} - the hostname running the client.

  • +
  • ${RANDID} - a random id that will be consistent within a single invocation.

  • +
+
+
+
+

flowCallbacks

+

Sarracenia makes extensive use of small python code snippets that customize +processing called flowCallback Flow_callbacks define and use additional settings:

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

There is also a shorter form to express the same thing:

+
callback log
+
+
+

Either way, the module refers to the sarracenia/flowcb/msg/log.py file in the +installed package. In that file, the Log class is the one searched for entry +points. The log.py file included in the package is like this:

+
from sarracenia.flowcb import FlowCB
+import logging
+
+logger = logging.getLogger( __name__ )
+
+class Log(Plugin):
+
+  def after_accept(self,worklist):
+      for msg in worklist.incoming:
+          logger.info( "msg/log received: %s " % msg )
+      return worklist
+
+
+

It’s a normal python class, declared as a child of the sarracenia.flowcb.FlowCB +class. The methods (function names) in the plugin describe when +those routines will be called. For more details consult the +Programmer’s Guide

+

To add special processing of notification messages, create a module in the python +path, and have it include entry points.

+

There is also flowCallbackPrepend which adds a flowCallback class to the front +of the list (which determines relative execution order among flowCallback classes.)

+
+

callback options

+

callbacks that are delivered with metpx-sr3 follow the following convention:

+
    +
  • they are placed in the sarracenia/flowcb directory tree.

  • +
  • the name of the primary class is the same as the name of file containing it.

  • +
+

so we provide the following syntactic sugar:

+
callback log    (is equivalent to *flowCallback sarracenia.flowcb.log.Log* )
+
+
+

There is similarly a callback_prepend to fill in.

+
+
+

Importing Extensions

+

The import option works in a way familiar to Python developers, +Making them available for use by the Sarracenia core, or flowCallback. +Developers can add additional protocols for notification messages or +file transfer. For example:

+
import torr
+
+
+

would be a reasonable name for a Transfer protocol to retrieve +resources with bittorrent protocol. A skeleton of such a thing +would look like this:

+
import logging
+
+logger = logging.getLogger(__name__)
+
+import sarracenia.transfer
+
+class torr(sarracenia.transfer.Transfer):
+    pass
+
+logger.warning("loading")
+
+
+

For more details on implementing extensions, consult the +Programmer’s Guide

+
+
+

Deprecated v2 plugins

+

There is and older (v2) style of plugins as well. That are usually +prefixed with the name of the plugin:

+
msg_to_clusters DDI
+msg_to_clusters DD
+
+on_message msg_to_clusters
+
+
+

A setting ‘msg_to_clusters’ is needed by the msg_to_clusters plugin +referenced in the on_message the v2 plugins are a little more +cumbersome to write. They are included here for completeness, but +people should use version 3 (either flowCallback, or extensions +discussed next) when possible.

+

Reasons to use newer style plugins:

+
    +
  • Support for running v2 plugins is accomplished using a flowcb +called v2wrapper. It performs a lot of processing to wrap up +the v3 data structures to look like v2 ones, and then has +to propagate the changes back. It’s a bit expensive.

  • +
  • newer style extensions are ordinary python modules, unlike +v2 ones which require minor magic incantations.

  • +
  • when a v3 (flowCallback or imported) module has a syntax error, +all the tools of the python interpreter apply, providing +a lot more feedback is given to the coder. with v2, it just +says there is something wrong, much more difficult to debug.

  • +
  • v3 api is strictly more powerful than v2, as it works +on groups of notification messages, rather than individual ones.

  • +
+
+
+

Environment Variables

+

On can also reference environment variables in configuration files, +using the ${ENV} syntax. If Sarracenia routines needs to make use +of an environment variable, then they can be set in configuration files:

+
declare env HTTP_PROXY=localhost
+
+
+
+
+

LOGS and MONITORING

+
    +
  • +
    debug

    Setting option debug is identical to use logLevel debug

    +
    +
    +
  • +
  • logMessageDump (default: off) boolean flag +if set, all fields of a notification message are printed, rather than just a url/path reference.

  • +
  • +
    logEvents ( default after_accept,after_work,on_housekeeping )

    emit standard log messages at the given points in notification message processing. +other values: on_start, on_stop, post, gather, … etc…

    +
    +
    +
  • +
  • +
    logLevel ( default: info )

    The level of logging as expressed by python’s logging. Possible values are : critical, error, info, warning, debug.

    +
    +
    +
  • +
  • –logStdout ( default: False ) EXPERIMENTAL FOR DOCKER use case

    +
    +

    The logStdout disables log management. Best used on the command line, as there is +some risk of creating stub files before the configurations are completely parsed:

    +
    sr3 --logStdout start
    +
    +
    +

    All launched processes inherit their file descriptors from the parent. so all output is like an interactive session.

    +

    This is in contrast to the normal case, where each instance takes care of its logs, rotating and purging periodically. +In some cases, one wants to have other software take care of logs, such as in docker, where it is preferable for all +logging to be to standard output.

    +

    It has not been measured, but there is a reasonable likelihood that use of logStdout with large configurations (dozens +of configured instances/processes) will cause either corruption of logs, or limit the speed of execution of all processes +writing to stdout.

    +
    +
  • +
  • +
    log_reject <True|False> ( default: False )

    print a log message when rejecting notification messages (choosing not to download the corresponding files)

    +
    +
    +
  • +
  • +
    log <dir> ( default: ~/.cache/sarra/log ) (on Linux)

    The directory to store log files in.

    +
    +
    +
  • +
  • +
    statehost <False|True> ( default: False )

    In large data centres, the home directory can be shared among thousands of +nodes. Statehost adds the node name after the cache directory to make it +unique to each node. So each node has it’s own statefiles and logs. +example, on a node named goofy, ~/.cache/sarra/log/ becomes ~/.cache/sarra/goofy/log.

    +
    +
    +
  • +
  • +
    logRotateCount <max_logs> ( default: 5 )

    Maximum number of logs archived.

    +
    +
    +
  • +
  • +
    logRotateInterval <duration>[<time_unit>] ( default: 1d = 86,400s )

    The duration of the interval with an optional time unit (ie 5m, 2h, 3d)

    +
    +
    +
  • +
  • +
    permLog ( default: 0600 )

    The permission bits to set on log files.

    +
    +
    +
  • +
+

See the Subscriber Guide <../How2Guides/subscriber.rst> for a more detailed discussion of logging +options and techniques.

+
+
+

CREDENTIALS

+

One normally does not specify passwords in configuration files. Rather they are placed +in the credentials file:

+
edit ~/.config/sr3/credentials.conf
+
+
+

For every url specified that requires a password, one places +a matching entry in credentials.conf. +The broker option sets all the credential information to connect to the RabbitMQ server

+
    +
  • broker amqp{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

  • +
+
(default: amqps://anonymous:anonymous@dd.weather.gc.ca/ )
+
+
+

For all sarracenia programs, the confidential parts of credentials are stored +only in ~/.config/sarra/credentials.conf. This includes the url and broker +passwords and settings needed by components. The format is one entry per line. Examples:

+
    +
  • amqp://user1:password1@host/

  • +
  • amqps://user2:password2@host:5671/dev

  • +
  • amqps://usern:passwd@host/ login_method=PLAIN

  • +
  • sftp://user5:password5@host

  • +
  • sftp://user6:password6@host:22 ssh_keyfile=/users/local/.ssh/id_dsa

  • +
  • ftp://user7:password7@host passive,binary

  • +
  • ftp://user8:password8@host:2121 active,ascii

  • +
  • ftps://user7:De%3Aize@host passive,binary,tls

  • +
  • ftps://user8:%2fdot8@host:2121 active,ascii,tls,prot_p

  • +
  • https://ladsweb.modaps.eosdis.nasa.gov/ bearer_token=89APCBF0-FEBE-11EA-A705-B0QR41911BF4

  • +
+

In other configuration files or on the command line, the url simply lacks the +password or key specification. The url given in the other files is looked +up in credentials.conf.

+
+

Credential Details

+

You may need to specify additional options for specific credential entries. These details can be added after the end of the URL, with multiple details separated by commas (see examples above).

+

Supported details:

+
    +
  • ssh_keyfile=<path> - (SFTP) Path to SSH keyfile

  • +
  • passive - (FTP) Use passive mode

  • +
  • active - (FTP) Use active mode

  • +
  • binary - (FTP) Use binary mode

  • +
  • ascii - (FTP) Use ASCII mode

  • +
  • ssl - (FTP) Use SSL/standard FTP

  • +
  • tls - (FTP) Use FTPS with TLS

  • +
  • prot_p - (FTPS) Use a secure data connection for TLS connections (otherwise, clear text is used)

  • +
  • bearer_token=<token> (or bt=<token>) - (HTTP) Bearer token for authentication

  • +
  • login_method=<PLAIN|AMQPLAIN|EXTERNAL|GSSAPI> - (AMQP) By default, the login method will be automatically determined. This can be overriden by explicity specifying a login method, which may be required if a broker supports multiple methods and an incorrect one is automatically selected.

  • +
+
+
Note::

SFTP credentials are optional, in that sarracenia will look in the .ssh directory +and use the normal SSH credentials found there.

+

These strings are URL encoded, so if an account has a password with a special +character, its URL encoded equivalent can be supplied. In the last example above, +%2f means that the actual password isi: /dot8 +The next to last password is: De:olonize. ( %3a being the url encoded value for a colon character. )

+
+
+
+
+
+
+

PERIODIC PROCESSING

+

Most processing occurs on receipt of a notification message, but there is some periodic maintenance +work that happens every housekeeping interval (default is 5 minutes.) Evey housekeeping, all of the +configured on_housekeeping plugins are run. By default there are three present:

+
+
    +
  • log - prints “housekeeping” in the log.

  • +
  • nodupe - ages out old entries in the reception cache, to minimize its size.

  • +
  • memory - checks the process memory usage, and restart if too big.

  • +
+
+

The log will contain notification messages from all three plugins every housekeeping interval, and +if additional periodic processing is needed, the user can configure addition +plugins to run with the on_housekeeping option.

+
+

sanity_log_dead <interval> (default: 1.5*housekeeping)

+

The sanity_log_dead option sets how long to consider too long before restarting +a component.

+
+
+

nodup_ttl <off|on|999> (default: off)

+

The cleanup of expired elements in the duplicate suppression store happens at +each housekeeping.

+
+
+
+

ERROR RECOVERY

+

The tools are meant to work well unattended, and so when transient errors occur, they do +their best to recover elegantly. There are timeouts on all operations, and when a failure +is detected, the problem is noted for retry. Errors can happen at many times:

+
+
    +
  • Establishing a connection to the broker.

  • +
  • losing a connection to the broker

  • +
  • establishing a connection to the file server for a file (for download or upload.)

  • +
  • losing a connection to the server.

  • +
  • during data transfer.

  • +
+
+

Initially, the programs try to download (or send) a file a fixed number (attempts, default: 3) times. +If all three attempts to process the file are unsuccessful, then the file is placed in an instance’s +retry file. The program then continues processing of new items. When there are no new items to +process, the program looks for a file to process in the retry queue. It then checks if the file +is so old that it is beyond the retry_expire (default: 2 days). If the file is not expired, then +it triggers a new round of attempts at processing the file. If the attempts fail, it goes back +on the retry queue.

+

This algorithm ensures that programs do not get stuck on a single bad product that prevents +the rest of the queue from being processed, and allows for reasonable, gradual recovery of +service, allowing fresh data to flow preferentially, and sending old data opportunistically +when there are gaps.

+

While fast processing of good data is very desirable, it is important to slow down when errors +start occurring. Often errors are load related, and retrying quickly will just make it worse. +Sarracenia uses exponential back-off in many points to avoid overloading a server when there +are errors. The back-off can accumulate to the point where retries could be separated by a minute +or two. Once the server begins responding normally again, the programs will return to normal +processing speed.

+
+
+

EXAMPLES

+

Here is a short complete example configuration file:

+
broker amqps://dd.weather.gc.ca/
+
+subtopic model_gem_global.25km.grib2.#
+accept .*
+
+
+

This above file will connect to the dd.weather.gc.ca broker, connecting as +anonymous with password anonymous (defaults) to obtain notification messages about +files in the http://dd.weather.gc.ca/model_gem_global/25km/grib2 directory. +All files which arrive in that directory or below it will be downloaded +into the current directory (or just printed to standard output if -n option +was specified.)

+

A variety of example configuration files are available here:

+
+
+
+

QUEUES and MULTIPLE STREAMS

+

When executed, subscribe chooses a queue name, which it writes +to a file named after the configuration file given as an argument to subscribe +with a .queue suffix ( .”configfile”.queue). +If subscribe is stopped, the posted notification messages continue to accumulate on the +broker in the queue. When the program is restarted, it uses the queuename +stored in that file to connect to the same queue, and not lose any notification messages.

+

File downloads can be parallelized by running multiple subscribes using +the same queue. The processes will share the queue and each download +part of what has been selected. Simply launch multiple instances +of subscribe in the same user/directory using the same configuration file.

+

You can also run several subscribe with different configuration files to +have multiple download streams delivering into the the same directory, +and that download stream can be multi-streamed as well.

+
+

Note

+

While the brokers keep the queues available for some time, Queues take resources on +brokers, and are cleaned up from time to time. A queue which is not accessed for +a long (implementation dependent) period will be destroyed. A queue which is not +accessed and has too many (implementation defined) files queued will be destroyed. +Processes which die should be restarted within a reasonable period of time to avoid +loss of notifications.

+
+
+
+

report and report_exchange

+

For each download, by default, an amqp report message is sent back to the broker. +This is done with option :

+
    +
  • report <boolean> (default: True)

  • +
  • report_exchange <report_exchangename> (default: xreport|xs_*username* )

  • +
+

When a report is generated, it is sent to the configured report_exchange. Administrative +components post directly to xreport, whereas user components post to their own +exchanges (xs_*username*). The report daemons then copy the notification messages to xreport after validation.

+

These reports are used for delivery tuning and for data sources to generate statistical information. +Set this option to False, to prevent generation of reports.

+
+
+

INSTANCES

+

Sometimes one instance of a component and configuration is not enough to process & send all available notifications.

+

instances <integer> (default:1)

+

The instance option allows launching several instances of a component and configuration. +When running sender for example, a number of runtime files are created. +In the ~/.cache/sarra/sender/configName directory:

+
A .sender_configname.state         is created, containing the number instances.
+A .sender_configname_$instance.pid is created, containing the PID  of $instance process.
+
+
+

In directory ~/.cache/sarra/log:

+
A .sender_configname_$instance.log  is created as a log of $instance process.
+
+
+
+

Note

+

known bug in the management interface sr <sr.8.rst>_ means that instance should +always be in the .conf file (not a .inc) and should always be an number +(not a substituted variable or other more complex value.

+
+
+

Note

+

FIXME: indicate Windows location also… dot files on Windows?

+
+
+

Note

+

While the brokers keep the queues available for some time, Queues take resources on +brokers, and are cleaned up from time to time. A queue which is not +accessed and has too many (implementation defined) files queued will be destroyed. +Processes which die should be restarted within a reasonable period of time to avoid +loss of notifications. A queue which is not accessed for a long (implementation dependent) +period will be destroyed.

+
+
+

vip - ACTIVE/PASSIVE OPTIONS

+

sr3 can be used on a single server node, or multiple nodes +could share responsibility. Some other, separately configured, high availability +software presents a vip (virtual ip) on the active server. Should +the server go down, the vip is moved on another server. +Both servers would run sr3. It is for that reason that the +following options were implemented:

+
+
    +
  • vip <string> (None)

  • +
+
+

When you run only one sr3 on one server, these options are not set, +and sr3 will run in ‘standalone mode’.

+

In the case of clustered brokers, you would set the options for the +moving vip.

+

vip 153.14.126.3

+

When sr3 does not find the vip, it sleeps for 5 seconds and retries. +If it does find the vip, it consumes and processes a notification message and than rechecks for the vip. +if multiple vips are specified, then posession of any of the vips is sufficient +to be declared active.

+
+
+

[–blocksize <value>] (default: 0 (auto))

+

This blocksize option controls the partitioning strategy used to post files. +The value should be one of:

+
0 - autocompute an appropriate partitioning strategy (default)
+1 - always send entire files in a single part.
+<blocksize> - used a fixed partition size (example size: 1M )
+
+
+

Files can be announced as multiple parts. Each part has a separate checksum. +The parts and their checksums are stored in the cache. Partitions can traverse +the network separately, and in parallel. When files change, transfers are +optimized by only sending parts which have changed.

+

The outlet option allows the final output to be other than a post. +See sr3_cpump(1) for details.

+
+
+

[-pbd|–post_baseDir <path>] (optional)

+

The post_baseDir option supplies the directory path that, when combined (or found) +in the given path, gives the local absolute path to the data file to be posted. +The post_baseDir part of the path will be removed from the posted notification message. +For sftp urls it can be appropriate to specify a path relative to a user account. +Example of that usage would be: -pbd ~user -url sftp:user@host +For file: url’s, baseDir is usually not appropriate. To post an absolute path, +omit the -pbd setting, and just specify the complete path as an argument.

+
+
+

post_baseUrl <url> (MANDATORY)

+

The post_baseUrl option sets how to get the file… it defines the protocol, +host, port, and optionally, the user. It is best practice to not include +passwords in urls.

+
+
+

post_exchange <name> (default: xpublic)

+

The post_exchange option set under which exchange the new notification +will be posted. In most cases it is ‘xpublic’.

+

Whenever a publish happens for a product, a user can set to trigger a script. +The option on_post would be used to do such a setup.

+
+
+

post_exchangeSplit <number> (default: 0)

+

The post_exchangeSplit option appends a two digit suffix resulting from +hashing the last character of the checksum to the post_exchange name, +in order to divide the output amongst a number of exchanges. This is currently used +in high traffic pumps to allow multiple instances of winnow, which cannot be +instanced in the normal way. Example:

+
post_exchangeSplit 5
+post_exchange xwinnow
+
+
+

will result in posting notification messages to five exchanges named: xwinnow00, xwinnow01, +xwinnow02, xwinnow03 and xwinnow04, where each exchange will receive only one fifth +of the total flow.

+
+
+
+

Remote Configurations

+

One can specify URI’s as configuration files, rather than local files. Example:

+
+
    +
  • –config http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

On startup, subscribe checks if the local file cap.conf exists in the +local configuration directory. If it does, then the file will be read to find +a line like so:

+
+
    +
  • –remote_config_url http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

In which case, it will check the remote URL and compare the modification time +of the remote file against the local one. The remote file is not newer, or cannot +be reached, then the component will continue with the local file.

+

If either the remote file is newer, or there is no local file, it will be downloaded, +and the remote_config_url line will be prepended to it, so that it will continue +to self-update in future.

+
+
+

Extensions

+

One can override or add functionality with python scripting.

+

Sarracenia comes with a variety of example plugins, and uses some to implement base functionality, +such as logging (implemented by default use of msg_log, file_log, post_log plugins):

+
$ sr3 list fcb
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py          flowcb/filter/pclean_f90.py      flowcb/filter/pclean_f92.py
+flowcb/gather/file.py            flowcb/gather/message.py         flowcb/line_log.py               flowcb/line_mode.py
+flowcb/log.py                    flowcb/nodupe.py                 flowcb/pclean.py                 flowcb/post/message.py
+flowcb/retry.py                  flowcb/sample.py                 flowcb/script.py                 flowcb/v2wrapper.py
+flowcb/work/rxpipe.py
+$
+
+
+

Users can place their own scripts in the script sub-directory of their config directory +tree ( on Linux, the ~/.config/sarra/plugins).

+
+

flowCallback and flowCallbackPrepend <class>

+

The flowCallback directive takes a class to load can scan for entry points as an argument:

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

With this directive in a configuration file, the Log class found in installed package will be used. +That module logs messages after_accept (after notification messages have passed through the accept/reject masks.) +and the after_work (after the file has been downloaded or sent). Here is the source code +for that callback class:

+
import json
+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+class Log(FlowCB):
+  def __init__(self, options):
+
+      # FIXME: should a logging module have a logLevel setting?
+      #        just put in a cookie cutter for now...
+      if hasattr(options, 'logLevel'):
+          logger.setLevel(getattr(logging, options.logLevel.upper()))
+      else:
+          logger.setLevel(logging.INFO)
+
+  def after_accept(self, worklist):
+      for msg in worklist.incoming:
+          logger.info("accepted: %s " % msg_dumps(msg) )
+
+  def after_work(self, worklist):
+      for msg in worklist.ok:
+          logger.info("worked successfully: %s " % msg.dumps() )
+
+
+

If you have multiple callbacks configured, they will be called in the same order they are +configuration file. components in sr3 are often differentiated by the callbacks configured. +For example, a watch is a flow with sarracenia.flowcb.gather.file.File class that +is used to scan directories. A Common need when a data source is not easily accessed +with python scripts is to use the script callback:

+
flowCallbackPrepend sarracenia.flowcb.script.Script
+
+script_gather get_weird_data.sh
+
+
+

Using the _prepend variant of the flowCallback option, will have the Script callback +class’s entry point, before the File callback… So A script will be executed, and then +the directory will be checked for new files. Here is part of the Script callback class:

+
import logging
+from sarracenia.flowcb import FlowCB
+import subprocess
+
+logger = logging.getLogger(__name__)
+
+
+class Script(FlowCB):
+
+   .
+   .
+   .
+
+    def run_script( self, script ):
+        try:
+            subprocess.run( self.o.script_gather, check=True )
+        except Exception as err:
+            logging.error("subprocess.run failed err={}".format(err))
+            logging.debug("Exception details:", exc_info=True)
+
+
+    def gather(self ):
+        if hasattr( self.o, 'script_gather') and self.o.script_gather is not None :
+            self.run_script( self.o.script_gather )
+        return []
+
+
+
+
+
+

Identity

+

One can use the import directive to add new checksum algorithms by sub-classing +sarracenia.identity.Identity.

+
+
+

Transfer

+

One can add support for additional methods of downloading data by sub-classing +sarracenia.transfer.Transfer.

+

Transfer protocol scripts should be declared using the import option. +Aside the targetted built-in function(s), a module registered_as that defines +a list of protocols that these functions provide. Example :

+
+
def registered_as(self) :

return [‘ftp’,’ftps’]

+
+
+

See the Programming Guide for more information on Extension development.

+
+
+

ROLES - feeder/admin/declare

+

of interest only to administrators

+

Administrative options are set using:

+
edit ~/.config/sr3/admin.conf
+
+
+

The feeder option specifies the account used by default system transfers for components such as +shovel, sarra and sender (when posting).

+
    +
  • feeder amqp{s}://<user>:<pw>@<post_brokerhost>[:port]/<vhost>

  • +
  • admin <name> (default: None)

  • +
+

The admin user is used to do maintenance operations on the pump such as defining +the other users. Most users are defined using the declare option. The feeder can also be declared in that +way.

+
    +
  • declare <role> <name> (no defaults)

  • +
+
+

subscriber

+
+

A subscriber is user that can only subscribe to data and return report messages. Subscribers are +not permitted to inject data. Each subscriber has an xs_<user> named exchange on the pump, +where if a user is named Acme, the corresponding exchange will be xs_Acme. This exchange +is where an subscribe process will send its report messages.

+

By convention/default, the anonymous user is created on all pumps to permit subscription without +a specific account.

+
+
+
+

source

+
+

A user permitted to subscribe or originate data. A source does not necessarily represent +one person or type of data, but rather an organization responsible for the data produced. +So if an organization gathers and makes available ten kinds of data with a single contact +email or phone number for questions about the data and its availability, then all of +those collection activities might use a single ‘source’ account.

+

Each source gets a xs_<user> exchange for injection of data posts, and, similar to a subscriber +to send report messages about processing and receipt of data. Source may also have an xl_<user> +exchange where, as per report routing configurations, report messages of consumers will be sent.

+
+
+
+

feeder

+
+

A user permitted to write to any exchange. Sort of an administrative flow user, meant to pump +notification messages when no ordinary source or subscriber is appropriate to do so. Is to be used in +preference to administrator accounts to run flows.

+
+

User credentials are placed in the credentials files, and audit will update +the broker to accept what is specified in that file, as long as the admin password is +already correct.

+
+
+
+

CONFIGURATION FILES

+

While one can manage configuration files using the add, remove, +list, edit, disable, and enable actions, one can also do all +of the same activities manually by manipulating files in the settings +directory. The configuration files for an sr_subscribe configuration +called myflow would be here:

+
+
    +
  • linux: ~/.config/sarra/subscribe/myflow.conf (as per: XDG Open Directory Specication )

  • +
  • Windows: %AppDir%/science.gc.ca/sarra/myflow.conf , this might be: +C:UserspeterAppDataLocalscience.gc.casarramyflow.conf

  • +
  • MAC: FIXME.

  • +
+
+

The top of the tree has ~/.config/sarra/default.conf which contains settings that +are read as defaults for any component on start up. In the same directory, ~/.config/sarra/credentials.conf +contains credentials (passwords) to be used by sarracenia ( CREDENTIALS for details. )

+

One can also set the XDG_CONFIG_HOME environment variable to override default placement, or +individual configuration files can be placed in any directory and invoked with the +complete path. When components are invoked, the provided file is interpreted as a +file path (with a .conf suffix assumed.) If it is not found as a file path, then the +component will look in the component’s config directory ( config_dir / component ) +for a matching .conf file.

+

If it is still not found, it will look for it in the site config dir +(linux: /usr/share/default/sarra/component).

+

Finally, if the user has set option remote_config to True and if he has +configured web sites where configurations can be found (option remote_config_url), +The program will try to download the named file from each site until it finds one. +If successful, the file is downloaded to config_dir/Downloads and interpreted +by the program from there. There is a similar process for all plugins that can +be interpreted and executed within sarracenia components. Components will first +look in the plugins directory in the users config tree, then in the site +directory, then in the sarracenia package itself, and finally it will look remotely.

+
+
+

SUNDEW COMPATIBILITY OPTIONS

+

For compatibility with Sundew, there are some additional delivery options which can be specified.

+
+

destfn_script <script> (default:None)

+

This option defines a script to be run when everything is ready +for the delivery of the product. The script receives the sr_sender class +instance. The script takes the parent as an argument, and for example, any +modification to parent.msg.new_file will change the name of the file written locally.

+
+
+

filename <keyword> (default:WHATFN)

+

From metpx-sundew the support of this option give all sorts of possibilities +for setting the remote filename. Some keywords are based on the fact that +metpx-sundew filenames are five (to six) fields strings separated by for colons.

+

The default value on Sundew is NONESENDER, but in the interest of discouraging use +of colon separation in files, the default in Sarracenia is WHATFN

+

The possible keywords are :

+
+
None
    +
  • no Sundew-style filename processing. Leave the filename alone. +(Not the same as NONE, described below.)

  • +
+
+
WHATFN
    +
  • the first part of the Sundew filename (string before first :)

  • +
+
+
HEADFN
    +
  • HEADER part of the sundew filename

  • +
+
+
SENDER
    +
  • the Sundew filename may end with a string SENDER=<string> in this case the <string> will be the remote filename

  • +
+
+
NONE
    +
  • deliver with the complete Sundew filename (without :SENDER=…)

  • +
+
+
NONESENDER
    +
  • deliver with the complete Sundew filename (with :SENDER=…)

  • +
+
+
TIME
    +
  • time stamp appended to filename. Example of use: WHATFN:TIME

  • +
+
+
DESTFN=str
    +
  • direct filename declaration str

  • +
+
+
SATNET=1,2,3,A
    +
  • cmc internal satnet application parameters

  • +
+
+
DESTFNSCRIPT=script.py
    +
  • invoke a script (same as destfn_script) to generate the name of the file to write

  • +
+
+
+

accept <regexp pattern> [<keyword>]

+

keyword can be added to the accept option. The keyword is any one of the filename +options. A notification message that matched against the accept regexp pattern, will have its remote_file +plied this keyword option. This keyword has priority over the preceeding filename one.

+

The regexp pattern can be use to set directory parts if part of the notification message is put +to parenthesis. sr_sender can use these parts to build the directory name. The +rst enclosed parenthesis strings will replace keyword ${0} in the directory name… +the second ${1} etc.

+

Example of use:

+
filename NONE
+
+directory /this/first/target/directory
+
+accept .*file.*type1.*
+
+directory /this/target/directory
+
+accept .*file.*type2.*
+
+accept .*file.*type3.*  DESTFN=file_of_type3
+
+directory /this/${0}/pattern/${1}/directory
+
+accept .*(2016....).*(RAW.*GRIB).*
+
+
+

A selected notification message by the first accept would be delivered unchanged to the first directory.

+

A selected notification message by the second accept would be delivered unchanged to the second directory.

+

A selected notification message by the third accept would be renamed “file_of_type3” in the second directory.

+

A selected notification message by the forth accept would be delivered unchanged to a directory.

+

It’s named /this/20160123/pattern/RAW_MERGER_GRIB/directory if the notification message would have a notice like:

+

20150813161959.854 http://this.pump.com/ relative/path/to/20160123_product_RAW_MERGER_GRIB_from_CMC

+
+
+

Field Replacements

+

In MetPX Sundew, there is a much more strict file naming standard, specialised for use with +World Meteorological Organization (WMO) data. Note that the file naming convention predates, and +bears no relation to the WMO file naming convention currently approved, but is strictly an internal +format. The files are separated into six fields by colon characters. The first field, DESTFN, +gives the WMO (386 style) Abbreviated Header Line (AHL) with underscores replacing blanks:

+
TTAAii CCCC YYGGGg BBB ...
+
+
+

(see WMO manuals for details) followed by numbers to render the product unique (as in practice, +though not in theory, there are a large number of products which have the same identifiers). +The meanings of the fifth field is a priority, and the last field is a date/time stamp. +The other fields vary in meaning depending on context. A sample file name:

+
SACN43_CWAO_012000_AAA_41613:ncp1:CWAO:SA:3.A.I.E:3:20050201200339
+
+
+

If a file is sent to sarracenia and it is named according to the Sundew conventions, then the +following substitution fields are available:

+
${T1}    replace by bulletin's T1
+${T2}    replace by bulletin's T2
+${A1}    replace by bulletin's A1
+${A2}    replace by bulletin's A2
+${ii}    replace by bulletin's ii
+${CCCC}  replace by bulletin's CCCC
+${YY}    replace by bulletin's YY   (obs. day)
+${GG}    replace by bulletin's GG   (obs. hour)
+${Gg}    replace by bulletin's Gg   (obs. minute)
+${BBB}   replace by bulletin's bbb
+${RYYYY} replace by reception year
+${RMM}   replace by reception month
+${RDD}   replace by reception day
+${RHH}   replace by reception hour
+${RMN}   replace by reception minutes
+${RSS}   replace by reception second
+
+
+

The ‘R’ fields come from the sixth field, and the others come from the first one. +When data is injected into sarracenia from Sundew, the sundew_extension notification message header +will provide the source for these substitions even if the fields have been removed +from the delivered file names.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Concepts.html b/Explanation/Concepts.html new file mode 100644 index 000000000..f2124fd56 --- /dev/null +++ b/Explanation/Concepts.html @@ -0,0 +1,533 @@ + + + + + + + General Sarracenia Concepts — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

General Sarracenia Concepts

+

Sarracenia pumps form a network. The network uses Message Queueing Protocol (MQP) brokers as a transfer manager which sends notification messages of file availability in one direction and report messages in the opposite direction. Administrators configure the paths that data flows through at each pump, as each broker acts independently, managing transfers from transfer engines it can reach, with no knowledge of the overall network. The locations of pump and the directions of traffic flow are chosen to work with permitted flows. Ideally, no firewall exceptions are needed.

+

Sarracenia itself does no data transport. It is a management layer to co-ordinate the use of transport layers. To get a running pump, actual transport mechanisms (web or sftp servers) need to be set up as well (the most common for our use case being RabbitMQ). In the simplest case, all of the components are on the same server, but there is no need for that. The broker could be on a different server from both ends of a given hop of a data transfer.

+

The best way for data transfers to occur is to avoid polling, thus limiting unnecessary work and time. Suppliers of data will want to take advantage of writing appropriate sr3_post notification messages to advertise data that is ready. Similarly, it is ideal if the receivers of said data use subscribe components, and (if required) an on_file plugin to trigger their further processing. This ensures the file transfers from the source to the destination without polling for the data. This is the most efficient way of working, but it is understood that not all software can be made co-operative. Sarracenia components used to poll in order to start transport flows are known as poll, and watch.

+

Generally speaking, Linux is the main deployment platform, and the only platform on which server configurations are deployed and tested. Other platforms are used as client end points (Windows, etc.). This isn´t a limitation, it is just what is used and tested. Implementations of the pump on Windows should work, they just are not officially tested.

+

Sarracenia pumps can be built on a single server or an array of them, (see Deployment Consideration <DeploymentConsiderations.rst> for more details ) .but all the concepts discussed below apply.

+
+

The Flow Algorithm

+

All of the components (post, subscribe, sarra, sender, shovel, watch, winnow) share substantial code and differ only in default settings. Each component follows the same general algorithm known as the Flow algorithm. The steps for the Flow algorithm are:

+
    +
  • Gather a list of notification messages

  • +
  • Filter them with accept/reject clauses

  • +
  • Work on the accepted notification messages

  • +
  • Post the work accomplished for the next flow

  • +
+

In more detail:

+ + + + + + + + + + + + + + + + + + + +
Table 1: The Algorithm for All Components

PHASE

DESCRIPTION

gather

Get information about an initial list of files.

+

From: a queue, a directory, a polling script.

+

Output: worklist.incoming populated with messages.

+

Each notification message is a python dictionary.

+

Filter

Reduce the list of files to act on.

+

Apply accept/reject clauses.

+

after_accept callbacks +move messages from worklist.incoming to worklist.rejected.

+

Ones to run: flowcb/nodupe.py (runs duplicate suppresion.)

+

work

Process the message by downloading or sending.

+

run transfer (download or send.)

+

run after_work

+

post

Post notification message of file downloads/sent to +post_broker or otherwise dispose of task +(to file, or retry… or)

+

The main components of the python implementation of Sarracenia all implement the same algorithm described above. +The algorithm has various points where custom processing can be inserted (using flowCallbacks), +or deriving classes from flow, identity, or transfer classes.

+

The components just have different default settings:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Table 2: How Each Component Uses the Flow Algorithm

Component

Use of the algorithm

Key Option Settings

subscribe

+

Download files from a +pump.

+

Gather = gather.message

+

Filter

+

Work = Download +Post = optional

+

flowMain subscribe

+

mirror off +(all others it is on)

+

sarra

+

Used on pumps.

+

Download files from a +pump

+

Publish a message +downstream for +consumers

+

Subscribers to +this pump can +download in turn.

+

Gather = gather.message

+

Filter

+

Work = Download

+

Post = post

+

flowMain sarra

+

mirror on +download on

+

poll

+

Find files on other +servers to post to +a pump.

+

Uses has_vip* +(see notes below)

+

Gather +if has_vip: poll

+

Filter

+
+
if has_vip:

Work = Download

+

Post = yes

+
+
+

flowMain poll

+

pollUrl +path

+

download off +mirror on

+

scheduled_interval +scheduled_hour +schedules_minute

+

shovel

+

Move notification +messages around.

+

Gather = gather.message

+

Filter (shovel cache=off)

+

Work = nil

+

Post = yes

+

acceptUnmatched True

+

nodupe_ttl 0 +download off

+

callback gather.message

+

callback post.message

+

winnow

+

Move notification +messages around

+

Suppress duplicates +through caching and +a shared VIP

+

Gather = gather.message

+

Filter (shovel cache=on)

+

Work = nil

+

Post = yes

+

acceptUnmatched True

+

nodupe_ttl 300

+

callback gather.message

+

callback post.message

+

post/watch

+

Find file on a +local server to +publish

+

Gather = gather.file

+

Filter

+

Work = nil

+
+
Post = yes

Message?, File?

+
+
+

path /file/to/post

+

sleep -1 # for post

+

sleep 5 # for watch

+

callback gather.file

+

callback post.message

+

sender

+

Send files from a +pump somewhere else

+

Optional: +Publish message after

+

Gather = gather.message

+

Filter

+

Do = sendfile

+

flowMain sender

+

sendTo

+
+

In the left hand column, one can see the name and general description of each component. +in the middle column, we see what the various phases of the Flow algorithm are applied. +On the right, we see how to express, in a generic flow configuration file, the component. +Most components can just use the parent flow class, but those that need custom python +code use flow subclasses. Those cases are configured using the flowMain option.

+

Users can write their own flow subclasses and make their own components.

+

Components are easily composed using AMQP brokers, which create elegant networks of communicating sequential processes (in the Hoare sense).

+
+
+

Multiple processes: Instances, Singletons and VIP

+

The flow algorithm isn’t confined to a single process. Many processes can run the same flow configuration. For the sarra, sender, shovel and subscribe, components, one sets the instance setting to the number of processes to run and consume from the configured queue that they share.

+

The poll, post, and watch components, by contrast, are limited to a single process on any given server. In the table above, there is a note about has_vip. When there are multiple servers participating in a configuration, the vip directive can be used to have the servers co-operate to provide a single service (where only one server is active at a time.) For most components, the vip directive in a configuration file defines a virtual ip address that a server must have actively on it for that component to be active. If Sarracenia detects that the interface is not present on the server, then the component will run in passive mode.

+

For almost all components, passive mode means that no processing will occur. The node will passively check if it has obtained the vip. If the node does not have the vip, it will stand by indefinitely.

+

The exception to this is poll, which works differently. In poll, when you do not have the vip the following algorithmic loop will continue:

+
    +
  • gather

  • +
  • filter

  • +
  • after_accept

  • +
+

The poll’s gather (and/or poll) subscribes to the exchange other vip participants are posting to and updates its cache from the notification messages, avoiding the other polls from having to poll the same endpoint for the same file list.

+
+
+

Mapping AMQP Concepts to Sarracenia

+

It is helpful to understand a bit about AMQP to work with Sarracenia. AMQP is a vast and interesting topic in its own right. No attempt is made to explain the entirety of it here. This section intends to provide a little context and introduce background concepts needed to understand and/or use Sarracenia. For more information on AMQP itself, a set of links is maintained at the Metpx web site

+../_images/amqp_flow_concept.svg +

An AMQP Server is called a broker. A broker is sometimes used to refer to the software, other times server running the broker software (same confusion as web server.) In the above diagram, AMQP vocabulary is in Orange, and Sarracenia terms are in blue. There are many different broker software implementations. In our implementations we use rabbitmq. We are not trying to be rabbitmq specific, but it is to be noted that management functions differ between implementations.

+
+
Queues are usually taken care of transparently, but it is important to know that:
    +
  • A consumer/subscriber creates a queue to receive notification messages.

  • +
  • Consumer queues are bound to exchanges (AMQP-speak)

  • +
  • MQTT equivalent: client-id

  • +
+
+
An exchange is a matchmaker between publisher and consumer queues:
    +
  • A notification message arrives from a publisher.

  • +
  • notification message goes to the exchange, is anyone interested in this notification message?

  • +
  • in a topic based exchange, the notification message topic provides the exchange key.

  • +
  • interested: compare notification message key to the bindings of consumer queues.

  • +
  • notification message is routed to interested consumer queues, or dropped if there aren’t any.

  • +
  • concept does not exist in MQTT, used as root of the topic hierarchy.

  • +
+
+
Multiple processes can share a queue, they just take turns removing notification messages from it:
    +
  • This is used heavily for sr_sarra and sr_subcribe multiple instances.

  • +
  • Same concept is available as shared subscriptions in MQTT.

  • +
+
+
How to decide if someone is interested:
    +
  • For Sarracenia, we use (AMQP standard) topic based exchanges.

  • +
  • Subscribers indicate what topics they are interested in, and the filtering occurs server/broker side.

  • +
  • Topics are just keywords separated by a dot. Wildcards: # matches anything, * matches one word.

  • +
  • We create the topic hierarchy from the path name (mapping to AMQP syntax)

  • +
  • Resolution & syntax of server filtering is set by AMQP. (. separator, # and * wildcards)

  • +
  • Server side filtering is coarse, notification messages can be further filtered after download using regexp on the actual paths (the reject/accept directives.)

  • +
+
+
+
+

AMQP v09 (Rabbitmq) Settings

+

MetPX-Sarracenia is only a light wrapper/coating around Message Queueing Protocols. For those who are familiar with the underlying protocols, these are the mappings:

+
+
    +
  • A MetPX-Sarracenia data pump is a python AMQP application that uses a (rabbitmq) broker to co-ordinate SFTP and HTTP client data transfers, and accompanies a web server (apache) and sftp server (openssh), often on the same user-facing address.

  • +
  • A MetPX-Sarracenia data pump can also work with rabbitmq replaced by an MQTT broker such as mosquitto.org (but some administrivia must be handled manually).

  • +
  • Wherever reasonable, we use their terminology and syntax. If someone knows AMQP, they understand. If not, there are plentiful amounts of information available for research.

    +
      +
    • Users configure a broker, instead of a pump.

    • +
    • By convention, the default vhost ‘/’ is always used (did not feel the need to use other vhosts yet)

    • +
    • Users explicitly can pick their queue names (this ia a client-id in MQTT.)

    • +
    • Users set subtopic,

    • +
    • Topics with dot separator are minimally transformed, rather than encoded.

    • +
    • Queue is set to durable so that notification messages are not lost across broker restarts.

    • +
    • We use message headers (AMQP-speak for key-value pairs) rather than encoding in JSON or some other payload format.

    • +
    • expire how long to keep an idle queue or exchange around.

    • +
    +
  • +
  • +
    Reduce complexity through conventions.
      +
    • Use only one type of exchanges (Topic), take care of bindings.

    • +
    • +
      Naming conventions for exchanges and queues.
        +
      • Exchanges start with x. +- xs_Weather - the exchange for the source (mqp user) named Weather to post notification messages +- xpublic – exchange used for most subscribers.

      • +
      • Queues start with q_

      • +
      +
      +
      +
    • +
    +
    +
    +
  • +
+
+
+
+

MQTT (version =5) Settings

+

MQTT is actually a better match to Sarracenia than AMQP, as it is entirely based on hierarchical topics. While topics are only one among a variety of choices for routing methods in AMQP.

+
+
    +
  • In MQTT, topic separator is / instead of .

  • +
  • The MQTT topic wildcard # is the same as in AMQP (match rest of topic)

  • +
  • The MQTT topic wildcard + is the same as the AMQP * (match one topic.)

  • +
  • An AMQP “Exchange” is mapped to the root of the MQTT topic tree,

  • +
  • An AMQP “queue” is represented in MQTT by client-id and a shared subscription. Note: Shared subscriptions are only present in MQTTv5.

    +
      +
    • AMQP: A queue named queuename is bound to an exchange xpublic with key: v03.observations …

    • +
    • MQTT subscription: topic $shared/queuename/xpublic/v03/observations …

    • +
    +
  • +
  • Connections are clean_sesssion=0 normally, to recover notification messages when a connection is broken.

  • +
  • MQTT QoS==1 is used to assure notification messages are sent at least once, and avoid overhead +of ensuring only once.

  • +
  • AMQP prefetch mapped to MQTT receiveMaximum

  • +
  • expire has same meaning in MQTT as in AMQP.

  • +
+
+

MQTT v3 lacks shared subscriptions, and the recovery logic is quite different. Sarracenia only supports v5.

+
+
+
+

Flow Through Pumps

+../_images/sr3_flow_example.svg +

A description of the conventional flow of notification messages through exchanges on a pump:

+
    +
  • subscribers usually bind to the xpublic exchange to get the main data feed. This is the default in a subscribe component.

  • +
  • A user will have two exchanges:

    +
      +
    • xs_user the exhange where the user posts their file notifications and report messages.

    • +
    • xr_user the exchange where the user reads their report messages from.

    • +
    • Note: “user” exchanges will be whichever username the user specified. Not explicitly “xs_user” or “xr_user”.

    • +
    +
  • +
  • Usually the sarra component will read from xs_user, retrieve the data corresponding to the users post notification message, and then make it available on the pump, by re-announcing it on the xpublic exchange.

  • +
  • Administrators will have access to a xreport exchange to get system-wide monitoring. The user will not have permission to do that, they can only look at xr_user, which will have the specific report messages for only the user.

  • +
+

The purpose of these conventions is to encourage a reasonably secure means of operating. If a notification message is taken from xs_user, then the process doing the reading is responsible for ensuring that it is tagged as coming from the user on this cluster. This prevents certain types of ´spoofing´ as notification messages can only be posted by proper owners.

+
+
+

Users and Roles

+

Usernames for pump authentication are significant in that they are visible to all. They are used in the directory path on public trees, as well as to authenticate to the broker. They need to be understandable. They are often wider scope than a person… perhaps call them ‘Accounts’. It can be elegant to configure the same usernames for use in transport engines.

+

All Account names should be unique, but nothing will avoid clashes when sources originate from different pump networks and clients at different destinations. In practice, name clashes are addressed by routing to avoid two different sources’ with the same name having their data offerings combined on a single tree. On the other hand, name clashes are not always an error. Use of a common source account name on different clusters may be used to implement folders that are shared between the two accounts with the same name.

+

Pump users are defined with the declare option. Each option starts with the declare keyword, followed by the specified role and lastly the user name which has that role.

+

Role can be one of:

+
+
subscriber

A subscriber is a user that can only subscribe to data and report messages. Not permitted to inject data. Each subscriber gets an xs_<user> named exchange on the pump. If a user is named Acme, the corresponding exchange will be xs_Acme. This exchange is where an sr_subscribe process will send its report messages.

+

By convention/default, the anonymous user is created on all pumps to permit subscription without a specific account.

+
+
source

A user permitted to subscribe or originate data. A source does not necessarily represent one person or type of data, but rather an organization responsible for the data produced. So if an organization gathers and makes available ten kinds of data with a single contact email or phone number for questions about the data and it’s availability, then all of those collection activities might use a single ‘source’ account.

+

Each source gets a xs_<user> exchange for injection of data notification messages and, similar to a subscriber, to send report messages about processing and receipt of data. Each source is able to view all of the notification messages for data it has injected, but the location where all of these notification messages are available varies according to administrator configuration of report routing. A source may inject data on a pump, but may subscribe to reports on a different pump. The reports corresponding to the data the source injected are written in exchange xr_<user>.

+

When data is first injected, the path is modified by sarracenia to prepend a fixed upper part of the directory tree. The first level directory is the day of ingest into the network in YYYYMMDD format. The second level directory is the source name. So for a user Alice, injecting data on May 4th, 2016, the root of the directory tree is: 20160504/Alice. Note that all pumps are expected to run in the UTC timezone.

+

There are daily directories because there is a system-wide life-time for data, it is deleted after a standard number of days, data is just deleted from the root.

+

Since all clients will see the directories, and therefore client configurations will include them. It would be wise to consider the account name public, and relatively static.

+

Sources determine who can access their data, by specifying which cluster to send the data to.

+
+
feeder

A user permitted to subscribe or originate data, but understood to represent a pump. This local pump user would be used to run processes like sarra, report routing shovels, etc…

+
+
admin

A user permitted to manage the local pump. It is the real rabbitmq-server administrator. The administrator runs sr_audit to create/delete exchanges, users, or clean unused queues, etc.

+
+
+

Example of a complete valid admin.conf, for a host named blacklab:

+
+

cluster blacklab +admin amqps://hbic@blacklab/ +feeder amqps://feeder@blacklab/ +declare source goldenlab +declare subscriber anonymous

+
+

A corresponding credentials.conf would look like:

+
amqps://hbic:hbicpw@blacklab/
+amqps://feeder:feederpw@blacklab/
+amqps://goldenlab:puppypw@blacklab/
+amqps://anonymous:anonymous@blacklab/
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/DeploymentConsiderations.html b/Explanation/DeploymentConsiderations.html new file mode 100644 index 000000000..e4592e068 --- /dev/null +++ b/Explanation/DeploymentConsiderations.html @@ -0,0 +1,428 @@ + + + + + + + Deployment Considerations — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Deployment Considerations

+

Sarracenia data pumps are often placed in network designs near demarcation points, to provide +an application level demarcation point to allow for security scanning and to limit visibility +into different zones. Pumps may either have all services incorporated on a single server, +or commonly the main broker is on a dedicated node, and the nodes that do the data transfer +are called Transport Engines. In high performance deployments, each transport engine +may have a local broker to deal with local transfers and transformations.

+
+

Transport Engines

+

Transport engines are the data servers queried by subscribers, by the end users, or other pumps. +The subscribers read the notices and fetch the corresponding data, using the indicated protocol. +The software to serve the data can be either SFTP or HTTP (or HTTPS.) For specifics of +configuring the servers for use, please consult the documentation of the servers themselves. +Also note that additional protocols can be enabled through the use of do_ plugins, as +described in the Programming Guide.

+
+

IPv6

+

A sample pump was implemented on a small VPS with IPv6 enabled. A client +from far away connected to the rabbitmq broker using IPv6, and the +subscription to the apache httpd worked without issues. It just works. There +is no difference between IPv4 and IPv6 for sarra tools, which are agnostic of +IP addresses.

+

On the other hand, one is expected to use hostnames, since use of IP addresses +will break certificate use for securing the transport layer (TLS, aka SSL) No +testing of IP addresses in URLs (in either IP version) has been done.

+
+
+
+

Designs

+

There are many different arrangements in which sarracenia can be used.

+
+
Dataless

where one runs just sarracenia on top of a broker with no local transfer engines. +This is used, for example to run sr_winnow on a site to provide redundant data sources.

+
+
Standalone

the most obvious one, run the entire stack on a single server, openssh and a web server +as well the broker and sarra itself. Makes a complete data pump, but without any redundancy.

+
+
Switching/Routing

Where, in order to achieve high performance, a cluster of standalone nodes are placed behind +a load balancer. The load balancer algorithm is just round-robin, with no attempt to associate +a given source with a given node. This has the effect of pumping different parts of large files +through different nodes. So one will see parts of files announced by such pump, to be +re-assembled by subscribers.

+
+
Data Dissemination

Where in order to serve a large number of clients, multiple identical servers, each with a complete +mirror of data

+
+
FIXME:

ok, opened big mouth, now need to work through the examples.

+
+
+
+

Dataless or S=0

+

A configuration which includes only the AMQP broker. This configuration can be used when users +have access to disk space on both ends and only need a mediator. This is the configuration +of sftp.science.gc.ca, where the HPC disk space provides the storage so that the pump does +not need any, or pumps deployed to provide redundant HA to remote data centres.

+
+

Note

+

FIXME: sample configuration of shovels, and sr_winnow (with output to xpublic) to allow +subscribers in the SPC to obtain data from either edm or dor.

+
+

Note that while a configuration can be dataless, it can still make use of rabbitmq +clustering for high availability requirements (see rabbitmq clustering below.)

+
+
+

Winnowed Dataless

+

Another example of a dataless pump would be to provide product selection from two upstream +sources using sr_winnow. The sr_winnow is fed by shovels from upstream sources, and +the local clients just connect to this local pump. sr_winnow takes +care of only presenting the products from the first server to make +them available. One would configure sr_winnow to output to the xpublic exchange +on the pump.

+

Local subscribers just point at the output of sr_winnow on the local pump. This +is how feeds are implemented in the Storm prediction centres of ECCC, where they +may download data from whichever national hub produces data first.

+
+
+

Dataless With Sr_poll

+

The sr_poll program can verify if products on a remote server are ready or modified. +For each of the product, it emits an notification message on the local pump. One could use +sr_subscribe anywhere, listen to notification messages and get the products (provided the +credentials to access it)

+
+
+

Standalone

+

In a standalone configuration, there is only one node in the configuration. It runs all components +and shares none with any other nodes. That means the Broker and data services such as sftp and +apache are on the one node.

+

One appropriate usage would be a small non-24x7 data acquisition setup, to take responsibility of data +queuing and transmission away from the instrument. It is restarted when the opportunity arises. +It is just a matter of installing and configuring all a data flow engine, a broker, and the package +itself on a single server. The ddi systems are generally configured this way.

+
+
+

Switching/Routing

+

In a switching/routing configuration, there is a pair of machines running a +single broker for a pool of transfer engines. So each transfer engine’s view of +the file space is local, but the queues are global to the pump.

+

Note: On such clusters, all nodes that run a component with the +same config file created by default have an identical queue_name. Targetting the +same broker, it forces the queue to be shared. If it should be avoided, +the user can just overwrite the default queue_name inserting ${HOSTNAME}. +Each node will have its own queue, only shared by the node instances. +ex.: queue_name q_${BROKER_USER}.${PROGRAM}.${CONFIG}.${HOSTNAME} )

+

Often there is internal traffic of data acquired before it is finally published. +As a means of scaling, often transfer engines will also have brokers to handle +local traffic, and only publish final products to the main broker. This is how +ddsr systems are generally configured.

+
+
+
+

Security Considerations

+

This section is meant to provide insight to those who need to perform a security review +of the application prior to implementation.

+
+

Client

+

All credentials used by the application are stored +in the ~/.config/sarra/credentials.conf file, and that file is forced to 600 permissions.

+
+
+

Server/Broker

+

Authentication used by transport engines is independent of that used for the brokers. A security +assessment of rabbitmq brokers and the various transfer engines in use is needed to evaluate +the overall security of a given deployment.

+

The most secure method of transport is the use of SFTP with keys rather than passwords. Secure +storage of sftp keys is covered in documentation of various SSH or SFTP clients. The credentials +file just points to those key files.

+

For Sarracenia itself, password authentication is used to communicate with the AMQP broker, +so implementation of encrypted socket transport (SSL/TLS) on all broker traffic is strongly +recommended.

+

Sarracenia users are actually users defined on rabbitmq brokers. +Each user Alice, on a broker to which she has access:

+
+
    +
  • can create and publish to any exchange that starts with xs_Alice_

  • +
  • has an exchange xr_Alice, where she reads her report messages.

  • +
  • can configure (read from and acknowledge) queues named qs_Alice_.* to bind to exchanges

  • +
  • Alice can create and destroy her own queues and exchanges, but no-one else’s.

  • +
  • Alice can only post data that she is publishing (it will refer back to her)

  • +
  • Alice can also read (or subscribe to) any exchange whose name ends in public.

  • +
  • Alice can thus create an exchange others can subscribe to with the following name: xs_Alice_public

  • +
+
+

Alice cannot create any exchanges or other queues not shown above.

+

Rabbitmq provides the granularity of security to restrict the names of +objects, but not their types. Thus, given the ability to create a queue named q_Alice, +a malicious Alice could create an exchange named q_Alice_xspecial, and then configure +queues to bind to it, and establish a separate usage of the broker unrelated to sarracenia.

+

To prevent such misuse, sr_audit is a component that is invoked regularly looking +for mis-use, and cleaning it up.

+
+
+

Input Validation

+

Users such as Alice post their notification messages to their own exchange (xs_Alice). Processes which read from +user exchanges have a responsibility for validation. The process that reads xs_Alice (likely an sr_sarra) +will overwrite any source or cluster heading written into the notification message with the correct values for +the current cluster, and the user which posted the notification message.

+

The checksum algorithm used must also be validated. The algorithm must be known. Similarly, if +there is a malformed header of some kind, it should be rejected immediately. Only well-formed notification messages +should be forwarded for further processing.

+

In the case of sr_sarra, the checksum is re-calculated when downloading the data, it +ensures it matches the notification message. If they do not match, an error report message is published. +If the recompute_checksum option is True, the newly calculated checksum is put into the notification message. +Depending on the level of confidence between a pair of pumps, the level of validation may be +relaxed to improve performance.

+

Another difference with inter-pump connections, is that a pump necessarily acts as an agent for all the +users on the remote pumps and any other pumps the pump is forwarding for. In that case, the validation +constraints are a little different:

+
    +
  • source doesn´t matter. (feeders can represent other users, so do not overwrite.)

  • +
  • ensure cluster is not local cluster (as that indicates either a loop or misuse.)

  • +
+

If the notification message fails the non-local cluster test, it should be rejected, and logged (FIME: published … hmm… clarify)

+
+

Note

+
+
FIXME:
    +
  • if the source is not good, and the cluster is not good… cannot report back. so just log locally?

  • +
+
+
+
+
+
+

Privileged System Access

+

Ordinary (sources, and subscribers) users operate sarra within their own permissions on the system, +like an scp command. The pump administrator account also runs under a normal linux user account and, +given requires privileges only on the AMQP broker, but nothing on the underlying operating system. +It is convenient to grant the pump administrator sudo privileges for the rabbitmqctl command.

+

There may be a single task which must operate with privileges: cleaning up the database, which is an easily +auditable script that must be run on a regular basis. If all acquisition is done via sarra, then all of +the files will belong to the pump administrator, and privileged access is not required for this either.

+
+
+
+

Glossary

+

Sarracenia documentation uses a number of words in a particular way. +This glossary should make it easier to understand the rest of the documentation.

+
+
Source

Someone who wants to ship data to someone else. They do that by advertising a +trees of files that are copied from the starting point to one or more pumps +in the network. The notification message sources produced tell others exactly where +and how to download the files, and Sources have to say where they want the +data to go to.

+

Sources use programs like sr_post.1, +sr_watch.1, and sr_poll(1) to create +their notification messages.

+
+
Subscribers

are those who examine notification messages about files that are available, and +download the files they are interested in.

+

Subscribers use sr_subscribe(1)

+
+
Post, Notice, Notification, Advertisement, Announcement

These are AMQP messages build by sr_post, sr_poll, or sr3_watch to let users +know that a particular file is ready. The format of these AMQP messages is +described by the sr_post(7) manual page. All of these +words are used interchangeably. Advertisements at each step preserve the +original source of the posting, so that report messages can be routed back +to the source.

+
+
Report messages

These are AMQP messages (in sr_post(7) format, with _report_ +field included) built by consumers of notification messages, to indicate what a given pump +or subscriber decided to do with a notification message. They conceptually flow in the +opposite direction of notifications in a network, to get back to the source.

+
+
Pump or broker

A pump is a host running Sarracenia, a rabbitmq AMQP server (called a broker +in AMQP parlance) The pump has administrative users and manage the AMQP broker +as a dedicated resource. Some sort of transport engine, like an apache +server, or an openssh server, is used to support file transfers. SFTP, and +HTTP/HTTPS are the protocols which are fully supported by sarracenia. Pumps +copy files from somewhere, and write them locally. They then re-advertise the +local copy to its neighbouring pumps, and end user subscribers, they can +obtain the data from this pump.

+
+
+
+

Note

+

For end users, a pump and a broker is the same thing for all practical +purposes. When pump administrators configure multi-host clusters, however, a +broker might be running on two hosts, and the same broker could be used by +many transport engines. The entire cluster would be considered a pump. So the +two words are not always the same.

+
+
+
Dataless Pumps

There are some pumps that have no transport engine, they just mediate +transfers for other servers, by making notification messages available to clients and +servers in their network area.

+
+
Dataless Transfers

Sometimes transfers through pumps are done without using local space on the pump.

+
+
Pumping Network

A number of interconnects servers running the sarracenia stack. Each stack +determines how it routes items to the next hop, so the entire size or extent +of the network may not be known to those who put data into it.

+
+
Network Maps

Each pump should provide a network map to advise users of the known destination +that they should advertise to send to. FIXME undefined so far.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/DetectFileReady.html b/Explanation/DetectFileReady.html new file mode 100644 index 000000000..475c5bb2a --- /dev/null +++ b/Explanation/DetectFileReady.html @@ -0,0 +1,565 @@ + + + + + + + File Detection Strategies — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

File Detection Strategies

+

The fundamental job of watch is to notice when files are available to be transferred. +The appropriate strategy varies according to:

+
+
    +
  • the number of files in the tree to be monitored,

  • +
  • the minimum time to notice changes to files that is acceptable, and

  • +
  • the size of each file in the tree.

  • +
+
+

The easiest tree to monitor is the smallest one. With a single directory to +watch where one is posting for an sr_sarra component, then use of the +delete option will keep the number of files in directory at any one point +small and minimize the time to notice new ones. In such optimal conditions, +noticing files in a hundredth of a second is reasonable to expect. Any method +will work well for such trees, but the watch defaults (inotify) are usually +the lowest overhead.

+

When the tree gets large, the decision can change based on a number of factors, +described in the following table. It describes the approaches which will be lowest +latency and highest throughput first, and works its way to the least efficient +options that insert the most delay per detection.

+
+

File Detection Strategy Table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

File Detection Strategies (Order: Fastest to Slowest ) +Faster Methods Work for Larger Trees.

Method

Description

Application

Implicit +posting +using shim +library

+

(LD_PRELOAD)

+

(in C)

+
+
File delivery advertised by libsrshim
    +
  • requires C package.

  • +
  • export LD_PRELOAD=libsrshim.so.1

  • +
  • must tune rejects as everything +might be posted.

  • +
  • works on any size file tree.

  • +
  • very multi-threaded.

  • +
  • I/O by writer (better localized)

  • +
  • very multi-threaded (user processes)

  • +
+
+
+

Many user jobs which cannot be +modified to post explicitly.

+
+
    +
  • multi-million file trees.

  • +
  • most efficient.

  • +
  • more complicated to setup.

  • +
  • use where python3 not available.

  • +
  • no watch needed.

  • +
  • no plugins.

  • +
+
+

Explicit +posting by +clients

+

C: sr_cpost +or +Python: +sr_post

+

File delivery advertised by +sr_post(1) +or other sr_ components +after file writing complete.

+
+
    +
  • poster builds checksums

  • +
  • fewer round trips (no renames)

  • +
  • only a little slower than shim.

  • +
  • no directory scanning.

  • +
  • many sr_posts can run at once.

  • +
+
+

User posts only when file is complete.

+
+
    +
  • user has finest grain control.

  • +
  • usually best.

  • +
  • if available, do not use sr_watch.

  • +
  • requires explicit posting by user +scripts/jobs.

  • +
+
+

sr_cpost

+

(in C)

+

works like watch if sleep > 0

+
+
    +
  • faster than watch

  • +
  • uses less memory than sr_watch.

  • +
  • practical with a bit bigger trees.

  • +
+
+
    +
  • where python3 is hard to get.

  • +
  • where speed is critical.

  • +
  • where plugins not needed.

  • +
  • same issues with tree size +as sr_watch, just a little later. +(see following methods)

  • +
+

sr_watch with +reject +: .*\.tmp$ +(suffix)

+
+

(default)

+
+

(in Python)

+

Files transferred with a .tmp suffix. +When complete, renamed without suffix. +Actual suffix is settable.

+
+
    +
  • requires extra round trips for +rename (a little slower)

  • +
  • Assume 1500 limited to files/second

  • +
  • Large trees mean long startup.

  • +
  • each node in a cluster may need +to run an instance

  • +
  • each watch single threaded.

  • +
+
+

Receiving from most other systems +(.tmp support built-in) +Use to receive from Sundew.

+

best choice for most trees on a +single server or workstation. Full +plugin support.

+

works great with 10000 files +only a few seconds startup.

+

too slow for millions of files.

+

sr_watch with +reject +^\..* +(Prefix)

Use Linux convention to hide files. +Prefix names with ‘.’ +that need that. (compatibility) +same performance as previous method.

Sending to systems that +do not support suffix.

sr_watch with +inflight +number +(mtime)

+

Alternate +setting

+

fileAgeMin

+

Minimum age (modification time) +of the file before it is considered +complete. (aka: fileAgeMin)

+
+
    +
  • Adds delay in every transfer.

  • +
  • Vulnerable to network failures.

  • +
  • Vulnerable to clock skew.

  • +
+
+

Last choice, guarantees delay only if +no other method works.

+

Receiving from uncooperative +sources.

+

(ok choice with PDS)

+

If a process is re-writing a file +often, can use mtime to smooth out +the i/o pattern, by slowing posts.

+

force_polling +using reject +or mtime +methods above

As per above 3, but uses plain old +directory listings.

+
+
    +
  • Large trees means slower to notice +new files

  • +
  • should work anywhere.

  • +
+
+

Only use when INOTIFY has some sort +of issue, such as cluster file +system in a supercomputer.

+

needed on NFS shares with multiple +writing nodes.

+
+

sr_watch is sr3_post with the added sleep option that will cause it to loop +over directories given as arguments. sr3_cpost is a C version that functions +identically, except it is faster and uses much less memory, at the cost of the +loss of plugin support. With a watch (and sr3_cpost), the default method of +noticing changes in directories uses OS specific mechanisms (on Linux: INOTIFY) +to recognize changes without having to scan the entire directory tree manually. +Once primed, file changes are noticed instantaneously, but requires an +initial walk across the tree, a priming pass.

+

For example, assume a server can examine 1500 files/second. If a medium +sized tree is 30,000 files, then it will take 20 seconds for a priming pass. +Using the fastest method available, one must assume that on startup for such a +directory tree it will take 20 seconds or so before it starts reliably posting +all files in the tree. After that initial scan, files are noticed with +sub-second latency. So a sleep of 0.1 (check for file changes every tenth +of a second) is reasonable, as long as we accept the intial priming pass. +If one selects force_polling option, then that 20 second delay is incurred +for each polling pass, plus the time to perform the posting itself. For the +same tree, a *sleep* setting of 30 seconds would be the minimum to recommend. +Expect that files will be noticed about 1.5* the *sleep* settings on average. +In this example, about when they are about 45 seconds. Some will be picked up +sooner, others later. Apart from special cases where the default method misses +files, it is much slower on medium sized trees than the default and should not +be used if timeliness is a concern.

+

In supercomputing clusters, distributed files systems are used, and the OS +optimized methods for recognizing file modifications (INOTIFY on Linux) do not +cross node boundaries. To use watch with the default strategy on a +directory in a compute cluster, one usually must have a watch process +running on every node. If that is undesirable, then one can deploy it on a +single node with force_polling but the timing will be constrained by the +directory size.

+

As the tree being monitored grows in size, sr_watch’s latency on startup grows, +and if polling is used the latency to notice file modifications will grow as +well. For example, with a tree with 1 million files, one should expect, at best, +a startup latency of 11 minutes. If using polling, then a reasonable expectation +of the time it takes to notice new files would be in the 16 minute range.

+

If the performance above is not sufficient, then one needs to consider the use +of the shim library instead of sr_watch. First, install the C version of +Sarracenia, then set the environment for all processes writing files that +need to be posted to call it:

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

where shimpost.conf is an sr_cpost configuration file in +the ~/.config/sarra/post/ directory. An sr_cpost configuration file is the same +as an sr3_post one, except that plugins are not supported. With the shim +library in place, whenever a file is written, the accept/reject clauses of +the shimpost.conf file are consulted, and if accepted, the file is posted just +as it would be by sr_watch.

+

So far, the discussion has been about the time to notice a file has changed. +Another consideration is the time to post files once they have been noticed. +There are tradeoffs based on the checksum algorithm chosen. The most robust +choice is the default: s or SHA-512. When using the s sum method, the +entire file will be read in order to calculate it’s checksum, which is +likely to determine the time to posting. The check sum will used by +downstream consumers to determine whether the file being announced is new, +or one that has already been seen, and is really handy.

+

For smaller files, checksum calculation time is negligible, but it is +generally true that bigger files take longer to post. When using the +shim library method, the same process that wrote the file is the one +calculating the checksum, the likelihood of the file data being in a +locally accessible cache is quite high, so it is as inexpensive as +possible. It should also be noted that the sr_watch/sr_cpost directory +watching processes are single threaded, while when user jobs call sr_post, or +use the shim library, there can be as many processes posting files as there are +file writers.

+

To shorten posting times, one can select sum algorithms that do not read +the entire file, such as N (SHA-512 of the file name only), but then one +loses the ability to differentiate between versions of the file.

+
+
note ::

should think about using N on the watch, and having multi-instance shovels +recalculate checksums so that part becomes easily parallellizable. Should be +straightforward, but not yet explored as a result of use of shim library. FIXME.

+
+
+

A last consideration is that in many cases, other processes are writing files +to directories being monitored by sr_watch. Failing to properly set file +completion protocols is a common source of intermittent and difficult to +diagnose file transfer issues. For reliable file transfers, it is critical +that both the writer and watch agree on how to represent a file that +isn’t complete.

+
+
+

SHIM LIBRARY USAGE

+

Rather than invoking a sr3_post to post each file to publish, one can have processes automatically +post the files they right by having them use a shim library intercepting certain file i/o calls to libc +and the kernel. To activate the shim library, in the shell environment add:

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

where shimpost.conf is an sr_cpost configuration file in +the ~/.config/sarra/post/ directory. An sr_cpost configuration file is the same +as an sr3_post one, except that plugins are not supported. With the shim +library in place, whenever a file is written, the accept/reject clauses of +the shimpost.conf file are consulted, and if accepted, the file is posted just +as it would be by sr3_post. If using with ssh, where one wants files which are +scp’d to be posted, one needs to include the activation in the .bashrc and pass +it the configuration to use:

+
expoert LC_SRSHIM=shimpost.conf
+
+
+

Then in the ~/.bashrc on the server running the remote command:

+
if [ "$LC_SRSHIM" ]; then
+    export SR_POST_CONFIG=$LC_SRSHIM
+    export LD_PRELOAD="libsrshim.so.1"
+fi
+
+
+

SSH will only pass environment variables that start with LC_ (locale) so to get it +passed with minimal effort, we use that prefix.

+
+

Shim Usage Notes

+

This method of notification does require some user environment setup. +The user environment needs to the LD_PRELOAD environment variable set +prior to launch of the process. Complications that remain as we have +been testing for two years since the shim library was first implemented:

+
    +
  • if we want to notice files created by remote scp processes (which create non-login shells) +then the environment hook must be in .bashrc. and using an environment +variable that starts with LC_ to have ssh transmit the configuration value without +having to modify sshd configuration in typical linux distributions. +( full discussion: https://github.com/MetPX/sarrac/issues/66 )

  • +
  • code that has certain weaknesses, such as in FORTRAN a lack of IMPLICIT NONE +https://github.com/MetPX/sarracenia/issues/69 may crash when the shim library +is introduced. The correction needed in those cases has so far been to correct +the application, and not the library. +( also: https://github.com/MetPX/sarrac/issues/12 )

  • +
  • codes using the exec call ot tcl/tk, by default considers any +output to file descriptor 2 (standard error) as an error condition. +these notification messages can be labelled as INFO, or WARNING priority, but it will +cause the tcl caller to indicate a fatal error has occurred. Adding +-ignorestderr to invocations of exec avoids such unwarranted aborts.

  • +
  • Complex shell scripts can experience an inordinate performance impact. +Since high performance shell scripts is an oxymoron, the best solution, +performance-wise is to re-write the scripts in a more efficient scripting +language such as python ( https://github.com/MetPX/sarrac/issues/15 )

  • +
  • Code bases that move large file hierarchies (e.g. mv tree_with_thousands_of_files new_tree ) +will see a much higher cost for this operation, as it is implemented as +a renaming of each file in the tree, rather than a single operation on the root. +This is currently considered necessary because the accept/reject pattern matching +may result in a very different tree on the destination, rather than just the +same tree mirrored. See below for details.

  • +
  • export SR_SHIMDEBUG=1 will get your more output than you want. use with care.

  • +
+
+
+

Rename Processing

+

It should be noted that file renaming is not as simple in the mirroring case as in the underlying +operating system. While the operation is a single atomic one in an operating system, when +using notifications, there are accept/reject cases that create four possible effects.

+ + + + + + + + + + + + + + + + + + +

old name is:

New name is:

Accepted

Rejected

Accepted

rename

copy

Rejected

remove

nothing

+

When a file is moved, two notifications are created:

+
    +
  • One notification has the new name in the relpath, while containing and oldname +field pointing at the old name. This will trigger activities in the top half of +the table, either a rename, using the oldname field, or a copy if it is not present

    +

    at the destination.

    +
  • +
  • A second notification with the oldname in relpath which will be accepted +again, but this time it has the newname field, and process the remove action.

  • +
+

While the renaming of a directory at the root of a large tree is a cheap atomic operation +in Linux/Unix, mirroring that operation requires creating a rename posting for each file +in the tree, and thus is far more expensive.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/DuplicateSuppression.html b/Explanation/DuplicateSuppression.html new file mode 100644 index 000000000..47c3d45b3 --- /dev/null +++ b/Explanation/DuplicateSuppression.html @@ -0,0 +1,331 @@ + + + + + + + Duplicate Suppression — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Duplicate Suppression

+

When forwarding products in networks, one needs to avoid storms or loops of data transmission, +where the same data circulates infinitely in the network, (for example: A sends a file to B, B sends it to C, +C sends it back to A. If A sends it to B again, we have an infinite loop or storm if the volume is large enough. )

+

Another main feature of mission critical processing is having multiple systems produce the same data, or +hot backup systems being available. If the strategy is to have both production and backup systems both +running at once, one will always have multiple copies of the same data being in the network, with the +backup being blocked at some point. The potential for loops, or sending out redundant copies of data, +consuming excessive bandwidth and processing power, is high.

+

It turns out that duplicate suppression is deceptively complex. There is not one method of +duplicate suppression that is appropriate for all cases, so sr3 allows for customization. +The sarracenia.flowcb.nodupe module implements duplicate suppression by default but can be used +for more flexible filtering.

+
+
Duplicate suppression must:
    +
  • derive a notification message key.

  • +
  • if a notification message has been received with the same key and and path, then it is a duplicate.

  • +
+
+
+

Duplicates are dropped to avoid further processing.

+

A notification message key is preferably derived from the Identity field of the notification +message. If the producer does not provide an identity checksum, algorithms may fall +back on other metadata: mtime, size, pubTime. Since pubTime is a mandatory +field, a key can always be derived, but it’s effectiveness in a particular use case +is not assured. (the sarracenia.flowcb.nodupe.NoDupe.deriveKey(self,msg) helper routine +provides a standard key, given a message.)

+

in all methods, one can turn on duplicate suppression with the following option +in the config file:

+
nodupe_ttl 300|off|on
+
+
+

When supplied a number, that indicates the lifetime, in seconds of entries in the +duplicate suppresion memory (e.g. 300 seconds == 5 minutes.)

+
+

Standard (path and data oriented)

+

method: when products have the same key and path, they are duplicates.

+

Two routes can receive the same product, with the same relative path. In normal processing, +the products should be identical, and Identity checksums for it should be the same,

+

FIXME: the normal case when multiple intervening pumps.

+
+
+

Data Oriented

+

method: when products the same key, they are duplicates.

+

in the config file either:

+
nodupe_basis data
+
+
+

or:

+
flowcb_prepend sarracenia.flowcb.nodupe.data.Data
+
+
+

overrides the standard duplicate suppression key generation to include only the data +checksum. This module adjusts the path field that the standard duplicateion suppression field uses. +The flowcb_prepend directive ensures that it is called before the built-in duplicate suppression.

+

products that are the same should have identical checksums, regardless of path. Used when +multiple sources produce the same product. (Note: all zero-length files are identical, +it could be that products with unrelated purpose have identical content. scoping is +usually important when this option is applied.)

+
+
+

Name Oriented

+

method: when products have the same file name, they are duplicates.

+

In the config file, either:

+
nodupe_basis data
+
+
+

or:

+
flowcb_prepend sarracenia.flowcb.nodupe.name.Name
+
+
+

Override the standard duplicate suppression key generation to use only the file name.

+

When multiple sources produce a product, but the result is not binary identical, and no +appropriate Identity method is available, then then one needs a different approach. +Since the two sources are not, generally, synchronized,

+
+

URP

+

Radar production is done, in one case, on many different (6? ) operational servers producing +“identical” products. They are identical in the sense that they are based on the same +input, and have the same semantic meaning, but details of processing mean that none of +the products are binary identical.

+

The different servers are used as different “sources” so the relative path of products +from the different sources will not be the same, but every product that has the same +file name is interchangeable (“identical” for our purposes.) So using the file name +is correct in this case.

+
+
+
+

Files That Change Too Often (mdelaylatest)

+

method: wait until file is x seconds old before forwarding.

+

NOTE: This is an additional filter to duplicate suppression, and the above +methods can be used in conjunction with mdelaylatest. this filter is ideally +applied before duplicate suppression to reduce the database size.

+

In the configuration file:

+
mdelay 30
+flowcb_prepend sarracenia.flowcb.mdelaylatest.MDelayLatest
+
+
+

Delay all files by 30 seconds, if multiple versions are produced before the file +is 30 seconds old, then only send the last one, when it is 30 seconds old.

+

use case: +In some cases, there are data sources that overwrite files very often. +If the files are large (take a long time to copy) or there is queuing (the subscriber +is some time behind the producer.), an algorithm could overwrite a file, or +append to it, three or four times before having a “final” version that will last some time.

+

if network has a propagation delay that is longer than the overwrite period, then by +the time the consumer requests the file, it will be different (potentially causing checksum mismatches.) +or, if it is fast enough, copying a file that will not last more than a few seconds +could be a waste of bandwidth and processing.

+
+

Weatheroffice citypages

+

( https://hpfx.collab.science.gc.ca/YYYYMMDD/WXO-DD/citypage_weather/ )

+

The citypages are a compound product (derived from many separate upstream products.) +The script that creates the citypage products seems to write a header, then some records, +then at the very end, a footer. there were many cases of files being transmitted +as invalid xml because the footer is missing, leaving some XML entities incomplete. +One must wait until the script has finished writing the file before creating a notification message.

+
+
+

HPC mirrorring

+

In the high speed mirroring of data between high performance computing clusters, +shell scripts often spend time appending records to files, perhaps hundreds of times per second. +Once the script is complete, the file becomes read-only for consumers. It is not useful +to transmit these intermediate values. A 100 byte file monitored using the shim library +or an sr_watch, could be modified hundreds of times, causing a copy for every modification potentially +triggering hundreds of copies. It is better to wait for the end of the update process, +for the file to be quiescent, before posting a notification message.

+
+
+
+

Files That are Too Old

+

method: files that are too old are dropped.

+

in the configuration file:

+
fileAgeMax 600
+
+
+

Files which are older than 600 seconds (10 minutes) will not be considerred for transfer.

+

This is usually used with polls that have very long lasting directories on a remote +server. example: a remote server has a permanent database of remote files.

+

It is often the case that nodupe_ttl should be greater than nodupe_fileAgeMax to prevent +files from aging out of the cache before they are considered “too old” and then being +(erroneously) re-ingested. A warning message is emitted if this is the case in a poll +on startup.

+
+
+

Roll Your Own

+

In the configuration file:

+
your_settings
+flowcb_prepend your_class.YourClass
+
+
+

If none of the built-in methods of duplicate suppression work for your use case, you can +subclass sarracenia.flowcb.nodupe and derive keys in a different way. See the +sarracenia.flowcb.nodupe.name and sarracenia.flowcb.nodupe.data classes for examples of +how to do so.

+

One can also implement a filter that sets the nodupe_override field in the message:

+
msg['nodupe_override] = { 'key': your_key, 'path': your_path }
+
+
+

and the standard duplicate suppression method will use the provided key and value. +There is also a helper function available in the nodupe class:

+
def deriveKey(self, msg) --> str
+
+
+

which will look at the fields in the message and derive the normal key that would be +generated for a message, which you can then modify if only looking for a small change.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/FileCompletion.html b/Explanation/FileCompletion.html new file mode 100644 index 000000000..40bf774f0 --- /dev/null +++ b/Explanation/FileCompletion.html @@ -0,0 +1,297 @@ + + + + + + + Delivery Completion (inflight) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Delivery Completion (inflight)

+

Failing to properly set file completion protocols is a common source of intermittent and +difficult-to-diagnose file transfer issues. For reliable file transfers, it is +critical that both the sender and receiver agree on how to represent a file that isn’t complete. +The inflight option (meaning a file is in flight between the sender and the receiver) supports +many protocols appropriate for different situations:

+
+

Inflight Table

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Delivery Completion Protocols (in Order of Preference)

Method

Description

Application

NONE

File sent with right name. +Send sr3_post(7) +by AMQP after file is complete.

+
+
    +
  • fewer round trips (no renames)

  • +
  • least overhead / highest speed

  • +
+
+

Sending to Sarracenia, and +post only when file is complete

+
+
(Best when available)
    +
  • Default on sr_sarra.

  • +
  • Default on sr_subscribe and sender +when post_broker is set.

  • +
+
+
+

.tmp +(Suffix)

Files transferred with a .tmp suffix. +When complete, renamed without suffix. +Actual suffix is settable.

+
+
    +
  • requires extra round trips for +rename (a little slower)

  • +
+
+

sending to most other systems +(.tmp support built-in) +Use to send to Sundew

+
+
(usually a good choice)
    +
  • default when no post broker set

  • +
+
+
+

tmp/ +(subdir) +/dir

Files transferred to a subdir or dir +When complete, renamed to parent dir. +Actual subdir is settable.

+

same performance as Suffix method.

+

sending to some other systems

. +(Prefix)

Use Linux convention to hide files. +Prefix names with ‘.’ +that need that. (compatibility) +same performance as Suffix method.

Sending to systems that +do not support suffix.

number +(mtime)

+

fileAgeMin

+

Minimum age (modification time) +of the file before it is considered +complete.

+

Adds delay in every transfer. +Vulnerable to network failures. +Vulnerable to clock skew.

+

Last choice +guaranteed delay added

+

Receiving from uncooperative +sources.

+

(ok choice with PDS)

+
+

By default ( when no inflight option is given ), if the post_broker is set, then a value of NONE +is used because it is assumed that it is delivering to another broker. If no post_broker +is set, the value of ‘.tmp’ is assumed as the best option.

+

NOTES:

+
+

On versions of sr_sender prior to 2.18, the default was NONE, but was documented as ‘.tmp’ +To ensure compatibility with later versions, it is likely better to explicitly write +the inflight setting. The numeric variant is the same as setting fileAgeMin

+

inflight was renamed from the old lock option in January 2017. For compatibility with +older versions, can use lock, but name is deprecated.

+

The old PDS software (which predates MetPX Sundew) only supports FTP. The completion protocol +used by PDS was to send the file with permission 000 initially, and then chmod it to a +readable file. This cannot be implemented with SFTP protocol, and is not supported at all +by Sarracenia.

+
+
+
+

Frequent Configuration Errors

+

Setting NONE when sending to Sundew.

+
+

The proper setting here is ‘.tmp’. Without it, almost all files will get through correctly, +but incomplete files will occasionally picked up by Sundew.

+
+

Using mtime method to receive from Sundew or Sarracenia:

+
+

Using mtime is last resort. This approach injects delay and should only be used when one +has no influence to have the other end of the transfer use a better method.

+

mtime is vulnerable to systems whose clocks differ (causing incomplete files to be picked up.)

+

mtime is vulnerable to slow transfers, where incomplete files can be picked up because of a +networking issue interrupting or delaying transfers.

+

Sources may not to include mtime data in their posts ( timeCopy option on post.)

+
+

Setting NONE when delivering to non-Sarracenia destination.

+
+

NONE is to be used when there is some other means to figure out if a file is delivered. +For example, when sending to another pump, the sender will post the notification message to +the destination after the file is complete, so there is no danger of it being +picked up early.

+

When used inappropriately, there will occasionally be incomplete files delivered.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Glossary.html b/Explanation/Glossary.html new file mode 100644 index 000000000..7c1aaa370 --- /dev/null +++ b/Explanation/Glossary.html @@ -0,0 +1,324 @@ + + + + + + + Glossary — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Glossary

+

Sarracenia documentation uses a number of words in a particular way. +This glossary should make it easier to understand the rest of the documentation.

+
+

AMQP

+

AMQP is the Advanced Message Queuing Protocol, which emerged from the financial trading industry and has gradually +matured. Implementations first appeared in 2007, and there are now several open source ones. AMQP implementations +are not JMS plumbing. JMS standardizes the API programmers use, but not the on-the-wire protocol. So +typically, one cannot exchange notification messages between people using different JMS providers. AMQP standardizes +for interoperability, and functions effectively as an interoperability shim for JMS, without being +limited to Java. AMQP is language neutral, and notification message neutral. There are many deployments using +Python, C++, and Ruby. One could adapt WMO-GTS protocols very easily to function over AMQP. JMS +providers are very Java oriented.

+
    +
  • www.amqp.org - Defining AMQP.

  • +
  • www.openamq.org - Original GPL implementation from JPMorganChase

  • +
  • www.rabbitmq.com - Another free implementation. The one we use and are happy with.

  • +
  • Apache Qpid - Yet another free implementation.

  • +
  • Apache ActiveMQ - This is really a JMS provider with a bridge for AMQP. They prefer their own openwire protocol.

  • +
+

Sarracenia relies heavily on the use of brokers and topic based exchanges, which were prominent in AMQP standards efforts prior +to version 1.0, at which point they were removed. It is hoped that these concepts will be re-introduced at some point. Until +that time, the application will rely on pre-1.0 standard message brokers, such as rabbitmq.

+
+
+

Back Pressure

+

When a data pumping node is experiencing high latency, it is best not bring in more data +at high rate and worsen the overload. Instead, one should refrain from accepting messages +from the node so that upstream ones maintain queues, and other, less busy nodes can take +more of the load. Having slow processing affect the ingest of new messages is an example +of applying back pressure to a transfer stream.

+

Example of no back-pressure: paho-python-mqtt v3 library currently has acknowledgements +built-in to the library, so acknowledgement occur without user control, and there is +no maximum number of messages that can be in the library, before the application sees +them. If the application stops, all those messages, as yet unseen by the application, +are lost. In MQTT v5, there is a receiveMaximum setting which at least limits the number +of messages the library will queue up for the application, but ideally, the python +library would get application controlled acknowledgements, as the java library already has.

+
+
+

Dataless Pumps

+

There are some pumps that have no transport engine, they just mediate +transfers for other servers, by making notification messages available to clients and +servers in their network area.

+
+
+

Dataless Transfers

+

Sometimes transfers through pumps are done without using local space on the pump.

+
+
+

Latency

+

Time from the insertion of data into a network (the time the notification message about a file is first published) +to the time it is made available on an end point. We want to minimize latency in transfers, +and high latency can indicate configuration or capacity issues.

+
+
+

MQTT

+

The Message Queue Telemetry Transport (MQTT) version 5 is a second Message Queueing protocol with all the features +necessary to support sarracenia’s data exchange patterns.

+ +
+
+

Network Maps

+

Each pump should provide a network map to advise users of the known destination +that they should advertise to send to. FIXME undefined so far.

+
+
+

Post, Notice, Notification, Advertisement, Announcement

+

These are AMQP messages build by sr3_post, sr3 poll, or sr3 watch to let users +know that a particular file is ready. The format of these AMQP messages is +described by the sr_post(7) manual page. All of these +words are used interchangeably. Advertisements at each step preserve the +original source of the posting, so that report messages can be routed back +to the source.

+
+
+

Pump

+

A pump is a host running Sarracenia, either a rabbitmq AMQP server or an MQTTT +one such as mosquitto. The notification message queueing middleware is called a broker. +The pump has administrative users and manage the MQP broker +as a dedicated resource. Some sort of transport engine, like an apache +server, or an openssh server, is used to support file transfers. SFTP, and +HTTP/HTTPS are the protocols which are fully supported by sarracenia. Pumps +copy files from somewhere, and write them locally. They then re-advertise the +local copy to its neighbouring pumps, and end user subscribers, they can +obtain the data from this pump.

+
+

Note

+

For end users, a pump and a broker is the same thing for all practical +purposes. When pump administrators configure multi-host clusters, however, a +broker might be running on two hosts, and the same broker could be used by +many transport engines. The entire cluster would be considered a pump. So the +two words are not always the same.

+
+
+
+

Pumping Network

+

A number of interconnects servers running the sarracenia stack. Each stack +determines how it routes items to the next hop, so the entire size or extent +of the network may not be known to those who put data into it.

+
+
+

Report messages

+

These are AMQP messages (in sr_post(7) format, with _report_ +field included) built by consumers of messages, to indicate what a given pump +or subscriber decided to do with a message. They conceptually flow in the +opposite direction of notifications in a network, to get back to the source. +in materials from the original 2015 design phase, reports were called log messages. +This was changed to reduce confusing them with data in application log files.

+
+
+

Source

+

Someone who wants to ship data to someone else. They do that by advertising a +trees of files that are copied from the starting point to one or more pumps +in the network. The notification message sources produced tell others exactly where +and how to download the files, and Sources have to say where they want the +data to go to.

+

Sources use the post, +sr_watch.1, and +sr_poll(1) components to create +their notification messages.

+
+
+

Subscribers

+

are those who examine notification messages about files that are available, and +download the files they are interested in.

+

Subscribers use subscribe(1)

+
+
+

Sundew

+

MetPX Sundew is the ancestor of Sarracenia. +It is a pure TCP/IP WMO 386 oriented data pump. The configuration files look similar, +but the routing algorithms and concepts are quite different. MetPX is a push-only +file distribution method, that implemented WMO 386 sockets, AM Sockets, and +other obsolete technologies. It does not do pub/sub. +More History

+
+
+

WMO

+

The World Meteorological Organization, is a part of the United Nations that has the weather and environmental +monitoring, prediction, and alerting services of each country as members. For many decades, there has +been a real-time exchange of weather data between countries, often even in times of war. The standards +that cover these exchanges are:

+
    +
  • Manual on the Global Telecommunications´ System: WMO Manual 386. The standard reference for this +domain. (a likely stale copy is WMO 386.) Try https://www.wmo.int for the latest version.

  • +
+

Usually these links are referred to collectively as the GTS. The standards are very old, and a modernization +process has been ongoing for the last decade or two. Some current work on replacing the GTS is here:

+ +

The discussions around this topic are important drivers for Sarracenia.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/Evolution.html b/Explanation/History/Evolution.html new file mode 100644 index 000000000..9c3ec71d0 --- /dev/null +++ b/Explanation/History/Evolution.html @@ -0,0 +1,301 @@ + + + + + + + History/Context of Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

History/Context of Sarracenia

+

MetPX-Sarracenia is a product of the Meteorological Product Exchange Project, +originated in Environment Canada, but now run by Shared Services Canada on their +behalf. The project started in 2004, with the goal of providing a free stack that +implements World Meteorological Organization standard real-time data exchange, +and also adjacent needs. Sundew was the +first generation WMO 386 (GTS) switch.

+

In 2007, when MetPX was originally open sourced, the staff responsible were part of +Environment Canada. In honour of the Species At Risk Act (SARA), to highlight the plight +of disappearing species which are not furry (the furry ones get all the attention) and +because search engines will find references to names which are more unusual more easily, +the original MetPX WMO switch was named after a carnivorous plant on the Species At +Risk Registry: The Thread-leaved Sundew.

+

Sundew, the WMO switch, also needed compatibility with existing internal transfer +mechanisms based heavily on FTP. It worked, but the GTS itself is obsolete in many +deep ways, and work started in 2009 extending Sundew to leverage new technologies, +such as message queueing protocols, starting in 2008. Versions of Sundew are +generally labelled < 1.0

+

The initial prototypes of Sarracenia leveraged MetPX Sundew, Sarracenia’s ancestor. +Sundew plugins were developed to create notification messages for files delivered by Sundew, +and Dd_subscribe was initially developed as a download client for dd.weather.gc.ca, an +Environment Canada website where a wide variety of meteorological products are made +available to the public. It is from the name of this site that the Sarracenia +suite takes the dd_ prefix for its tools. The initial version was deployed in +2013 on an experimental basis as dd_subscribe.

+
+

dd_subscribe Renaming

+

The new project (MetPX-Sarracenia) has many components, is used for more than +distribution, and more than one website, and causes confusion for sysadmins thinking +it is associated with the dd(1) command (to convert and copy files). So, components +were switched to use the sr_ prefix. The following year, support of +checksums was added, and in the fall of 2015, the feeds were updated to v02.

+

We eventually ran into the limits of this extension approach, and in 2015 we +started Sarracenia +as a ground-up second generation replacement, unburdened by strict legacy GTS compatibility. +Sarracenia (version 2) was initially a prototype, and many changes of many kinds occurred during it’s lifetime. +It is still (in 2022) the only version operationally deployed. It went through three changes in operational +message format (exp, v00, and v02.) It supports hundreds of thousands file transfers per hour 24/7 +in Canada.

+

Where Sundew supports a wide variety of file formats, protocols, and conventions +specific to the real-time meteorology, Sarracenia takes a step further away from +specific applications and is a ruthlessly generic tree replication engine, which +should allow it to be used in other domains. The initial prototype client, dd_subscribe, +in use since 2013, was sort of a logical dead end. A path forward was described +with the Sarracenia in 10 Minutes +in November 2015, which led to dd_subscribe’s replacement in 2016 by the full blown +Sarracenia package, with all components necessary for production as well as +consumption of file trees.

+

The organization behind MetPX has moved to Shared Services Canada in 2011, but when +it came time to name a new module, we kept with a theme of carnivorous plants, and +chose another one indigenous to some parts of Canada: Sarracenia, a variety +of insectivorous pitcher plants. We like plants that eat meat!

+

Sarracenia was initially called v2, as in the second data pumping architecture +in the MetPX project, (v1 being Sundew.) a good timeline of deployments/acheivements +is here. While it proved very promising, +worked quite well, Over the years a number of limitations with the existing +implementation became clear:

+
    +
  • The poor support for python developers. v2 code is not at all idiomatic Python.

  • +
  • the odd plugin logic, with poor error reporting.

  • +
  • The inability to process groups of messages.

  • +
  • The inability to add other queueing protocols (limited to rabbitmq/AMQP.)

  • +
  • Difficulty of adding transfer protocols.

  • +
+

In 2020, Development began on Version 3, now +dubbed Sr3. Sr3 is about 30% less code that v2, offers a much improved API, +and supports additional message protocols, rather than just rabbitmq.

+
+
+

Fewer Klocs, Better klocs

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

History of Data Pumping Applications for Environment Canada

Era

Application

Code size

Features

1980s

Tandem, PDS (domestic GTS)

500kloc

X.25, WMO Socket, AM Socket, FTP (push only)

2000s

Sundew

30kloc

WMO Socket/TCP, FTP, SFTP (push only)

2010s

Sarracenia v2

25kloc

AMQP, HTTP, SFTP, FTP (pub/sub and push)

2020s

Sarracenia v3 (sr3)

15kloc

AMQP, MQTT, HTTP, SFTP, API (pub/sub and push)

+
+
+

Deployments/Use Cases

+

Deployment status in 2015: Sarracenia in 10 Minutes Video (5:26 in)

+

Deployment status in 2018: Deployments as of January 2018

+
+
+

Project Website

+

Prior to March 2018, the primary web-site for the project was metpx.sf.net. +That MetPX website was built from the documentation in the various modules +in the project. It builds using all .rst files found in +sarracenia/doc as well as some of the .rst files found in +sundew/doc. In the Spring of 2018, development moved to github.com. +github.com renders .rst when showing pages, so separate processing to render +web pages is no longer needed.

+

Between 2018 and 2022, updating was done by committing changes to .rst files +directly on github, so the home page is: https://github.com/MetPX/sarracenia) +There was no post-processing required. As the links are all relative and +other services such as gitlab also support such rendering, the +website is portable any gitlab instance, etc… And the entry point is from +the README.rst file at the root directory of each repository.

+

That had some readability problems, with people saying “Doesn’t it have a web site?” +and poor navigation, no indexing. In 2022/03, sphinx was adopted as part +of #380 (Document Restructure) and the local build process is now essentially +run sphinx. The new website ( https://metpx.github.io/sarracenia ) is updated +by a github workflow on each commit.

+
+

Updating The sf.net Website

+

Only the index-e.html and index-f.html pages are used on the sf.net website +today. Unless you want to change those pages, this operation is useless. +For all other pages, the links go directly into the various .rst files on +github.com.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/HPC_Mirroring_Use_Case.html b/Explanation/History/HPC_Mirroring_Use_Case.html new file mode 100644 index 000000000..0b58c3794 --- /dev/null +++ b/Explanation/History/HPC_Mirroring_Use_Case.html @@ -0,0 +1,659 @@ + + + + + + + Case Study: HPC Mirroring — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Case Study: HPC Mirroring

+
+

Continuously Mirror 27 Million File Tree Very Quickly

+
+

Summary

+

This project has taken longer than expected, over three years, as the problem space was explored with the +help of a very patient client while the tool to design and implement the efficient solution was eventually +settled on. The client asked for a solution to make files available on the backup cluster within five +minutes of their creation on the primary one, and the first version of mirroring, deployed in 2017, +achieves roughly a 20 minute delay. Version 2 was deployed in January 2020.

+

The client is actually more of a partner, who had very large test cases available and +ended up shouldering the responsibility for all of us to understand whether the solution was working or not. +While there are many specificities of this implementation, the resulting tool relies on no specific features +beyond a normal Linux file system to achieve a 72:1 speedup compared to rsync on real-time continuous +mirroring of 16 terabytes in 1.9 million files per day between two trees of 27 million files each. While +this averages to 185 Mbytes/second sustained over a 24 hour period. It should be noted that the transfers +are very peaky. On the same equipment at the same time, another +4 terabytes per day is being written to clusters on another network, so the aggregate read rate on +the operational cluster is 20 Terabytes per day (231 mbytes/second) for this application, while +the file systems are used for all the normal user applications as well.

+

While this project had to suffer through the development, with the lessons learned and the tools +now available, it should be straightforward to apply this solution to other cases. The end result is +that one adds a Shim Library to the users’ environment (transparent to user jobs), and +then every time a file is written, an AMQP message with file metadata is posted. A pool of transfer +daemons are standing by to transfer the files posted to a shared queue. The number of subscribers +is programmable and scalable, and the techniques and topology to do the transfer are all easily +controlled to optimize transfers for whatever criteria are deemed most important.

+
+
+

Problem Statement

+

In November 2016, Environment and Climate Change Canada’s (ECCC) Meteorological Service of Canada (MSC), +as part of the High Performance Computing Replacement (HPCR) project asked for very large directory +trees to be mirrored in real-time. Shared Services Canada (SSC) had primary responsibility for deployment +of HPCR, with ECCC/MSC being the sole user community. It was known from the outset that these trees would be too large to +deal with using ordinary tools. It was expected that it would take about 15 months to explore the +issue and arrive at an effective operational deployment. It should be noted that SSC worked throughout +this period in close partnership with ECCC, and that this deployment required the very active participation of +sophisticated users to follow along with the twists and turns and different avenues explored and implemented.

+

The computing environment is Canada’s national weather centre, whose primary application is production numerical +weather prediction, where models run 7/24 hours/day running different simulations (models of the atmosphere, +and sometimes waterways and ocean, and the land under the atmosphere) either ingesting current observations +aka assimilation, mapping them to a grid analysis, and then walking the grid forward in +time prediction/prognostic. The forecasts follow a precise schedule throughout the 24hour cycle, feeding +into one another, so delays ripple, and considerable effort is expended to avoid interruptions and +maintain schedule.

+
+
+

HPCR Solution Overview

+../../_images/HPC-XC_High_Availability.png + +

In the diagram above, if operations are in Data Hall 1 (left of centre) and they fail, then the goal is to resume +operations promptly from Data Hall 2 (on the right). For this to be realistic, production data must be available +in the other hall, on the other site store, quickly. The mirroring problem is synchronizing a very large +subset of data between site store 1 and site store 2. For monitoring purposes, at the same time, a smaller +subset to must be mirrored to data hall 0.

+
+
+

Continuous Mirroring

+

There is a pair of clusters running these simulations, one normally mostly working on operations, +and the other as a spare (running only research and development loads). When the primary fails, +the intent is to run operations on the other supercomputer, using a spare disk to which all the +live data has been mirrored. As there are (nearly) always runs in progress, the directories never +stop being modified, and there is no maintenance period when one can catch up if one falls behind.

+

There are essentially three parts of the problem:

+
+
    +
  • Detection: obtain the list of files which have been modified (recently).

  • +
  • Transfer: copy them to the other cluster (minimizing overhead.)

  • +
  • Performance: aspirational deadline to deliver a mirrored file: five minutes.

  • +
+
+

The actual trees to mirror were the following in the original contract phase (retrospectively called U0):

+
psilva@eccc1-ppp1:/home/sarr111/.config/sarra/poll$ grep directory *hall1*.conf
+policy_hall1_admin.conf:directory /fs/site1/ops/eccc/cmod/prod/admin
+policy_hall1_archive_dbase.conf:directory /fs/site1/ops/eccc/cmod/prod/archive.dbase
+policy_hall1_cmop.conf:directory /fs/site1/ops/eccc/cmod/cmop/data/maestro/smco500
+policy_hall1_daily_scores.conf:directory /fs/site1/ops/eccc/cmod/prod/daily_scores
+policy_hall1_hubs.conf:directory /fs/site1/ops/eccc/cmod/prod/hubs
+policy_hall1_products.conf:directory /fs/site1/ops/eccc/cmod/prod/products
+policy_hall1_stats.conf:directory /fs/site1/ops/eccc/cmod/prod/stats
+policy_hall1_version_control.conf:directory /fs/site1/ops/eccc/cmod/prod/version_control
+policy_hall1_work_ops.conf:directory /fs/site1/ops/eccc/cmod/prod/work_ops
+policy_hall1_work_par.conf:directory /fs/site1/ops/eccc/cmod/prod/work_par
+psilva@eccc1-ppp1:/home/sarr111/.config/sarra/poll$
+
+
+

Initially, it was known that the number of files was large, but there was no knowledge of the actual +amounts involved. Nor was that data even available until much later.

+

The most efficient way to copy these trees, as was stated at the outset, would be for all of the jobs +writing files in the trees to explicitly announce the files to be copied. This would involve users +modifying their jobs to include invocation of sr_cpost (a command which queues up file transfers for +third parties to perform). However, the client set the additional constraint that modification of user jobs was +not feasible, so the method used to obtain the list of files to copy had to be implicit (done by the +system without active user involvement).

+
+
+

Reading the Tree Takes Too Long

+

One could just scan at a higher level in order to scan a single parent directory, but the half-dozen +sub-trees trees were picked in order to have smaller ones which worked more quickly, regardless of the +method being used to obtain lists of new files. What do we mean when we say these trees are too large? +The largest of these trees is hubs ( /fs/site1/ops/eccc/cmod/prod/hubs ). Rsync was run on the hubs +directory, as just walking the tree once, without any file copying going on. The walk of the tree, using +rsync with checksumming disabled as an optimization, resulted in the log below:

+
psilva@eccc1-ppp1:~/test$ more tt_walk_hubs.log
+nohup: ignoring input
+rsync starting @ Sat Oct  7 14:56:52 GMT 2017
+number of files examined is on the order of: rsync --dry-run --links -avi --size-only /fs/site1/ops/eccc/cmod/prod/hubs /fs/site2/ops/eccc/cmod/prod/hubs |& wc -l
+27182247
+rsync end @ Sat Oct  7 20:06:31 GMT 2017
+psilva@eccc1-ppp1:~/test$
+
+
+

A single pass took over five hours, to examine 27 million files or examining about 1500 files per second. +The maximum rate of running rsyncs on this tree is thus on the order of once every six hours (to allow some +time for copying) for this tree. Note that any usual method of copying a directory tree requires traversing +it, and that there is no reason to believe that any other tool such as find, dump, tar, tree, etc… would +be significantly quicker than rsync. We need a faster method of knowing which files have been modified +so that they can be copied.

+
+
+

Detection Methods: Inotify, Policy, SHIM

+

There is a Linux kernel feature known as INOTIFY, which can trigger an event when a file is modified. By +setting an INOTIFY trigger on every directory in the tree, we can be notified when any file is modified +in the tree. This was the initial approach taken. It turns out (in January 2017), that INOTIFY is indeed a +Linux feature, in that the INOTIFY events only propagate across a single server. With a cluster file +system like GPFS, one needs to run an INOTIFY monitor on every kernel where files are written. So rather +than running a single daemon, we were faced with running several hundred daemons (one per physical node), +each monitoring the same set of tens of millions of files. Since the deamons were running on many nodes, +the memory use rose into the terabyte range.

+

An alternate approach: instead of running the modification detection at the Linux level, use the file +system itself, which is database driven, to indicate which files had been modified. The HPC solution’s main +storage system uses IBM’s General Parallel File System, or GPFS. Using the GPFS-policy method, a query is +run against the file system database at as high a rhythm as can be sustained (around five to ten minutes per +query) combined with sr_poll to announce the files modified (and thus eligible for copying). This is +completely non-portable, but was expected to be much faster than file tree traversal.

+

Over the winter 2016/2017, both of these methods were implemented. The INOTIFY-based sr3_watch was the +fastest method (instantaneous), but the daemons were having stability and memory consumption problems, +and they also took too long to startup (requires an initial tree traversal, which takes the same time +as the rsync). While slower (taking longer to notice a file was modified), the GPFS policy had acceptable +performance and was far more reliable than the parallel sr3_watch method, and by the spring, with deployment +expected for early July 2017, the GPFS policy approach was selected.

+

As the migration progressed, the file systems grew in that they had more files in the trees, and the GPFS-policy +method progressively slowed. Already in July 2017, this was not an acceptable solution. At this point, +the idea of intercepting jobs’ file i/o calls with a shim library was introduced. ECCC told SSC +at the time, that having correct feeds, and having everything ready for transition was the +priority, so the focus of efforts was in that direction until the migration was achieved in +September. In spite of being a lower priority over the summer, a C implementation of the +sending portion of the sarra library was implemented along with a prototype shim library to call it.

+

It should be noted that the GPFS-policy runs have been operationally deployed since 2017. This has +turned out to be version 1 of the mirroring solution, and has achieved a mirroring to secondary +clusters with approximately 20 minutes of delay in getting the data to the second system. Three years +in, there is now an upgrade of the supercomputer clusters (called U1) in progress with two new additional +clusters online, The client is now using normal Sarracenia methods to mirror from the old backup cluster +to the new ones, with only a few seconds delay beyond what it takes to get to the backup cluster.

+

It should also be noted that use of GPFS policy queries have imposed a significant and continuous +load on the GPFS clusters, and are a constant worry to the GPFS administrators. They would very much +like to get rid of it. Performance has stabilized in the past year, but it does appear to slow +as the size of the file tree grows. Many optimisations were implemented to obtain adequate +performance.

+
+

Shim Library

+

The method settled on for notification is a shim library +When a running application makes calls to API entry points that are provided by +libraries or the kernel, there is a search process (resolved at application +load time) that finds the first entry in the path that has the proper signature. +For example, in issuing a file close(2) call, the operating system will arrange +for the correct routine in the correct library to be called.

+../../_images/shim_explanation_normal_close.svg

A call to the close routine, indicates that a program has finished writing the +file in question, and so usually indicates the earliest time it is useful to +advertise a file for transfer. We created a shim library, which has entry +points that impersonate the ones being called by the application, in order +to have file availability notifications posted by the application itself, +without any application modification.

+../../_images/shim_explanation_shim_close.svg

Usage of the shim library is detailed in sr_post(1)

+
+
+
+

Copying Files

+

It needs to be noted that while all of this work was progressing on the ‘obtain the list of +files to be copied’ part of the problem, we were also working on the ‘copy the files to the +other side’ part of the problem. Over the summer, results of performance tests and other +considerations militated frequent changes in tactics. The site stores are clusters in +their own right. They have protocol nodes for serving traffic outside of the GPFS cluster. There are +siteio nodes with infiniband connections and actual disks. The protocol nodes (called nfs or proto) +are participants in the GPFS cluster dedicated to i/o operations, used to offload i/o from the +main compute clusters (PPP and Supercomputer), which have comparable connections to the site store +as the protocol nodes.

+

There are multiple networks (40GigE, Infiniband, as well as management networks) and the one +to use needs to be chosen as well. Then there are the methods of communication (ssh over tcp/ip? +bbcp over tcp/ip? GPFS over tcpip? ipoib? native-ib?).

+../../_images/site-store.jpg +

Many different sources and destinations (ppp, nfs, and protocol nodes), as well many different +methods (rcp, scp, bbcp, sscp, cp, dd) and were all trialled to different degrees at different +times. At this point several strengths of sarracenia were evident:

+
    +
  • The separation of publishing from subscribing means that one can subscribe on the source node +and push to the destination, or on the destination and pull from the source. It is easy to +adapt for either approach (ended up on destination protocol nodes, pulling from the source).

  • +
  • The separation of copying from the computational jobs means that the models run times are +unaffected, as the i/o jobs are completely separate.

  • +
  • The ability to scale the number of workers to the performance needed (eventually settled +on 40 workers performing copies in parallel).

  • +
  • The availability of plugins download_cp, download_rcp, download_dd, allow many different +copy programs (and hence protocols) to be easily applied to the transfer problem.

  • +
+

Many different criteria were considered (such as: load on nfs/protocol nodes, other nodes, +transfer speed, load on PPP nodes). The final configuration selected of using cp (via the +download_cp plugin) initiated from the receiving site store’s protocol nodes. So the reads +would occur via GPFS over IPoIB, and the writes would be done over native GPFS over IB. +This was not the fastest transfer method tested (bbcp was faster) but it was selected because +it spread the load out to the siteio nodes, resulted in more stable NFS and protocol +nodes and removed tcp/ip setup/teardown overhead. The ‘copy the files to the other side’ part +of the problem was stable by the end of the summer of 2017, and the impact on system stability +is minimized.

+
+
+

Shim Library Necessary

+

Unfortunately, the mirroring between sites was running with about a 10-minute lag on the source files +system (about 30 times faster than a naive rsync approach), and was only working in principle, with +many files missing in practice, it wasn’t usable for its intended purpose. The operational commissioning of the +HPCR solution as a whole (with mirroring deferred) occurred in September of 2017, and work on mirroring essentially +stopped until October (because of activities related to the commissioning work).

+

We continued to work on two approaches, the libsrshim, and the GPFS-policy. The queries run by the GPFS-policy had to to be tuned, eventually +an overlap of 75 seconds (where a succeeding query would ask for file modifications up to a point 75 seconds before the last one +ended) because there were issues with files being missing in the copies. Even with this level of overlap, there were still missing +files. At this point, in late November, early December, the libsrshim was working well enough to be so encouraging that folks lost +interest in the GPFS policy. In contrast to an average of about a 10-minute delay starting a file copy with GPFS-policy queries, +the libsrshim approach has the copy queued as soon as the file is closed on the source file system.

+

It should be noted that when the work began, the python implementation of Sarracenia was a data distribution tool, with no support for mirroring. +As the year progressed features (symbolic link support, file attribute transportation, file removal support) were added to the initial package. +The idea of periodic processing (called heartbeats) was added, first to detect failures of clients (by seeing idle logs) but later to initiate +garbage collection for the duplicates cache, memory use policing, and complex error recovery. The use case precipitated many improvements in +the application, including a second implementation in C for environments where a Python3 environment was difficult to establish, or +where efficiency was paramount (the libsrshim case).

+
+
+

Does it Work?

+

In December 2017, the software for the libsrshim approach looked ready, it was deployed in some small parallel (non-operational runs). Testing +in parallel runs started in January 2018. There were many edge cases, and testing continued for two years, until finally being +ready for deployment in December 2019. I

+
    +
  • FIXME: include links to plugins

  • +
  • FIXME: Another approach being considered is to compare file system snapshots.

  • +
+

As the shim library was used in wider and wider contexts to get it closer to deployment, a significant number of edge cases +were encountered:

+ +

Over the ensuing two years, these edge cases have been dealt with and deployment finally happenned +with the transition to U1 in January 2020. It is expected that the delay in +files appearing on the second file system will be on the order of five minutes +after they are written on the source tree, or 72 times faster than rsync (see +next section for performance info), but we don´t have concrete metrics yet.

+

The question naturally arose, if the directory tree cannot be traversed, how do we know that the source and destination trees are the same? +A program to pick random files on the source tree is used to feed an sr_poll, which then adjusts the path to compare it to the same file +on the destination. Over a large number of samples, we get a quantification of how accurate the copy is. The plugin for this comparison +is still in development.

+
+
+

Is it Fast?

+

The GPFS-policy runs are the still the method in use operationally as this is written (2018/01). The performance numbers given in +the summary are taken from the logs of one day of GPFS-policy runs.

+
+
    +
  • Hall1 to Hall2: bytes/days: 18615163646615 = 16T, nb file/day: 1901463

  • +
  • Hall2 to CMC: bytes/days: 4421909953006 = 4T, nb file/day: 475085

  • +
+
+

All indications are that the shim library copies more data more quickly than the policy based runs, +but so far (2018/01) only subsets of the main tree have been tested. On one tree of 142000 files, the GPFS-policy run had a mean +transfer time of 1355 seconds (about 23 minutes), where the shim library approach had a mean transfer time of 239 seconds (less than +five minutes) or a speedup for libshim vs. GPFS-policy of about 4:1. On a second tree where the shim library transferred 144 +thousand files in a day, the mean transfer time was 264 seconds, where the same tree with the GPFS-policy approach took 1175 +(basically 20 minutes). The stats are accumulated for particular hours, and at low traffic times, the average transfer time with +the shim library was 0.5 seconds vs. 166 seconds with the policy. One could claim a 300:1 speedup, but this is just inherent to +the fact that GPFS-policy method must be limited to a certain polling interval (five minutes) to limit impact on the file system, +and that provides a lower bound on transfer latency.

+

On comparable trees, the number of files being copied with the shim library is always higher than with the GPFS-policy. While +correctness is still being evaluated, the shim method is apparently working better than the policy runs. If we return to the +original rsync performance of 6 hours for the tree, then the ratio we expect to deliver on is 6 hours vs. 5 minutes … +or 72:1 speedup.

+

The above is based on the following client report:

+
Jan 4th
+Preload:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t daily -d 2018-01-04
+Mean transfer time: 238.622s
+Max transfer time: 1176.83s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wh/banco/cutoff/2018010406_078_prog_gls_rel.tb0
+Min transfer time: 0.0244577s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/backup/ATX_radprm
+Total files: 142426
+Files over 300s: 44506
+Files over 600s: 14666
+Policy:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/mirror-ss1-from-hall2.conf  -t daily -d 2018-01-04
+Mean transfer time: 1355.42s
+Max transfer time: 2943.53s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/surface/201801041500_tt.obs
+Min transfer time: 1.93106s for file: /space/hall2/sitestore/eccc/cmod/prod/archive.dbase/dayfiles/par/2018010416_opruns_capa25km_rdpa_final
+Total files: 98296
+Files over 300s: 97504
+Files over 600s: 96136
+
+Jan 3rd
+Preload:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t daily -d 2018-01-03
+Mean transfer time: 264.377s
+Max transfer time: 1498.73s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/capa/bassin/6h/prelim/05/2018010312_05ME005_1.dbf
+Min transfer time: 0.0178287s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/XSS_0p1_statqpe
+Total files: 144419
+Files over 300s: 60977
+Files over 600s: 14185
+Policy:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/mirror-ss1-from-hall2.conf  -t daily -d 2018-01-03
+Mean transfer time: 1175.33s
+Max transfer time: 2954.57s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/surface/201801032200_tt.obs
+Min transfer time: -0.359947s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/1h/XTI/201801031300~~PA,PA_PRECIPET,EE,1H:URP:XTI:RADAR:META:COR1
+Total files: 106892
+Files over 300s: 106176
+Files over 600s: 104755
+
+To keep in mind:
+
+We have 12 instances for the preload while we’re running 40 for the policy.
+
+* I filtered out the set of files that skewed the results heavily.
+* The preload audit in hourly slices shows that it’s heavily instance-bound.
+* If we were to boost it up it should give out much better results in high count situations.
+
+Here’s Jan 4th  again but by hourly slice:
+
+
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t hourly -d 2018-01-04
+00 GMT
+Mean transfer time: 0.505439s
+Max transfer time: 5.54261s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/6h/XME/201801040000~~PA,PA_PRECIPET,EE,6H:URP:XME:RADAR:META:NRML
+Min transfer time: 0.0328007s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/IWX_0p5_statqpe
+Total files: 847
+Files over 300s: 0
+Files over 600s: 0
+01 GMT
+Mean transfer time: 166.883s
+Max transfer time: 1168.64s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wh/banco/cutoff/2018010318_078_prog_gls_rel.tb0
+Min transfer time: 0.025425s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/XPG/201801031800_XPG_statomr
+Total files: 24102
+Files over 300s: 3064
+Files over 600s: 1
+02 GMT
+Mean transfer time: 0.531483s
+Max transfer time: 4.73308s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/archive.dbase/dayfiles/par/2018010401_opruns_capa25km_rdpa_preli
+Min transfer time: 0.0390887s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/XMB/201801031900_XMB_radprm
+Total files: 774
+Files over 300s: 0
+Files over 600s: 0
+03 GMT
+Mean transfer time: 0.669443s
+Max transfer time: 131.666s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/1h/WKR/201801032000~~PA,PA_PRECIPET,EE,1H:URP:WKR:RADAR:META:COR2
+Min transfer time: 0.0244577s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/backup/ATX_radprm
+Total files: 590
+Files over 300s: 0
+Files over 600s: 0
+04 GMT
+Mean transfer time: 59.0324s
+Max transfer time: 236.029s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wf/depot/2018010400/nemo/LISTINGS/ocean.output.00016.672
+Min transfer time: 0.033812s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/resps_20171107/forecast/products_dbase/images/2018010400_resps_ens-point-ETAs_239h-boxplot-NS_Pictou-001_240.png
+Total files: 2297
+Files over 300s: 0
+Files over 600s: 0
+05 GMT
+Mean transfer time: 6.60841s
+Max transfer time: 28.6136s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/rewps_20171018/forecast/products_dbase/images_prog/2018010400_rewps_ens-point-Hs_Tp_072h-45012-000_072.png
+Min transfer time: 0.0278831s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/XSM/201801032200_XSM_0p2_statqpe
+Total files: 3540
+Files over 300s: 0
+Files over 600s: 0
+06 GMT
+Mean transfer time: 1.90411s
+Max transfer time: 18.5288s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/ARX_0p5_statqpe
+Min transfer time: 0.0346384s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/WWW/201801040600_WWW_statomr
+Total files: 757
+Files over 300s: 0
+Files over 600s: 0
+07 GMT
+Mean transfer time: 262.338s
+Max transfer time: 558.845s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/capa/bassin/6h/final/11/2018010400_11AA028_1.shp
+Min transfer time: 0.028173s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/DLH/201801040000_DLH_statomr
+Total files: 23849
+Files over 300s: 11596
+Files over 600s: 0
+
+
+
+
+

Overheads

+

What is the effect on user jobs of putting the shim library in service? +When used in large models with good i/o patterns necessary for high +performance, the overhead added by the shim library can be negligeable. +However there is additional overhead introduced whenever a process is spawned, +closes a file, and when it terminates. Shell scripts, which +function by spawning and reaping processes continuously, see maximum +impact from the shim library. This is explored in Issue https://github.com/MetPX/sarrac/issues/15 :

+

Issue 15 describes the worst case shell script that re-writes a file, one line +at a time, spawning and reaping a process every time. In that case, we see as +much as an 18 fold penalty in shell script performance. However re-writing +the shell script in python can yield a 20 fold improvement in performance, +with almost no overhead from the shim library (360 times faster than the +equivalent shell script with the shim library active.)

+

So shell scripts that were slow before, may be much slower with the shim +library, but the accelleration available by re-formulating to more efficient +methods can have much larger benefits as well.

+
+
+

Contributions

+

Dominic Racette - ECCC CMC Operations Implementation

+
+

Client lead on the mirroring project. A lot of auditing and running of tests. +Integration/deployment of copying plugins. A great deal of testing and extraction of log reports. +This was a project relied extensive client participation to provide a hugely varied test suite, +and Dominic was responsible for the lion´s share of that work.

+
+

Anthony Chartier - ECCC CMC Development

+
+

Client lead on the Acquisition de Données Environnementales the data acquisition system used by +Canadian numerical weather prediction suites.

+
+

Doug Bender - ECCC CMC Operations Implementation

+
+

Another client analyst participating in the project. Awareness, engagement, etc…

+
+

Daluma Sen - SSC DCSB Supercomputing HPC Optimization

+
+

Building C libraries in HPC environment, contributing the random file picker, general consulting.

+
+

Alain St-Denis - Manager, SSC DCSB Supercomputing HPC Optimization

+
+

Inspiration, consultation, wise man. Initially proposed shim library. Helped with debugging.

+
+

Daniel Pelissier - SSC DCSB Supercomputing HPC Integration / then replacing Alain.

+
+

Inspiration/consultation on GPFS-policy work, and use of storage systems.

+
+

Tarak Patel - SSC DCSB Supercomputing HPC Integration.

+
+

Installation of Sarracenia on protocol nodes and other specific locations. Development of GPFS-policy scripts, +called by Jun Hu’s plugins.

+
+

Jun Hu - SSC DCSB Supercomputing Data Interchange

+
+

Deployment lead for SSC, developed GPFS-policy Sarracenia integration plugins, +implemented them within sr_poll, worked with CMOI on deployments. +Shouldered most of SSC’s deployment load. Deployment of inotify/sr_watch implementation.

+
+

Noureddine Habili - SSC DCSB Supercomputing Data Interchange

+
+

Debian packaging for C-implementation. Some deployment work as well.

+
+

Peter Silva - Manager, SSC DCSB Supercomputing Data Interchange

+
+

Project lead, wrote C implementation including shim library, hacked on the Python +also from time to time. Initial versions of most plugins (in Sarra.)

+
+

Michel Grenier - SSC DCSB Supercomputing Data Interchange

+
+

Python Sarracenia development lead. Some C fixes as well.

+
+

Deric Sullivan - Manager, SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/work on deployments with inotify solution.

+
+

Walter Richards - SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/work on deployments with inotify solution.

+
+

Jamal Ayach - SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/work on deployments with inotify solution, also +native package installation on pre and post processors.

+
+

Michael Saraga - SSC DCSB .Data Interchange

+
+

work on the C implementation in 2019, prepared native packaging and packages +for Suse and Redhat distributions.

+
+

Binh Ngo - SSC DCSB Supercomputing HPC Solutions

+
+

native package installation on cray backends.

+
+

FIXME: who else should be here: ?

+

There was also support and oversight from management in both ECCC and SSC throughout the project.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/deployment_2018.html b/Explanation/History/deployment_2018.html new file mode 100644 index 000000000..2fcac2b4f --- /dev/null +++ b/Explanation/History/deployment_2018.html @@ -0,0 +1,427 @@ + + + + + + + Sarracenia Status January 2018 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sarracenia Status January 2018

+

[ version française ]

+

Sarracenia is a small application iteratively developed by addressing one use +case at a time, so development and deployment have been inextricably linked up +to this point. That iterative process precipitated changes in the core of the +application which have made it something of a moving target until now. In +January 2018, the application has reached the point where all intended use cases +are addressed by the application core. In the coming year, the emphasis will be +on facilitating on-boarding, development of some derived services, and +deploying the newly complete application more generally.

+ +
+

Comparison to 2015 Video

+

The November 2015 video ( Sarracenia in 10 Minutes ) +outlined a vision. First phase of development work occurred in 2015 and early +2016, followed by important deployments later in 2016. This update, +written in early 2018, explores progress made mostly in 2017.

+

Use cases mentioned in the video which were implemented:

+
    +
  • Central meteorological pumping has made substantial progress in migrating +to the new stack. This was the central initial use case that drove initial work. +The transformation is not complete but is well in hand.

  • +
  • Redundant RADAR acquisition through two national hubs (spring 2016)

  • +
  • National Ninjo (main workstation for Forecasters) Dissemination (summer 2016)

  • +
  • Unified RADAR Processing (application to transform volume scans to products) +data flows (through 2017)

  • +
+

Use cases in the video, but not yet realized:

+
    +
  • End user usage. A few trials were completed in early in 2017 leading to some +review, refactoring, and now retesting.

  • +
  • Data sets from sequencers have been waiting for end user use cases to be +improved.

  • +
  • Reporting to sources who consumed their products. The feature is +designed but further implementation, testing and careful deployment is needed.

  • +
  • Multi-pump routing by source-routing. Currently routing through multiple +pumps is done by pump administrators, rather than end users. Without end-user +on-boarding, routing by sources was a low priority.

  • +
  • The mesh interconnect model envisioned has not seen an appropriate use +case.

  • +
+

Unanticipated use cases implemented:

+
    +
  • GTS data exchange: CMC <-> NWS. NWS requested a change in connectivity +in December 2015.

  • +
  • HPC mirroring (to be completed in Spring 2018).

  • +
  • Legacy Application 7-way replication (for SPCs) implemented last year.

  • +
  • GOES-R acquisition (live as of January 2018).

  • +
+

Details to follow.

+
+
+

Central Data Flows

+

The slide below corresponds to deployed daily data flows in support of +Environment Canada, mostly for operational weather forecasting, in place since +January 2018.

+../../_images/E-services_data-volume_pas.png +

Sarracenia is being used operationally to acquire about four terabytes of +observations from automated weather observing systems, weather RADARS which +deliver data directly to our hubs, and international peer operated public file +services, which provide satellite imagery and numerical products from other +national weather centres.

+

Within the main high performance computing (HPC) data centre, there are two +supercomputers, two site stores, and two pre- and post-processing clusters. +Should a component in one chain fail, the other can take over. The input +data is sent to a primary chain, and then processing on that chain is mirrored, +using sarracenia to copy the data to the other chain. That’s about 16 of the +25 terabytes of the data centre traffic in this diagram.

+

A distillation of the data acquired, and the analysis and forecasts done in HPC, +is the 7 terabytes at the top right, that is sent to the seven regional +Storm Prediction Centres (SPCs).

+

The products of the SPCs and the central HPC are then shared with the public +and partners in industry, academia and other governments.

+
+
+

Weather Application Flows

+

FIXME: picture?

+

There is a number (perhaps a dozen?) of older applications (the most prominent ones +being BULLPREP and Scribe) used for decades in the Storm Prediction Centres +to create forecast and warning products. These applications are based on a file +tree that they read and write. Formerly, each application had its own backup +strategy with one of the six other offices and bi-lateral arrangements were made +to copy specific data among the trees.

+

In January 2017, complete 7-way replication of the state file trees of the +applications was implemented so that all offices have copies of files in +real-time. This is accomplished using Sarracenia through the eastern hub. Any +forecast office can now take over work of any product for any other, with no specific +application work needed at all.

+
+
+

GOES-R Acquisition

+

Acquisition of simulated and real GOES-R products from NOAA’s PDA, as well as +via local downlinks at one location (eventually to become two) was entirely +mediated by Sarracenia. The operational deployment of GOES-R happened in the +first week of January, 2018.

+
+
+

HPC Acquisition Feeds

+

FIXME: picture?

+

The supercomputing environment was entirely replaced in 2017. As part of that, +the client Environmental Data acquisition suite (ADE in French) was +refactored to work with much higher performance than previously, and to accept +Sarracenia feeds directly, rather than accepting feeds from previous generation +pump (Sundew). The volume and speed of data acquisition has been substantially +improved as a result.

+
+
+

RADAR Data Flows

+

If we begin with RADAR data acquisition as an example, individual RADAR systems +use FTP and/or SFTP to send files to eastern and western communications hubs. +Those hubs run the directory watching component (sr_watch) and determine +checksums for the volume scans as they arrive. The Unified RADAR Processing +(URP) systems sr_subscribes to a hub, listening for new volume scans, and +downloads new data as soon as they are posted. URP systems then derive new +products and advertise them to the local hub using the sr_post component. +In time, we hope to have a second URP fully at the western hub.

+

In regional offices, the NinJo visualization servers download volume scans and +processed data from URP using identical subscriptions, pulling the data from +whichever national hub makes the data available first. The failure of a +national hub is transparent for RADAR data in that the volume scans will be +downloaded from the other hub, and the other URP processor will produce the +products needed.

+../../_images/RADAR_DI_LogicFlow_Current.gif + +

Each site has multiple Ninjo servers. We use http-based file servers, or web accessible folders to serve data. +This allows easy integration of web-proxy caches, which means that only the first Ninjo server to request data +will download from the national hub. Other Ninjo servers will get their data from the local proxy cache. +The use of Sarracenia for notifications when new products are available is completely independent of the +method used to serve and download data. Data servers can be implemented with a wide variety of tools +and very little integration is needed.

+
+
+

HPC Mirroring

+

All through 2017, work was proceeding to implement high speed mirroring between the supercomputer site stores +to permit failover. That work is now in a final deployment phase, and should be in operations by spring 2018. +For more details see: HPC Mirroring Use Case

+
+
+

Application Changes in 2017

+

Development of Sarracenia had been exploratory over a number of years. The use cases initially attacked +were those with a high degree of expert involvement. It proceeded following the minimum viable product (MVP) +model for each use case, acquiring features to deal with next use case prior to deployment. In 2016, +national deployment of NinJo and the Weather.

+

Expanded use cases explored:

+
    +
  • Mirroring: Prior to this use case, Sarracenia was used for raw data dissemination without regard for +permissions, ownership, symbolic links, etc… For the mirroring use case, exact metadata +replication was a surprisingly complex requirement.

  • +
  • C-implementation: In exploring large scale mirroring, it became obvious that for sufficiently large +trees (27 Million files), the only practical method available was the use of a C shim library. +Having all user codes invoke a Python3 script is complete nonsense in an HPC environment, so +it was necessary to implement a C version of Sarracenia posting code for use by the shim library. +Once the C implementation was begun, it was only a little additional work to implement a C version +of sr_watch (called sr_cpost) which was much more memory and CPU efficient than the Python original.

  • +
  • Node.js implementation: A client of the public datamart decided to implement enough of Sarracenia +to download warnings in real-time.

  • +
  • The application was refactored to maximize consistency through code reuse, reducing about 20% of +the code size at one point. The code returned to the initial size when new features were added, +but it remains quite compact at less than 20 kloc.

  • +
  • End-user usage: All of the deployments thus far are implemented by analysts with a deep understanding +of Sarracenia, and extensive support and background. This year, we went through several iterations +of having users deploy their flows, collecting feedback and then making it easier for end users at +the next iteration. Many of these changes were breaking changes, in that options and ways or +working were still prototypes and required revision.

  • +
+

Changes to support end user usage:

+
    +
  • Exchanges were an administrator-defined resource. Permission model changed such that users can now declare exchanges.

  • +
  • Previously, one had to look on web sites to find examples. Now, the list command displays many examples included with the package.

  • +
  • It was hard to find where to put settings files. The list/add/remove/edit commands simplify that.

  • +
  • In each plugin entry point, one had to modify different instance variables, was refactored for consistency +across all of them (on_msg, on_file, on_part, on_post, do_download, do_send, etc…).

  • +
  • Partitioning specifications were arcane and were replaced with the +blocksize option, with only three possibilities: 0, 1, many.

  • +
  • Routing across multiple pumps was arcane. The original algorithm was +replaced by a simpler one with some smarter defaults. Users can now usually +ignore it.

  • +
  • A much more elegant plugin interface is available to have multiple routines that +work together, specified in a single plugin.

  • +
  • Previously, only advertised on web servers relative to the root URL. Now, +non-root base URL support was added.

  • +
+

The only major operational feature introduced in 2017 was +save/restore/retry: if a destination has a problem, there is +substantial risk of overloading AMQP brokers by letting queues of products to +transfer grow into millions of entries. Functionality to efficiently (in +parallel) offload broker queues to local disk was implemented to address +this. At first, recovery needed to be manually triggered (restore) but by +the end of the year, an automated recovery (retry) mechanism was working its +way to deployment, which will reduce requirements for oversight and +intervention in operations.

+
+
+

Coming in 2018

+

As of release 2.18.01a5, all of the use cases targeted have been explored and +reasonable solutions are available, so there should be no further changes to +the existing configuration language or options. No changes to existing +configuration settings are planned. Some minor additions may still occur, +but not at the cost of breaking any existing configurations. The core +application is now complete.

+

Expect in early 2018 for the last alpha package release and +for subsequent work to be on a beta version with a target of a much more +long-lived stable version some time in 2018.

+
    +
  • HPC mirroring use case deployment will be completed.

  • +
  • The Permanent File Depot (PFD) use case will be deployed. Currently, this +is used to cover a short time horizon. One can extend it arbitrarily into the +past by persisting the time-based tree to nearline storage. In development +since 2016, gradually progressing.

  • +
  • Improve deployment consistency: The changes in 2017 were confusing for the +expert analysts, as significant changes in details occurred across versions. +Different deployments currently use different operational versions, and most +issues arising in operations are addressed by the existing code, but are not +yet deployed to that use case. In 2018, we will revisit early deployments to +bring them up to date.

  • +
  • Continued improvement in pre-deployment testing.

  • +
  • The Sarrasemina indexing tool, which facilitates finding feeds, to be deployed +to assist onboarding.

  • +
  • Improved onboarding documentation. Reference materials are thorough, but +introductory quick-start and gateway oriented materials need work. +French translations are also needed.

  • +
  • Reporting: While reporting was baked in from the start, it proved to be very +expensive, and so deployments to date have omitted it. Now that deployment +loads are quieting down, this year should allow us to add real-time report +routing to deployed configurations. There is no functionality to develop, +as everything is already in the application, but mostly not used. Use may +uncover additional issues.

  • +
  • Pluggable checksum algorithms. Currently checksum algorithms are baked into +the implementations. There is a need to support plugins to support +user-defined checksum algorithms (expected in 2.18.02a1).

  • +
  • Continued progressive replacement of legacy application configurations +(RPDS, Sundew).

  • +
  • Continued adaptation of applications to Sarracenia (DMS, GOES-R).

  • +
  • Deployment of additional instances: flux.weather.gc.ca, +hpfx.collab.science.gc.ca, etc…

  • +
  • Continued work on the corporate approval and funding of the western hub (aka. +Project Alta).

  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/index.html b/Explanation/History/index.html new file mode 100644 index 000000000..8a9fa50cb --- /dev/null +++ b/Explanation/History/index.html @@ -0,0 +1,217 @@ + + + + + + + History — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

History

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/mesh_gts.html b/Explanation/History/mesh_gts.html new file mode 100644 index 000000000..2998ddfee --- /dev/null +++ b/Explanation/History/mesh_gts.html @@ -0,0 +1,1014 @@ + + + + + + + Mesh-Style Data Exchange for the WIS-GTS in 2019 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Mesh-Style Data Exchange for the WIS-GTS in 2019

+

Originally part of a demonstration to the WMO committee on message queueing protocols. +While this proposal was not retained at that time, it has served as the basis +for much discussion since.

+ +
+

Executive Summary

+

Over the past decade, the World Meteorological Organization (WMO) Information +Service WIS Part 1 has implemented meta data catalogues for some world-wide +meteorological datasets. In WIS-DAR, Discovery, Access, and Retrieval, +the emphasis on this early work was on Discovery, as this was a major +difficulty. In terms of actual data exchange, the pre-existing network: the +Global Telecommunications System (GTS) has remained the WMO’s accepted method +to circulate weather data in meteorological real-time [1] used to +achieve Access and support Retrieval. It does so by established +bi-lateral agreements among individual members.

+

The pre-existing network feeds pre-existing systems that operate on different +premises from WIS, and there isn’t a particularly good mapping from D +to A, and R today. Use of WIS metadata to drive real-time data +exchange is not efficient, and GTS data is not easily interoperable with +other industries’ mostly web-based technologies.

+

This brief suggests that a mesh-like network of web servers is possible now +because of great advances in technology that reduce the need for rules and +structure for international exchange of meteorological data. Previously, +technological limitations placed stringent constraints on international data +exchange. These days, effective international data exchange can be arranged +with fewer interveners, less hierarchy, less coordination. A more +loosely coupled and flexible interconnection strategy makes it much +easier for members to exchange new datasets in a systematic way, without having +to develop the bi-lateral or multi-lateral exchange agreements. +Indeed often, the GTS is circumvented because it is so ill-suited +to such exchanges.

+

Members that agree to exchange data, can do so directly, which completes +the dream of the WIS. The existing work on WIS, standardizing metadata +exchange is completed by adding the exchange of the underlying +data using standardized open methods, with a straightforward mapping +from the metadata. As the proposed implementation uses +existing standards, the WMO does not need to define any additional ones, +and interoperability and access for other players in the broader +society should be straightforward. The sr3_post protocol, and +a number of existing implementations of it, are a great fit.

+

While it is believed that sr3_post protocol has great potential +to improve WMO data exchange, it will take a few years to adopt it, +and prior to adoption, there needs to be agreement on the file tree content. +Today, the next step would be to find some partner countries with which +to engage in some experimental data exchanges to validate the approach, +and collaboratively determine implementation details.

+ +
+
+

GTS was Designed Long Ago

+../../_images/gtsstructureL.png +

The World Meteorological Organization (WMO) Information Service (WIS)’s Global +Telecommunications System (GTS) is the WMO’s accepted method to circulate +weather data in meteorological real-time [1] It does so by pre-established +bi-lateral agreements about who circulates what to whom. When data +arrives, it is sent to the proper destinations immediately. In hardware terms, +the GTS used to be a set of point-to-point links. It is, in general, very reliable +and has good performance for the sorts of data for which it was envisioned.

+

The GTS successfully and elegantly applied the technologies of the 1940’s to +obtain world-wide exchange of weather data, most obviously, to enable +commercial air travel to expand exponentially over the succeeding decades. It +was made in a world of expensive point-to-point telephone links, very low +bandwidth, very little computational power, and few existing standards for +reliable data transfer. Today, The underlying technologies embodied by +Internet and Regional Main Data Communications Network (RMDCN) +(which have already subsumed the physical links of GTS) are completely +different: bandwidth and storage are relatively cheap, computing power is +cheaper, point-to-point links are now more expensive than multi-point clouds. +Today’s WMO members want to exchange orders of magnitude more data types +and higher volumes at higher speed.

+

The weather community has, in the past, needed to create standards because no +similar need in the rest of industry existed. Over the last few decades, as +internet adoption has broadened and deepened, horizontal solutions for most of +the technical issues addressed by WMO standards have emerged. The WMO’s +strategic direction always has been and will continue to be to use +technologies from the wider world and not define their own standards +unnecessarily.

+
+
+

GTS is Limited & Inflexible

+../../_images/GTS_Routing.jpg +

In the traditional GTS, when new data is made available by a National +Meteorological Centre (NMC), it needs to issue notices, and likely discuss with +their Regional Telecommunications Hub (RTH), about accepting and routing the +new data. The RTHs must discuss amongst each other whether to exchange the +data, and other NMC must further request from their RTH. For NMC A to make +data available to NMC B, the GTS staff at both NMC’s and all intervening +RTHs must agree and action the request.

+

Also, the routing tables at each NMC and each RTH are theoretically, but in +practice, not easily, inspectable. It may be that the product NMC B is looking +for is already available at their local RTH, but neither A, nor B have any +effective way of determining that, other than asking the administrators of B’s +RTH. Manual routing is ineffective, opaque and human resource intensive.

+

Lastly, the GTS has a maximum message size, which though it was raised from +14,000 bytes to 500,000 bytes in the last few years, is still very limiting. +This rules out many modern important data sets (e.g. RADAR, Satellite, NWP). +WMO doesn’t need its own data transport, as is demonstrated by many +members’ use of protocols without such limitations, adopted from the broader +internet, such as FTP, SFTP, and HTTP. Even more often, such transfers +are accomplished by bi-lateral arrangement, as transfers of larger datasets +cannot be expressed in current GTS protocols.

+

The initial WIS, as formulated over a decade ago, was in part an attempt to address +this opaqueness by introducing some Information Management (IM) concepts, and +support for DAR: Discovery, Access, and Retrieval. All WMO members want to +know is which data is available from which centres. So we publish metadata to +Global Information Service Centres, where the world’s real-time weather +information is available and some means of retrieval is specified.

+

When dealing in the abstract, without time constraints, add/or with small +datasets, retrieval time does not matter, but the access penalty imposed by +using databases for individual retrieval grows with the number of +items stored and the number of queries or retrievals to be sustained. +Initially, WIS was most concerned with getting higher visibilty of data, +understanding what data was available. WIS Part 1 primarily implemented a +metadata layer, while the GTS has persisted to transfer actual data. Once +WIS Part 1 was in service and DAR was available, which at first blush appears +much easier and friendlier, why didn’t everyone just use DAR to replace the +GTS?

+../../_images/dar.png +

The WIS architecture tends to concentrate load at the GISCS, whether they want it +or not. Even assuming they want it, answering large volumes of queries in such +an architecture is a problem. The mental model for this is a database and each +retrieval is conceptualized as a query. From computational complexity theory[2]_, +each query is often an O(N) operation (or in the best case of perfect indexing, +O(log N)) and retrieval of specific items by their key is also, at best, O(log N).

+

To perform Retrieval (the R in DAR) of all the items from an index, one +at a time, the best algorithm has complexity N * log N. The best case only +arises if the indices are properly designed for the access method intended. +In practice, without careful design & implementation, the performance can +devolve to N**2.

+ +
+
+

Databases are Optimal for Fixed Sized Records. No One Uses Them That Way

+

There are typically two major schools of thought on meteorological data +storage: Files and Databases. For many years, data was relatively small, +there were relatively few datatypes, and they fit in small databases and +with enough analysis one could normalize them down to fixed size fields.

+

Relational databases were invented a decade or two after the GTS, and they +optimize storage and retrieval of fixed size data. They achieve near optimal +performance by careful selection of the data model and extensive use of +fixed size fields. In practice, the use of fixed size fields turns out to +be a difficult constraint to satisfy and many interesting data sets, especially +on systems whose primary function is data transmisssion, are most logically +stored as arbitarily sized byte sequences, generally termed: Binary Large +Objects, or BLOBS. When one stores BLOBS, a database becomes +algorithmically a little different from a file system.

+

Essentially BLOBS are an indexed list of byte streams. Once a database +uses BLOBS, it incurs the same overhead for data retrieval as +a properly used file system. Regardless of the storage method, +the cost of retrieval is going to be O(log N) for a known key. +In a file system, the key is the name. In a DB, the key is an object-id +or index value. If one wishes to use multiple indices, then one +can still use a database, which is largely what WIS Part 1 is. It +is simply a standard pratice to store the actual data in BLOBS, and file +systems are a competitive method of storing those.

+

Over time, as data items grow in individual size, it makes progressively +more and more sense to store them in file systems, and to confine use of +database systems to store metadata and indices that point to the data +items stored in files.

+
+
+

Internet Push is a Poor Fit for Large Feeds

+

So called Push technologies are actually Pull. A client asks a server if +they have new data available for them. The server responds with the list of new +data available, then the client pulls the data. This means that an entity +storing data has to retrieve the items from the data store (with an O(log N) cost +to each retrieval.) As the domain is real-time processing, the time for data +to be obtained by a client is also relevant, and bound by the maximum frequency +that a client is allowed to ask for updates. In general, the ATOM/RSS protocols +expect a minimum interval between polling events of five minutes.

+

Each polling event requires the server to examine its database for all +matching entries, this search is likely an order N operation. So the responses +to polling requests are expensive, and the retrievals from the data system are +likewise expensive, which likely motivates the usual discouragement of rapid +polling.

+

In the best case, indices based on time will be present, and one will be able +to search table with that dimension and incur log(N) operations to find the +earliest observation to retrieve, and then walk forward along that index. +In many practical cases, databases are not indexed by time, and so the initial +search is for all stations, and then one must examine the time for the entries +retrieved, which will incur N**2 operations, and in some cases it can be +even worse.

+

The actual cost of serving a client depends critically on the server’s indices +being optimally constructed. These characteristics are hidden in a database +and are not easily inspected by anyone but the database administrator.

+
+
+

Store And Forward is Often Better in Practice

+

“Store and Forward” is a term we will use here to denote technologies that +deal with data on receipt, in contrast to simply storing the data and +awaiting clients’ polls. Real-time systems such as the GTS get around the +retrieval expense problem by storing and forwarding at the same time. When +a datum is received, a table of interested parties is consulted, and then +the forwarding is done based on the data already “retrieved”.

+

The cost to forward an item to a given client is closer to O( log N ).

+

This works as an optimization because one is forwarding the message at exactly +the time it is received, so the entire lookup and search process is skipped +for all those known consumers. For comparison, the polling web standards +standardizes the cost of search at every polling interval.

+

The cost of the search is highly variable and not under server control. Poorly +structured queries (e.g. by station, and then time) can result in an N*log(N) +query or even N-squared complexity.

+

This is especially acute for weather alert information, where a high polling +frequency is a business need, but the volume of data is relatively low (alerts +are rare). In such cases the polling data can be 10 times or even 100 times the +amount of data transfer needed to send the warnings themselves.

+

In practice, the load on servers with large real-time flows to many clients will +be orders of magnitude lower with a real push technology, such as the +traditional GTS, than supporting the same load with Internet Push technologies. +A separate but related cost of polling is the bandwidth for the polling data. +By forwarding notifications on receipt, rather than having to service polls, one +reduces overall load, eliminating the vast majority of read traffic.

+

A real-world example of bandwidth savings, from 2015, would be that of a German +company that began retrieving NWP outputs from the Canadian datamart using web-scraping +(periodic polling of the directory.) When they transitioned to using the +AMQP push method, the total bytes downloaded went from 90 Gbytes/day to +60 Gbytes per day for the same data obtained. 30 GBytes/day was just +(polling) information about whether new model run outputs were available.

+

The requirements for a store and forward system:

+
    +
  • TCP/IP connectivity,

  • +
  • real-time data transmission,

  • +
  • per destination queueing to allow asynchrony (clients that operate at different speeds or have transient issues),

  • +
  • application level identity guarantees.

  • +
+

In addition, the ability to tune subscriptions, according to the client’s +interest will further optimize traffic.

+

In terms of internet technologies, the main protocols for real-time data +exchange are XMPP and websocket. XMPP provides real-time messaging, but it does +not include any concept of subscriptions, hierarchical or otherwise, or +queueing. Web sockets are a transport type technology. Adopting either of these +would mean building a domain specific stack to handle subscriptions and +queueing. The Advanced Message Queueing Protocol (AMQP) is not web technology, +but it is a fairly mature internet standard, which came from the financial +industry and includes all of the above characteristics. It can be adopted +as-is and a relatively simple AMQP application can be built to to serve +notifications about newly arrived data.

+

While AMQP provides a robust messaging and queueing layer, a small additional +application that understands the specific content of the AMQP messages, and +that is the value of the Sarracenia protocol and application offerred +as the protocol’s reference implementation. Sarracenia sends and receives +notifications over AMQP. That application neither requires, nor has, +any WMO-specific features, and can be used for real-time data replication +in general.

+../../_images/A2B_message.png +

A Sarracenia notification contains a Uniform Resource Location (URL) informing +clients that a particular datum has arrived, thus inviting them to download it. +The URL can advertise any protocol that both client and server understand: HTTP, +HTTPS, SFTP for example. If new protocols become important in the future, +then their implementation can be done with no change in the notification layer.

+

As these notifications are sent in real-time, clients can initiate +downloads while the datum in question is still in server memory and thus benefit +from optimal retrieval performance. As the clients’ time of access to the data +is more closely clustered in time, overall i/o performed by the server is +minimized.

+

A notification also contains a fingerprint, or checksum, that uniquely +identifies a product. This allows nodes to identify whether they have +received a particular datum before or not. This means that the risks of +misrouting data are lower than before because if there are any cycles in the +network, they are resolved automatically. Cycles in the connectivity graph are +actually a benefit as they indicate multiple routes and redundancy in the +network, which will automatically be used in the event of node failure.

+
+
+

With AMQP Notices on a Standard File Server

+

Several robust and mature protocols and software stacks are available for many +data transport protocols: FTP, HTTP, HTTP(S), SFTP. A file server, as a means +of Transporting data is a solved problem with many solutions available from +the broader industry. In contrast to data transport, pub/sub is an atomized +area with myriad niche solutions, and no clearly dominant solution.

+

The Advanced Message Queueing Protocol is an open standard, pioneered +by financial institutions, later adopted by many software houses large +and small. AMQP is a replacement for proprietary message passing systems +such as IBM/MQ, Tibco, Oracle SOA and/or Tuxedo. RabbitMQ is a prominent +AMQP implementation, with deployments in many different domains:

+ +

Rabbitmq provides a mature, reliable message passing implementation +currently, but there are many other open source and proprietary +implementations should that ever change. AMQP brokers are +servers that provide message publish and subscribe services, with +robust queuing support, and hierarchical topic based exchanges.

+

Each Server runs a broker to advertise their own contribution, and they subscribes to +each others’ notification messages. Advertisements are transitive, in that each +node can advertise whatever it has downloaded from any other node so that other +nodes connected to it can consume them. This implements mesh networking +amongst all NC/DCPC/GISCs.

+

An AMQP notification layer added to the existing file transfer network would:

+
    +
  • improve security because users never upload, never have to write to a remote server. +(all transfers can be done by client initiated subscriptions, no write to peer servers needed).

  • +
  • permit ad-hoc exchanges among members across the RMDCN without having to involve third parties.

  • +
  • can function with only anonymous exchanges, to eliminate the need for authentication entirely. +additional explicit authentication is available if desired.

  • +
  • provide a like-for-like mechanism to supplant the traditional GTS +(similar performance to existing GTS, no huge efficiency penalties).

  • +
  • in contrast to current GTS: no product size limit, can function with any format. +inserting data is a matter of picking a file hierarchy (name)

  • +
  • transparent (can see what data is on any node, without requiring human exchanges). +(Authorized persons can browse an FTP/SFTP/HTTP tree).

  • +
  • enable/support arbitrary interconnection topologies among NC/DCPC/GISCs +(cycles in the graph are a feature, not a problem, with fingerprints).

  • +
  • Shorten the time for data to propagate from NMC to other data centres across the world +(fewer hops between nodes than in GTS, load more distributed among nodes).

  • +
  • relatively simple to configure for arbitrary topologies +(configure subscriptions, little need to configure publication).

  • +
  • route around node failures within the network in real-time without human intervention +(routing is implicit and dynamic, rather than explicit and static).

  • +
+
+
+

And an Agreed Directory Tree

+

Similar to the choice of indices in databases, efficiency of exchange in file +servers depends critically on balancing the hierarchy in terms of numbers of files per +directory. A hierarchy which ensures less than 10,000 files per directory performs +well.

+

Example server: http://dd.weather.gc.ca

+

The tree on dd.weather.gc.ca is the original deployment of this type of service. +As an example of the kind of service (though the details would be different for WMO), +it has directory ordering as follows:

+
+
+

There is an initially fixed base url: +http://dd.weather.gc.ca/bulletins/alphanumeric/, +Then the subdirectories begin: date (YYYYMMDD), WMO-TT, CCCC, GG, then +the bulletins, whose content is:

+
Parent Directory                                               -
+[   ] SACN31_CWAO_111200__CYBG_42669            11-Feb-2018 12:01   98
+[   ] SACN31_CWAO_111200__CYQQ_42782            11-Feb-2018 12:02  106
+[   ] SACN31_CWAO_111200__CYTR_43071            11-Feb-2018 12:03   98
+[   ] SACN31_CWAO_111200__CYYR_42939            11-Feb-2018 12:01   81
+[   ] SACN31_CWAO_111200__CYZX_43200            11-Feb-2018 12:02   89
+[   ] SACN43_CWAO_111200__CWHN_43304            11-Feb-2018 12:12   85
+  .
+  .
+  .
+
+
+
+

Note

+

These files do not follow WMO naming conventions, but illustrate some interesting +questions. In WMO bulletins, one should issue only one bulletin with the AHL: SACN31 CWAO 111200 +For circulation to the WMO, these individual observations are collected and indeed sent +as a single SACN31 CWAO 111200, but that means delaying forwarding of CYBG, BYQQ, CYTR +reports while we wait until the end of the collection interval ( 12:05? ) before emitting +the collected bulletin. This datamart, for national use, offers individual observations +as they arrive in real-time, appending the station id as well as a randomizing integer +to the file name, to ensure uniqueness.

+

This is an illustration of an early prototype which remains in use. The actual tree +for WMO use would likely be different.

+
+

Aside from the contents of the tree, the rest of the functionality proposed +would be as described. One can easily subscribe to the datamart to replicate +the entire tree as the data is delivered to it. While the application does not +require it, the standardization of the tree to be exchanged by WMO members +will substantially simplify data exchange. Most likely, an appropriate +tree to standardize for WMO uses would be something along the lines of:

+
20180210/          -- YYYYMMDD
+     CWAO/         -- CCCC, origin, or 'Source' in Sarracenia.
+          00/      -- GG (hour)
+             SA/   -- TT
+                  follow the naming convention from WMO-386...
+
+
+

If we have an ordering by Day ( YYYYMMDD ), then ORIGIN ( CCCC? ), then data +types, and perhaps hour then the trees that result would be nearly optimally +balanced, and ensure rapid retrieval. The optimal configuration is also clearly +visible since this tree is can be inspected by any WMO member simply by browsing +the web site, in contrast to databases, where the indexing schemes are +completely hidden.

+

Nodes copy trees from each other verbatim, so the tree is the relative location +on any node. WIS metadata pointers to the actual data can then be +programmatically modified to refer to the nearest node for data, or a +straight-forward search algorithm can be implemented to ask other nodes, without +the need to resort to an expensive search query.

+

In AMQP, subscriptions can be organized into hierarchical topics, with the +period character (‘.’) as a separator. For this application, the directory tree, +with ‘/’ or ‘' as a separator replaced by AMQP’s separator is is translated +into an AMQP topic tree. AMQP has rudimentary wildcarding, in that it uses the +asterisk (‘*’) to denote any single topic, and the hash symbol (‘#’) is used to +match any remainder of the topic tree. So examples of how one could subscribe +selectively on a node are:

+
v02.post.#            -- all products from all Origins (CCCC)'s on a node.
+v02.post.*.CWAO.#     -- all products from CWAO (Canada) on a node
+v02.post.*.CWAO.WV.#  -- all volcanic ash warnings (in CAP?) from Canada RSMC/VAAC.
+
+
+
+

Note

+

The topic prefix (beginning of the topic tree) is constant for this discussion. Explanation:

+

v02 - identifies the protocol version. Should the scheme change in future, this allows for a server +to serve multiple versions at once. This has already been used to progressively migrate from exp, to v00, to v02.

+

post - identifies the message format. Other formats: report, and pulse. described elsewhere.

+
+

After this first level of filtering is done server side, Sarracenia implements a +further level of client-side filtering using +full Regular Expressions +to either exclude or include specific subsets.

+

To exchange known data types, one needs only define the directories that will be +injected into the network. Nations can adopt their own policies about how much +data to acquire from other countries, and how much to offer for re-transmission. +To propose a new data format or convention, a country uploads to a new directory +on their node. Other countries that wish to participate in evaluating the +proposed format can subscribe to the feed from that node. Other countries that +start producing the new format add the directory to their hierarchy as well. No +co-ordination with intervening parties is needed.

+

Should two countries decide to exchange Numerical Weather Products (NWP), or +RADAR data, in addition to the core types exchanged today, they simply agree on +the directories where this data is to be placed, and subscribe to each others’ +node feeds.

+
+
+

Simple/Scalable Peer Configurations for Nations

+../../_images/WMO_mesh.png +

Assume a mesh of national nodes with arbitrary connectivity among them. +Nodes download from the first neighbour to advertise data, transfers +follow the speed of downloading from each node. If one node slows, +neighbours will get notification messages from other nodes that present +new data earlier. So the network should balance bandwidth naturally.

+

National centres can have as much, or as little, information locally as +they see fit. The minimum set is only the country’s own data. Redundancy is +achieved by many nations being interested in other nations’ data sets. If +one NC has an issue, the data can likely be obtained from another node. NC’s +can also behave selfishly if they so choose, downloading data to internal +services without making it available for retransmission to peers. Super +national nodes may be provisioned in the cloud, for management or resource +optimization purposes. These nodes will ease communication by adding +redundancy to routes between nations. With mesh style interconnection, in the +case of a failure of a cloud provisioned node, it is likely that connections +between countries automatically compensate for individual failures.

+

There is also little to no requirement for the super-national GISC in this +model. Nodes can be established with greater or lesser capacity and they can +decide for themselves which data sets are worth copying locally. As the +subscriptions are under local control, there is a sharply reduced need for +co-ordination when obtaining new data sets. +There is also no need for a node to correspond uniquely to a national centre. +There are many situations where members with more resources assist other members, +and that practice could continue by having nodes insert data onto the GTS +on other countries’ behalf. Redundancy for uploading could also be accomplished +by uploading to multiple initial sites.

+

If there are nodes that, for some reason, do not want to directly +communicate, they do not subscribe to each others notification messages directly. Each +can acquire data safely through intermediaries that each is comfortable with. +As long as there is a single path that leads between the two nodes, data will +arrive at each node eventually. No explicit action by intermediaries is needed +to ensure this exchange, as the normal network will simply route around +the missing edge in the graph.

+

If there is misbehaviour, other nodes can cease subscribing certain data on +a node or cease to bring in any data at all from a node which is injecting +corrupt or unwanted data. It could happen that some nations have very good +bandwidth and server performance. The motivation would be to obtain the data +most quickly for themselves, however by implementing this excellent service, it +attracts more demand for data from the rest of the world. If one node feels +they are shouldering too much of the global load of traffic exchange, there +are many straight-forward means to encourage use of other nodes: not posting, +delayed posting, traffic shaping, etc… All of these techniques are +straight-forward applications of industry technology, without the need to +resort to WMO specific standards.

+
+
+

Using An Open Reference Stack

+../../_images/A2B_oldtech.png +

A sample national mesh node (Linux/UNIX most likely) configuration would +include the following elements:

+
    +
  • subscription application to post national data to the local broker for +others ( Sarracenia )

  • +
  • subscription application connects to other nodes’ brokers ( Sarracenia ) +and post it on the local broker for download by clients.

  • +
  • AMQP broker serve notifications ( Rabbitmq )

  • +
  • http server to serve downloads (plain old apache-httpd, with indexes).

  • +
  • ssh server for management and local uploads by national entities (OpenSSH)

  • +
+

The stack consists of entirely free software, and other implementations can be +substituted. The only uncommon element in the stack is Sarracenia, which so far +as only been used with the RabbitMQ broker. While Sarracenia +( https://metpx.github.io/sarracenia ) +was inspired by the GISC data exchange problem, it is in no way specialized to +weather forecasting, and the plan is to offer it to other for in other domains +to support high speed data transfers.

+

FIXME: add diagram comparing size of various code bases.

+

Sarracenia’s reference implementation is less than 20 thousand lines in Python +3. Clients have contributed open source partial implementations in javascript, +C#, and Go, and have implemented another in C was done to support the +High Performance Computing use case. +The message format is published and demonstrably program +language agnostic.

+

This stack can be deployed on very small configurations, such as a Raspberry Pi +or a very inexpensive hosted virtual server. Performance will scale with +resources available. The main Canadian internal meteorological data pump is +implemented across 10 physical servers (likely too many, as all of them are +lightly loaded).

+
+
+

Maturity

+

For Canada, this is not an experimental project beside other initiatives. +Sarracenia is the focus of around a decade of work and the core of currently +operational data pumping. It is in operational use to transfer +tens of terabytes per day in a wide variety of different use cases.

+

Timeline:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Date

Milestone

2008 for MetPX/Sundew +sender and receiver added.

Initial experiments

2010 National Unified RADAR +processing outputs

Experiment in improving +reliability by first-come +first-serve algorithm. +for outputs of NURP.

+

mutliple calls per month ->0

+

2010 WMO CBS-Ext 10 Windhoek

Initial WMO discussions. +mesh model for GISCs conceived +(work was still experimental)

2013 dd.weather.gc.ca +to present… +dd_subscribe (initial client)

first public deployment

+

some used provided client +software, others wrote their +own. handful of implementations +now.

+

one German client for +Grib output download traffic +saves 30 G/day of bandwidth

+

2013 MetPX/Sarracenia begins

Decision to base Next Gen. +WMO data pump on AMQP.

2015 to present +(variety of clients)

datamart clients have used +clients provided and/or built +their own.

2015 Sarracenia in 10 Minutes +(to give own analysts big +picture )

Maps out vision for Sarracenia

2015 NWS WMO socket replaced

NWS offers only SFTP tree. +Tree consumption via Sarracenia +poll on broker distributes to +with 40 tranfer processes +on eight transfer nodes.

2015 PanAmerican Games

Fed Ninjo over internet +via Sarracenia subscription.

2016 Ninjo deployment

Central office feeds all +ninjo servers over WAN +use of caching/proxies reduces +WAN traffic after deployment

2016 Weather Apps.

Consistent, National failover +for BULLPREP, Scribe, etc… +(key forecaster applications)

+

implement a shared drive +to provide common view of +application state across 9 +offices

+

2016 Redundant RADAR Acq.

C-band radars uplink to two +locations, first-come, +first-serve for inputs to URP.

2016-2017 HPC Mirroring.

+

Gen 1: GPFS Policy

+

mirror between to HPC clusters

+

12x faster than rsync +(5 to 40 minutes lag)

+

2018 US FAA radar feed. +( trial in progress )

FAA use sarracenia package to +subscribe to Canadian RADAR +volume scans (C and S Band)

2017-2019 HPC Mirroring.

+

Gen 2: shim library

+

mirror between to HPC clusters

+

72x faster than rsync +(less than 5 minutes lag)

+
+

For more information: Deployments as of January 2018

+
+
+

Statelessness/Crawlable

+

As the file servers in question present static files, transactions with the +proferred stack are completely stateless. Search engines crawl +such trees easily, and, given critical mass, one could arrange with search +engines to provide them with a continuous feed of notifications so that a given +user’s index could be updated in real time. These characteristics require no +work or cost as they are inherent to the technologies proposed.

+
+
+

Programmability/Interoperability

+

A new application to process sr3_post messages can be re-implemented if there +is a desire to do so, as in addition to full documentation, source code +for a handful of implementations +(Python, C, Go, node.js), is readily publically available. +The python implementation has an extensive plugin interface available to +customize processing in a wide variety of ways, such as to add file +transfer protocols, and perform pre or post processing before sending +or after receipt of products. Interoperability with Apache NiFi has +been demonstrated by some clients, but they declined to release +the work.

+
+
+

Priorities

+

FIXME: Make a picture, with separate queues for separate data types?

+

In WMO GTS, data is segregated into alphanumeric vs. binary data, and within +a single flow, a priority mechanism was available, whose implementation was not +really specified. The goal is essentially for the most time critical data +to be transferrred before other queued information. When too much data +is sent over a high priority channel, some implementations can end up +starving the lower priority data, which is not always desirable.

+

The effect of priority is to establish separate queue for products at +each priority level. In this proposal, rather than having explicit priorities +within a single queue, one just uses separate queues for different +data sets. As high priority data must be smaller or infrequent than +other data in order to transferred and processed quickly, the queueing +on these high priority queues will naturally be shorter than those containing +other data. Since the mechanism is general, the details of implementation +do not require rigid standardization, but can be implemented by each +NMC to fit their needs.

+

In practice, Canadian deployments achieve sub-second warning forwarding +using only separate queues for high priority data types, such as warnings +and RADAR.

+
+

Inline Content in Messages

+

It is tempting to inline (or include) data within the AMQP messages +for small data types. The hope is that we avoid a connection initiation +and an extra round-trip. The typical example would be weather warnings. +Can we improve timeliness by including weather warnings in the AMQP data +flow rather than downloading them separately?

+

Whenever messaging brokers are benchmarked, the benchmarks always include +notes about message size, and the performance of the systems in terms +of messages per second are invariably higher with shorter messages. It +is fairly obvious that every system imposes a maximum message size, +that messages are normally kept in memory, and that the maximum message +size each peer would need to support would need to be specified in +order to assure interoperability. It isn’t clear that while individual +messages could benefit from inlining, that there isn’t a cost in overall +data pump performance that outweighs it.

+

With the above in mind, there are three possible approaches to +limiting message size: truncation, segmentation, and thresholds.

+

Truncation: the current WMO limits messages to being less than 500,000 +bytes. This prevents many modern data types from being transferred +(radar volume scans, satellite imagery, video, etc…) People +will suggest only warnings would be sent inline. The current +format for warning messages is Common Alerting Protocol, a highly +flexible XML format which permits things like embedding media. +There is no maximum message size for CAP messages, and so one +could not guarantee that all CAP messages would fit into any +truncation limit we would impose.

+

Segmentation: To avoid truncation one could instead implement +sending of products segmented into multiple messages. There is a +long, troubled, history of message segmentation in the GTS, +to the extent that segmentation was purged from GTS when +the message size limit was raised to 500,000 bytes. +Protocols like FTP, HTTP, TCP already do this work. Adding +another layer of software that replicates what is done at +lower levels is unlikely to be helpful. There is likely +very little appetite to define message segmentation to be +overlaid on AMQP message passing.

+

Note: The Sarracenia protocol implements file segmentation +(partitioning) over the data transfer protocols, with a +view to using it a far larger segment sizes, on the order +of 50 megabytes per segment. The purpose is to overlap file +transfer and processing (allowing the beginning of multi-gigabyte +files to begin before it is completely delivered.)

+

Threshold: It is likely that thresholding is the only reasonable +data inlining strategy. If the datum is larger than X bytes, use +another transport mechanism. This guarantees that only +data smaller than X bytes will be inlined. It provides +a message size for all brokers to optimize for. On +the other hand, it means that one must always implement +two transfer methods, since one cannot guarantee that +all data will fit into the AMQP stream, one must provision +for the alternate data path to be used when the threshold +is exceeded.

+

Picking X isn´t obvious. Data types are growing, with +future or current formats like: AvXML, CAP, ODIM, GIF +being an order of magnitude or more larger than traditional +alphanumeric codes (TAC.) Picking an X sufficient +for such data types is likely to be much harder on +the brokers, and no value we can pick will take all warnings.

+

As going forward, the intent is to use this method +with satellite imagery, RADAR data, and large GRIB data +sets, it is suspected that a great deal of high priority +data will exceed any reasonable value of X. If we don’t +use separate queues for high priority data, then a +downward pressure on X comes from avoiding large +messages from overly delaying a higher priority +message from being sent.

+

To guarantee warning transfer performance, one would need +to guarantee it for the large warnings as well, which is +accomplished quite well using separate queues alone.

+

It isn´t clear that the value of X we pick for today +wil make sense in ten years. A higher X +will use more memory in the brokers, and will +reduce absolute message passing performance. The brokers +are the most critical elements of these data pumps, +and minimizing complexity there is a benefit.

+

Another consideration is how much time is saved. The Sarracenia +application maintain connections, so it does not cost a +connection establishment to transfer a file. One typically +operates a number of parallel downloaders sharing a queue +to achieve parallelism. With the Canadian acquisition +of data from NWS, there are 40 processes pulling data +simultaneously, and there is very little queueing. It may +be more important to initiate transfers more quickly +rather than to accellerate individual streams.

+

A final consideration is the separation of control and data paths. +The AMQP end point might not be the data transfer end point. +In Canadian high performance deployments, there are brokers which are separate +servers from the data movers. The broker’s only purpose is to distribute +load among the data mover nodes, where the ideal is for that distribution +to be as even as possible. In that design, It makes little sense to +download messages to the brokers, and may actually delay forwarding +by adding a hop (a further transfer to a data mover node before +forwarding.) The Canadian main data pump deployments transfer several +hundred messages per second, and we are not sanguine about adding +payloads into that mix.

+

In summary: Without inlining, current deployments already achieve +sub-second forwarding using separate queues alone. If we wish to +avoid re-introducing segmentation and reassembly, inlining is +likely only practical with a fixed maximum payload size. Determining +a reasonable threshold is not obvious, and once the threshold is +established, one must ensure that high priority traffic above +the threshold also transfers quickly, obviating the motivation +for inlining. High performance deployments often feature brokers +completely separate from the data transfer path, where the broker has +a load distribution function, and simpler data transfer nodes +do the transport work. A threshold adds complexity in the +application, adds load on the broker, which is the most +complex element to scale, and so may make the overall system +slower. It isn´t clear that the benefits will be worthwhile +compared to the overhead cost in real world loads.

+
+
+
+

Caveat: Solution for This Problem, Not Every Problem

+

AMQP brokers work well, with the sarracenia implementations at the Canadian +meteorological service, they are used for tens of millions of file transfers +for a total of 30 terabytes per day. Adoption is still limited as it is more +complicated to understand and use than say, rsync. There are additional +concepts (brokers, exchanges, queues) that are a technical barrier to +entry.

+

Also, while brokers work well for the moderate volumes in use (hundreds of +message per second per server) it is completely unclear if this is suitable +as a wider Internet technology (ie. for the 10K problem). For now, this sort +of feed is intended for dozens or hundreds of sophisticated peers with a +demonstrated need for real-time file services. Demonstrating scaling to +internet scale deployment is future work.

+

There are many other robust solutions for the file transfer problem. AMQP +is best used only to transfer notifications (real-time transfer metadata), which +can be very large in number but small in volume, and not the data itself.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/messages_v01.html b/Explanation/History/messages_v01.html new file mode 100644 index 000000000..88782b111 --- /dev/null +++ b/Explanation/History/messages_v01.html @@ -0,0 +1,377 @@ + + + + + + + Message v01 Format — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Message v01 Format

+

Status: Approved-Draft1-20150805

+

Description of the message protocol / format.

+

This file documents final conclusions/proposals, reasoning/debates goes elsewhere.

+

Messages posted include a ´topic´ and a ´body.´

+

The message topic breaks down as follows:

+
<version>.<type>.[varies by version].<dir>.<dir>.<dir>...
+
+<version>:
+        exp -- initial version, deprecated (not covered in this document)
+        v00 -- used for NURP & PAN-AM in 2013-2014. (not covered in this document)
+        v01 -- 2015 version.
+
+<type>:
+        adm  - change settings
+                ´admin´, ´config´, etc...
+
+        log  - report status of operations.
+
+        notify - ´post´ but in exp and v00 versions. (not covered here.)
+
+        post - announce or notify that a new product block is available.
+                possible strings: post,ann(ounce), not(ify)
+
+<source>:
+
+
+

Rest of this document assumes version 1 (v01 topic):

+

topic: <version>.<type>.<src>(.<dir>.)*.<filename> +content: 1st line: +<date stamp> <blocksize in bytes> <filesize in blocks> <block#> <remainder> <flags> <md5sum> <flowid> <srcpath> <relpath>

+

breaks down to:

+
<date stamp>: date
+      YYYYMMDDHHMMSS.<decimal>
+
+<blocksize in bytes>: bsz
+      the number of bytes in a block.
+      checksums are calculated per block, so one post
+
+<filesize in blocks>: fzb
+      the integer total number of blocks in the file
+      FIXME: (including the last block or not?)
+      if set to 1.
+
+<block#>: bno
+      0 origin, the block number covered by this posting.
+
+<remainder>: brem
+      normally 0, on the last block, it remaining blocks in the file
+      to transfer.
+
+      -- if (fzb=1 and brem=0)
+             then bsz=fsz in bytes in bytes.
+             -- entire files replaced.
+             -- this is the same as rsync's --whole-file mode.
+
+<flags>:a comma-separated list of option letters, some with arguments after ´=´.
+
+      checksum setting contained in ´flags´ field, but is not the whole
+      thing.  Other letters/digits could be there to designate other things.
+      ´=´ acts as a separator of flags from arguments.
+
+      results in ´flags´ entry:
+
+      0 - no checksums (unconditional copy.)
+      d - checksum the entire data
+      n - checksum the file name
+      c=<script> - checksum with a script, named <script>
+
+              <script> should be ´registered´ in the switch network.
+                      registered means that all downstream subscribers
+                      can obtain the script to validate the checksum.
+                      there needs to be a retrieval mechanism.
+
+      other possible flag values:
+
+              u - unlinked... for files that have been removed? 'r'?
+
+      File Segment strategy:
+              i - inplace (do not create temporary files, just lseek
+                      within file.)
+                  may result in .ddsig file being created?
+              p - part files.  use .part files,  suffix fixed.
+                  do not know which will be default.
+         - file segment strategy can be overridden by client. just a suggestion.
+         - analogous to rsync options: --inplace, --partial,
+
+<flowid>
+      an arbitrary tag used for tracking of data through the network.
+
+The two paths are subtly inter-related.  Neither can be interpreted on their own.
+One must consider both path components.
+------
+
+      what if there are spaces in the file name?
+      it is url-encoded, so a space should turn into: %20
+
+------
+
+
+<srcpath> -- the base URL used to retrieve the data.
+
+      options: Complete URL:
+
+      sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif
+
+      in the case where the URL does not end with a path separator ('/'),
+      the src path is taken to be the complete source of the file to retrieve.
+
+
+      Static URL:
+
+      sftp://afsiext@cmcdataserver/
+
+      If the URL ends with a path separator ('/'), then the src URL is
+      considered a prefix for the variable part of the retrieval URL.
+
+
+<relpath> -- The relative path from the current directory in which to
+      place the file.
+
+      Two cases based on the end being a path separator or not.
+
+      case 1: NURP/GIF/
+
+      based on the current working directory of the downloading client,
+      create a subdirectory called URP, and within that, a subdirectory
+      called GIF will be created.  The file name will be taken from the
+      srcpath.
+
+      if the srcpath ends in pathsep, then the relpath here will be
+      concatenated to the srcpath, forming the complete retrieval URL.
+
+      case 2: NRP/GIF/mine.gif
+
+      if the  srcpath ends in pathsep, then the relpath will be concatenated
+      to srcpath for form the complete retrieval URL.
+
+      if the src path does not end in pathsep, then the src URL is taken
+      as complete, and the file is renamed on download according to the
+      specification (in this case, mine.gif)
+
+
+
+
FIXME: verify the following:

fsz = Size of a file in bytes = ( bsz * (fsb-1) ) + brem ?

+
+
+

example 1:

+
v01.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif
+201506011357.345 457 1 0 0 d <md5sum> exp13 sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif NRDPS/GIF/
+
+      v01 - version of protocol
+      post - indicates the type of message
+
+      version and type together determine format of following topics and the message body.
+
+      ec_cmc - the account used to issue the post (unique in a network).
+
+        -- blocksize is 457  (== file size)
+        -- block count is 1
+        -- remainder is 0.
+        -- block number is 0.
+        -- d - checksum was calculated on the body.
+        -- flow is an argument after the relative path.
+        -- complete source URL specified (does not end in '/')
+        -- relative path specified for
+
+      pull from:
+              sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif
+
+      complete relative download path:
+              NRDPS/GIF/NRDPS_HiRes_000.gif
+
+              -- takes file name from srcpath.
+              -- may be modified by validation process.
+
+
+

example 2:

+
v01.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif
+201506011357.345 457 1 0 0 d <md5sum> exp13 http://afsiext@cmcdataserver/data/  NRDPS/GIF/NRDPS_HiRes_000.gif
+
+in this case, the
+      pull from:
+              http://afsiext@cmcdataserver/data/NRPDS/GIF/NRDPS_HiRes_000.gif
+
+              -- srcpath ends in '/', so concatenated, takes file from relative URL.
+              -- true 'mirror'
+
+
+      complete relative download path:
+              NRDPS/GIF/NRDPS_HiRes_000.gif
+
+              -- may be modified by validation process.
+
+
+
+

Log messages

+

Log message contains:

+

is only emitted after processing is completed, to indicate a final status.

+

topic matches notification message message except…

+

v01.log.<source>.<consumer>……

+

version is protocol version, should increment in sync with notify.

+

start is as per post… just add fields after:

+
+

<date> blksz blckcnt remainder blocknum flags <flow> baseurl relativeurl <status> <host> <client> <duration>

+
+
+
+

CFG messages

+

just a place holder.

+

really not baked yet. thinking is in configuration.txt

+

v01.cfg

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/messages_v02.html b/Explanation/History/messages_v02.html new file mode 100644 index 000000000..885902023 --- /dev/null +++ b/Explanation/History/messages_v02.html @@ -0,0 +1,348 @@ + + + + + + + Description of the message v02 protocol / format — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Approved-Draft2-20150825

+
+

Description of the message v02 protocol / format

+

This file was used during the design phase, but post-implementation, +it is replaced by the sr_post(7) manual page.

+

This file documents final conclusions/proposals, reasoning/debates +goes elsewhere.

+

Messages posted include four parts: +topic: <version>.<type>.<src>(.<dir>.)*.<filename> +headers: series of key-value pairs, as per AMQP spec. +1st line (whitespace separated fields): <date stamp> <srcURL> <relURL><newline> +rest of body:

+

The message topic breaks down as follows:

+
+

<version>.<type>.[varies by version].<dir>.<dir>.<dir>…

+
+
<version>:

exp – initial version, deprecated (not covered in this document) +v00 – used for NURP & PAN-AM in 2013-2014. (not covered in this document) +v01 – 2015 version. +v02 – 2015 switched to AMQP headers for non-mandatory components

+
+
<type>:
+
adm - change settings

´admin´, ´config´, etc…

+
+
+

log - report status of operations.

+

notify - ´post´ but in exp and v00 versions. (not covered here.)

+
+
post - announce or notify that a new product block is available.

possible strings: post,ann(ounce), not(ify)

+
+
+
+
+

<source>:

+
+

Rest of this document assumes version 2 (v02 topic):

+

breaks down to:

+
+
<date stamp>: date

YYYYMMDDHHMMSS.<decimal>

+
+
+

<srcURL> – the base URL used to retrieve the data.

+
+

options: Complete URL:

+

sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif

+

in the case where the URL does not end with a path separator (‘/’), +the src path is taken to be the complete source of the file to retrieve.

+

Static URL:

+

sftp://afsiext@cmcdataserver/

+

If the URL ends with a path separator (‘/’), then the src URL is +considered a prefix for the variable part of the retrieval URL.

+
+
+
<relURL> – The relative path from the current directory in which to

place the file.

+

Two cases based on the end being a path separator or not.

+

case 1: NURP/GIF/

+

based on the current working directory of the downloading client, +create a subdirectory called URP, and within that, a subdirectory +called GIF will be created. The file name will be taken from the +srcpath.

+

if the srcpath ends in pathsep, then the relpath here will be +concatenated to the srcpath, forming the complete retrieval URL.

+

case 2: NRP/GIF/mine.gif

+

if the srcpath ends in pathsep, then the relpath will be concatenated +to srcpath for form the complete retrieval URL.

+

if the src path does not end in pathsep, then the src URL is taken +as complete, and the file is renamed on download according to the +specification (in this case, mine.gif)

+
+
+

AMQP provides HEADERS which are key/value pairs.

+

Describe what part of the URL is being announced:

+
+
parts=1,sz

– fetch in a single part, of the given size in bytes

+
+
parts=<i|p>,<bsz>,<fzb>,<bno>,<remainder>

– multipart fetch.

+

– File Segment strategy:

+
i - inplace (do not create temporary files, just lseek within file.)
+    may result in .srsig file being created?
+p - part files.  use .part files,  suffix fixed.
+    do not know which will be default.
+
+
+

– file segment strategy can be overridden by client. just a suggestion. +– analogous to rsync options: –inplace, –partial,

+
+
<blocksize in bytes>: bsz

the number of bytes in a block. +checksums are calculated per block, so one post

+
+
<filesize in blocks>: fzb

the integer total number of blocks in the file +FIXME: (including the last block or not?) +if set to 1.

+
+
<block#>: bno

0 origin, the block number covered by this posting.

+
+
<remainder>: brem

normally 0, on the last block, it remaining blocks in the file +to transfer.

+
+
– if (fzb=1 and brem=0)

then bsz=fsz in bytes in bytes. +– entire files replaced. +– this is the same as rsync’s –whole-file mode.

+
+
+
+
+

sum=<algorithm>,<value>

+
+

<algorithm>

+

d - checksum the entire data +n - checksum the file name +<script> - checksum with a script, named <script>

+
+
+
<script> should be ´registered´ in the switch network.

registered means that all downstream subscribers +can obtain the script to validate the checksum. +there needs to be a retrieval mechanism.

+
+
+
+

<value> is the checksum value

+
+
+
flow=<flowid>

an arbitrary tag used for tracking of data through the network.

+
+
+

The two paths are subtly inter-related. Neither can be interpreted on their own. One must consider both path components.

+
+
FIXME: verify the following:

fsz = Size of a file in bytes = ( bsz * (fsb-1) ) + brem ?

+
+
+

example 1:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif +HEADERS: +parts=1,457 +rename=NRDPS/GIF/ +sum=d,<md5sum> +flow=exp13

+
+

v01 - version of protocol +post - indicates the type of message

+

version and type together determine format of following topics and the message body.

+

ec_cmc - the account used to issue the post (unique in a network).

+
+

– file size is 457 (== file size) +– d - checksum was calculated on the body. +– flow is called ´exp13´ by the poster… +– complete source URL specified (does not end in ‘/’) +– relative path specified for

+
+
+
pull from:

sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif

+
+
complete relative download path:

NRDPS/GIF/NRDPS_HiRes_000.gif

+

– takes file name from srcpath. +– may be modified by validation process.

+
+
+
+

example 2:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 http://afsiext@cmcdataserver/data/ +HEADERS: +rename=NRDPS/GIF/NRDPS_HiRes_000.gif +parts=1,457 +sum=d,<md5sum> +flow=exp13

+
+
in this case, the
+
pull from:

http://afsiext@cmcdataserver/data/NRPDS/GIF/NRDPS_HiRes_000.gif

+

– srcpath ends in ‘/’, so concatenated, takes file from relative URL. +– true ‘mirror’

+
+
complete relative download path:

NRDPS/GIF/NRDPS_HiRes_000.gif

+

– may be modified by validation process.

+
+
+
+
+

example 3:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 http://afsiext@cmcdataserver/data/ +HEADERS: +rename=NRDPS/GIF/NRDPS_HiRes_000.gif +parts=i,457,0,0,1,0 +sum=d,<md5sum> +flow=exp13

+

wait case.

+

wait=on/off

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/messages_v03.html b/Explanation/History/messages_v03.html new file mode 100644 index 000000000..f0c0b8848 --- /dev/null +++ b/Explanation/History/messages_v03.html @@ -0,0 +1,214 @@ + + + + + + + Changes Made to create v03 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Changes Made to create v03

+

Actual format is defined here +An explanation of motivation of the changes is below:

+
+

Changes from v02

+

Version 03 is a change in encoding, but the semantics of the fields +are unchanged from version 02. Changes are limited to how the fields +are placed in the messages. In v02, AMQP headers were used to store name-value +pairs.

+
+
    +
  • v03 headers have practically unlimited length. In v02, individual +name-value pairs are limited to 255 characters. This has proven +limiting in practice. In v03, the limit is not defined by the JSON +standard, but by specific parser implementations. The limits in common +parsers are high enough not to cause practical concerns.

  • +
  • use of message payload to store headers makes it possible to consider +other messaging protocols, such as MQTT 3.1.1, in future.

  • +
  • In v03, pure JSON payload simplifies implementations, reduces documentation +required, and amount of parsing to implement. Using a commonly implemented +format permits use of existing optimized parsers.

  • +
  • In v03, JSON encoding of the entire payload reduces the features required for +a protocol to forward Sarracenia posts. For example, one might +consider using Sarracenia with MQTT v3.11 brokers which are more +standardized and therefore more easily interoperable than AMQP.

  • +
  • v02 fixed fields are now “pubTime”, “baseURL”, and “relPath” keys +in the JSON object that is the messge body.

  • +
  • v02 sum header with hex encoded value, is replaced by v03 identity header with base64 encoding.

  • +
  • v03 content header allows file content embedding.

  • +
  • Change in overhead… approximately +75 bytes per message (varies.)

    +
      +
    • JSON object marking curly braces ‘{’ ‘}’, commas and quotes for +three fixed fields. net: +10

    • +
    • AMQP section Application Properties no longer included in payload, saving +a 3 byte header (replaced by 2 bytes of open and close braces payload.) +net: -1 byte

    • +
    • each field has a one byte header to indicate the table entry in an AMQP +packet, versus 4 quote characters, a colon, a space, and likely a comma: 7 total. +so net change is +6 characters. per header. Most v02 messages have 6 headers, +net: +36 bytes

    • +
    • the fixed fields are now named: pubTime, baseUrl, relPath, adding 10 characters +each. +30 bytes.

    • +
    +
  • +
  • In v03, the format of save files is the same as message payload. +In v02 it was a json tuple that included a topic field, the body, and the headers.

  • +
  • In v03, the report format is a post message with a header, rather than +being parsed differently. So this single spec applies to both.

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/sr3_Announcement.html b/Explanation/History/sr3_Announcement.html new file mode 100644 index 000000000..f8541545e --- /dev/null +++ b/Explanation/History/sr3_Announcement.html @@ -0,0 +1,181 @@ + + + + + + + Announcing Sr3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Announcing Sr3

+

After two years of development, on 2022/04/11, we are pleased to announce the availability +of the first beta version of Sarracenia version 3: Sr3. To celebrate the release, +there is a new web-site with in depth information:

+
+
+

Compared to v2, Sr3 brings:

+
    +
  • Native support for mqtt and amqp (rabbitmq and MQTT brokers.) with a modular implementation that allows straightforward additional message queueing protocols to be supported.

  • +
  • The Flow Algorithm unifies +all components into slight variations of this single common code. This re-factor has enabled the elimination of code duplication and allowed reduction of total lines of code by approximately 30% while adding features.

  • +
  • A new command-line interface centred on a single entry-point: sr3

  • +
  • Improved, jupyter Notebook-driven Tutorials

  • +
  • A new plugin API, which allows pythonic customization of default application processing.

  • +
  • A new python API, which gives complete access to the implementation, allowing elegant extension through sub-classing.

  • +
  • Applications can call Sarracenia Python API from their mainline. +(In v2, one had to write callbacks to call application code, the application mainline could not be used.)

  • +
  • Newly added GitHub Discussion module, for questions, and community-driven +clarification: https://github.com/MetPX/sarracenia/discussions

  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/History/talks/SarraIntro/README.html b/Explanation/History/talks/SarraIntro/README.html new file mode 100644 index 000000000..44e766cda --- /dev/null +++ b/Explanation/History/talks/SarraIntro/README.html @@ -0,0 +1,161 @@ + + + + + + + <no title> — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

This is the raw materials to produce a sort of slide show for use +in a presentation.

+

One uses:

+
make
+
+
+

to create the slideshow, which is a series of jpg images exported from +a single Dia diagram. The diagram has many layers, and which images +are present in a given slide is controlled by script.txt.

+

The script.txt has Layers directives that are used to drive the Makefile +to do the jpg exports. The output of the Make will be script.rst +which can +HUGE CAVEAT… BEFORE YOU TRY EDITING THE IMAGE IN DIA…

+

dia stores absolute paths for images that are included in the file, +so if someone git clones your repo, the .dia file won’t work for them.

+

As soon as you make any save of the file in dia, before you commit the work +for others to use, you need to do something like:

+
cp A2B.dia A2B.gz
+gunzip A2B.gz
+
+
+

The file A2B is now XML, which can be (painfully) edited. +In the editor, you need to remove all the references to the directory +leaving only reltive paths for include:

+
vi A2B
+# :%s+`cwd`++
+# :wq
+gzip A2B
+mv A2B.gz A2B.dia
+
+
+

This replaces the old .dia file, might be upsetting…

+

Then we run awk in the Makefile to produce a .rst file for sphinx.

+

then when sphinx runs, it puts the paths back in

+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Overview.html b/Explanation/Overview.html new file mode 100644 index 000000000..b3fb04045 --- /dev/null +++ b/Explanation/Overview.html @@ -0,0 +1,229 @@ + + + + + + + Overview — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Overview

+

MetPX-Sarracenia is a publication/subscription management toolkit for publication of real-time data.

+

Sarracenia adds a message queueing protocol layer of file availability notifications to file and web servers to drive workflows that transfer and transform data continuously in a real-time and mission-critical context.

+

A main goal of the toolkit is to link together processes so that they avoid having to poll (repeatedly query, list, and then filter) servers or directories. Sarracenia can also be used to implement an initial upstream poll, which is still win because tasks beyond initial identification of the file to process can be driven by notifications, which are substantially cheaper (in i/o and processing) than polling even local directories.

+

This management layer provides simple methods to get parallellism in file transfers, robustness in the face of failures, load balancing among steps in a workflow, and takes care of many failure modes, so application developers do not need to. Hundreds of such flows can be composed together into large data pumps and operated using common methods familiar to Linux System Administrators.

+

Design Video from 2015: Sarracenia in 10 Minutes Video

+
+

Longer Overview

+

MetPX-Sarracenia is a configuration file and command line driven service to download files as they are made available. One can subscribe to a Sarracenia enabled web server (called a data pump) and select data to stream from it, using linux, Mac, or Windows. More than that:

+
    +
  • It avoids people having to poll the web server to know if their data is there yet (can be 100x less work for client and server from just this).

  • +
  • It is faster at downloading by using true pub/sub. It receives notifications exactly when the file is ready.

  • +
  • It is naturally parallel: when one process is not enough, just add more. They will share the same selections seamlessly.

  • +
  • One can daisy-chain multiple data pumps together, so that people can maintain independent real-time copied trees for service redundancy and also for network topology reasons (to serve private networks, for example).

  • +
  • Multiple copies means de-coupled availability. One server being down does not affect the entire web of interlocking APIS. Data pumps form meshes, where the data is transferred so that each one can have a copy if they want it. It’s a very simple way to achieve that.

  • +
  • It can also push trees (using a sender instead of a subscriber) which is great for transfers across network demarcations (firewalls).

  • +
  • Using only configuration, files can be renamed on the fly and the directory structure can be changed completely.

  • +
  • With the extensive plugin API, can transform the tree or the files in the tree. The output tree can bear no resemblance to the input tree.

  • +
  • The plugin API can be used to implement efficient data-driven workflows, reducing or eliminating polling of directories and scheduled tasks that impose heavy loads and increase transfer latency.

  • +
  • Multi-step workflows are naturally implemented with it as an adjunct of connecting producers with consumers. Transformation is a consumer within the data pump, +while external consumers access end products. Queues between components provides co-ordination of entire workflows.

  • +
  • You can set up a poll to make any web site act like a Sarracenia Data pump. So the workflow can work even without a Sarracenia pump to start with.

  • +
  • Sarracenia is robust. It operates 24x7 and makes extensive provision to be a civilised participant in mission critical data flows:

    +
      +
    • When a server is down, it uses exponential backoff to avoid punishing it.

    • +
    • When a transfer fails, it is placed in a retry queue. Other transfers continue and the failed transfer is retried later as real-time flows permit.

    • +
    • Reliability is tunable for many use cases.

    • +
    +
  • +
  • Since Sarracenia takes care of transient failures and queueing, your application just deals with normal cases.

  • +
  • It uses message queueing protocols (currently AMQP and/or MQTT) to send file notification messages, and file transfers can be done over SFTP, HTTP, or any other web service.

  • +
  • It does not depend on any proprietary technologies at all. Completely free to use for any purpose whatever.

  • +
  • Is a sample implementation following the World Meteorological Organizations work to replace the Global Teleceommunications System (GTS) with modern solutions.

  • +
+

At its heart, Sarracenia exposes a tree of web accessible folders (WAF), using any standard HTTP server (tested with apache) or SFTP server, with other types of servers as a pluggable option. Weather applications are soft real-time, where data should be delivered as quickly as possible to the next hop, and minutes, perhaps seconds, count. The standard web push technologies, ATOM, RSS, etc… are actually polling technologies that when used in low latency applications consume a great deal of bandwidth and overhead. For exactly these reasons, those standards stipulate a minimum polling interval of five minutes. Advanced Message Queueing Protocol (AMQP) messaging brings true push to notifications, and makes real-time sending far more efficient.

+../_images/sr3_flow_example.svg +

Sources of data announce their products, pumping systems pull the data using HTTP or SFTP onto their WAF trees, and then announce their trees for downstream clients. When clients download data, they may write a report message back to the server. Servers are configured to forward those client report messages back through the intervening servers to the source. The Source can see the entire path that the data took to get to each client. With traditional data switching applications, sources only see that they delivered to the first hop in a chain. Beyond that first hop, routing is opaque, and tracing the path of data required assistance from administrators of each intervening system. With Sarracenia’s report forwarding, the switching network is relatively transparent to the sources. Diagnostics are vastly simplified.

+

For large files / high performance, files are segmented on ingest if they are sufficiently large to make this worthwhile. Each file can traverse the data pumping network independently, and reassembly is only needed at end points. A file of sufficient size will announce the availability of several segments for transfer, multiple threads or transfer nodes will pick up segments and transfer them. The more segments available, the higher the parallelism of the transfer. In many cases, Sarracenia manages parallelism and network usage without explicit user intervention. As intervening pumps do not store and forward entire files, the maximum file size which can traverse the network is maximized.

+
    +
  • NOTE: For v03, segmentation functionality was removed temporarily. Planned to return in version 3.1.

  • +
+
+
+

Implementations

+

Part of Sarracenia defines an application layer message over AMQP as a transport. Sarracenia has multiple implementations:

+ +

More implementations are welcome.

+
+

Why Not Just Use Rsync?

+

There are a number of tree replication tools that are widely used, why invent another? RSync, for example is a fabulous tool, and we +recommend it highly for many use cases. But there are times when Sarracenia can go 72 times faster than rsync: Case Study: HPC Mirroring Use Case

+

Rsync and other tools are comparison based (dealing with a single Source and Destination). Sarracenia, while it does not require or use multi-casting, is oriented towards delivery to multiple receivers, particularly when the source does not know who all the receivers are (pub/sub). Where rsync synchronization is typically done by walking a large tree, that means that the synchronization interval is inherently limited to the frequency at which you can do the file tree walks (in large trees, that can be a long time.) Each file tree walk reads the entire tree in order to generate signatures, so supporting larger numbers of clients causes +large overhead. Sarracenia avoids file tree walks by having writers calculate the checksums once, and signal their activity directly to readers by messages, reducing overhead by orders of magnitude. Lsyncd is a tool that leverages the INOTIFY features of Linux to achieve the same liveness, and it might be more suitable but it is obviously not portable. Doing this through the file system is thought to be cumbersome and less general than explicit middleware message passing, which also handles the logs in a straight-forward way.

+

One of the design goals of Sarracenia is to be end-to-end. Rsync is point-to-point, meaning it does not support the transitivity of transfers across multiple data pumps that is desired. On the other hand, the first use case for Sarracenia is the distribution of new files. Updates to files were not common initially. ZSync is much closer in spirit to this use case. Sarracenia now has a similar approach based on file partitions (or blocks), but with user selectable size (50M is a good choice), generally much larger than Zsync blocks (typically 4k), more amenable to acceleration. Using an notification message per checksummed block allows transfers to be accelerated more easily.

+

The use of the AMQP message bus enables use of flexible third party transfers, straight-forward system-wide monitoring and integration of other features such as security scanning within the flow.

+

Another consideration is that Sarracenia doesn’t actually implement any transport. It is completely agnostic to the actual protocol used to tranfer data. Once can post arbitrary protocol URLs, and add plugins to work with those arbitrary protocols, or substitute accelerated downloaders to deal with certain types of downloads. The built-in transfer drivers include binary accellerators and tunable criteria for using them.

+

Caveat file segmentation was dropped. FIXME

+
+
+

Why No FTP?

+

The transport protocols fully supported by Sarracenia are http(s) and SFTP (SSH File Transfer Protocol). In many cases, when public data is being exchanged, FTP is a lingua franca that is used. The main advantage being relatively simple programmatic access, but that advantage is obviated by the use of Sarracenia itself. Further, these days, with increased security concerns, and with cpu instructions for encryption and multiple cores something of a cpu glut, it no longer makes much sense not to encrypt traffic. Additionally, to support multi-streaming, Sarracenia makes use of byte-ranges, which are provided by SFTP and HTTP servers, but not FTP. So we cannot support file partitioning on FTP. So while FTP sort-of-works, it is not now, nor ever will be, fully supported.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/SarraPluginDev.html b/Explanation/SarraPluginDev.html new file mode 100644 index 000000000..749199709 --- /dev/null +++ b/Explanation/SarraPluginDev.html @@ -0,0 +1,1133 @@ + + + + + + + Sarracenia Programming Guide — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sarracenia Programming Guide

+
+

Working with Plugins

+
+

Revision Record

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+
+

Audience

+

Readers of this manual should be comfortable with light scripting in Python version 3. +While a great deal of v2 compatibility is included in Sarracenia version 3, wholesale +replacement of the programming interfaces is a big part of what is in Version 3. +If working with version 2, Programmers should refer to the version 2 programmer’s Guide, +as the two are very different.

+
+
+

Introduction

+

Sarracenia v3 includes a number of points where processing can be customized by +small snippets of user provided code, known as flowCallbacks. The flowCallbacks themselves +are expected to be concise, and an elementary knowledge of Python should suffice to +build new ones in a copy/paste manner, with many samples being available to read.

+

There are other ways to extend Sarracenia v3 by subclassing of:

+
    +
  • Sarracenia.transfer.Transfer to add more data transfer protocols

  • +
  • Sarracenia.identity.Identity to add more checksumming methods.

  • +
  • Sarracenia.moth.Moth to add support for more messaging protocols.

  • +
  • Sarracenia.flow.Flow to create new flows.

  • +
  • Sarracenia.flowcb.FlowCB to add custom callback routines to flows.

  • +
  • Sarracenia.flowcb.poll.Poll to customize poll flows.

  • +
  • Sarracenia.flowcb.scheduled.Scheduled to customize scheduled flows.

  • +
+

That will be discussed after callbacks are dealt with.

+
+
+

Introduction

+

A Sarracenia data pump is a web server with notifications for subscribers to +know, quickly, when new data has arrived. To find out what data is already +available on a pump, view the tree with a web browser. For simple immediate +needs, one can download data using the browser itself or through a standard tool +such as wget. The usual intent is for sr_subscribe to automatically download +the data wanted to a directory on a subscriber machine where other software +can process it.

+

Often, the purpose of automated downloading is to have other code ingest +the files and perform further processing. Rather than having a separate +process look at a file in a directory, one can insert customized +processing at various points in the flow.

+

Examples are available using the list command:

+
fractal% sr3 list fcb
+Provided plugins: ( /home/peter/Sarracenia/development/sarra )
+flowcb/gather/file.py            flowcb/gather/message.py         flowcb/line_log.py               flowcb/line_mode.py
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py          flowcb/filter/log.py             flowcb/nodupe.py
+flowcb/post/log.py               flowcb/post/message.py           flowcb/retry.py                  flowcb/v2wrapper.py
+fractal%
+fractal% fcbdir=/home/peter/Sarracenia/development/sarra
+
+
+
+

Worklists

+

The worklist data structure is a set of lists of notification messages. There are four:

+
+
    +
  • worklist.incoming – notification messages yet to be processed. (built by gather)

  • +
  • worklist.rejected – notification message which are not to be further processed. (usually by filtering.)

  • +
  • worklist.ok – notification messages which have been successfully processed. (usually by work.)

  • +
  • worklist.failed – notification messages for which processing was attempted, but it failed.

  • +
+
+

The worklist is passed to the after_accept and after_work plugins as detailed in the next section.

+
+
+

The Flow Algorithm

+

All of the components (post, subscribe, sarra, sender, shovel, watch, winnow) +share substantial code and differ only in default settings. The Flow +algorithm is:

+
    +
  • Gather a list of notification messages, from a file, or an upstream source of notification messages (a data pump.) +places new notification messages in _worklist.incoming_

  • +
  • Filter them with accept/reject clauses, rejected notification messages are moved to _worklist.rejected_ . +after_accept callbacks further manipulate the worklists after initial accept/reject filtering.

  • +
  • Work on the remaining incoming notification messages, by doing the download, send or other work that creates new files. +when work for a notification message succeeds, the notification message is moved to the _worklist.ok_ . +work work for a notification message fails, the notification message is moved to the _worklist.failed_ .

  • +
  • (optional) Post the work accomplished (notification messages on _worklist.ok_ ) for the next flow to consume.

  • +
+
+
+
+

Flow Callbacks

+

The many ways to extend functionality, the most common one being adding callbacks +to flow components. All of the Sarracenia components are implemented using +the sarra.flow class. There is a parent class sarra.flowcb to implement them. +The package’s plugins are shown in the first grouping of available ones. Many of them have arguments which +are documented by listing them. In a configuration file, one might have the line:

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

That line cause Sarracenia to look in the Python search path for a class like:

+
blacklab% cat sarra/flowcb/msg/log.py
+
+from sarracenia.flowcb import FlowCB
+import logging
+
+logger = logging.getLogger(__name__)
+
+class Log(FlowCB):
+  def after_accept(self, worklist):
+      for msg in worklist.incoming:
+          logger.info("received: %s " % msg)
+
+  def after_work(self, worklist):
+      for msg in worklist.ok:
+          logger.info("worked successfully: %s " % msg)
+
+
+

The module will print each notification message accepted, and each notification message after work on it +has finished (download has occurred, for example.) To modify the callback class, +copy it from the directory listed in the list fcb command to somewher in the +environment’s PYTHONPATH, and then modify it for the intended purpose.

+

One can also see which plugins are active in a configuration by looking at the notification messages on startup:

+
blacklab% sr3 foreground subscribe/clean_f90
+2018-01-08 01:21:34,763 [INFO] sr_subscribe clean_f90 start
+
+.
+.
+.
+
+2020-10-12 15:20:06,250 [INFO] sarra.flow run callbacks loaded: ['sarra.flowcb.retry.Retry', 'sarra.flowcb.msg.log.Log', 'file_noop.File_Noop', 'sarra.flowcb.v2wrapper.V2Wrapper', 'sarra.flowcb.gather.message.Message'] 2
+.
+.
+.
+blacklab%
+
+
+

Use of the flowCallbackPrepend option will have the the class loaded at the beginning of the list, rather than +at the end.

+
+
+

Settings

+

Often when writing extensions through subclassing, additional options need to be set. The +sarracenia.config class does command-line and configuration file based +option parsing. and has a routine that can be called from new code +to define additional settings, usually from the __init__ routine, which +in built-in classes and such as flowcb accept as an _options_ parameter +on their __init__() routines:

+
somewhere in the __init__(self, options):
+
+options.add_option('accel_wget_command', 'str', '/usr/bin/wget')
+
+
+def add_option(self, option, kind='list', default_value=None):
+
+"""
+     options can be declared in any plugin. There are various *kind* of options, where the declared type modifies the parsing.
+
+     'count'      integer count type.
+     'duration'   a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)
+                  modified by a unit suffix ( m-minute, h-hour, w-week )
+     'flag'       boolean (True/False) option.
+     'list'       a list of string values, each succeeding occurrence catenates to the total.
+                  all v2 plugin options are declared of type list.
+     'size'       integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.
+     'str'        an arbitrary string value, as will all of the above types, each succeeding occurrence overrides the previous one.
+
+"""
+
+
+

The example above defines an “accel_wget_command” option +as being of string type, with default value _/usr/bin/wget_ .

+

Other useful methods in the sarracenia.config.Config class:

+
    +
  • variableExpansion( value, Message=None) … to expand patterns such as ${YYYYMMDD-5m} in configuration files. +one may want to evaluate these expansions at different times in processing, depending on the purpose +of the user defined options.

  • +
+

full list here: https://metpx.github.io/sarracenia/Reference/code.html#sarracenia.config.Config

+
+

Hierarchical Settings

+

One can also create settings specifically for individual callback classes using the _set_ +command and by identifying the exact class to which the setting applies. For example, +sometimes turning the logLevel to debug can result in very large log files, and one would +like to only turn on debug output for select callback classes. That can be done via:

+
set sarracenia.flowcb.gather.file.File.logLevel debug
+
+
+

The _set_ command, can also be used to set options to be passed to any plugin.

+
+
+

Viewing all Settings

+

Use the _sr3_ _show_ command to view all active settings resulting from a configuration file:

+
fractal% sr3 show sarra/download_f20.conf
+
+Config of sarra/download_f20:
+_Config__admin=amqp://bunnymaster@localhost, _Config__broker=amqp://tfeed@localhost, _Config__post_broker=amqp://tfeed@localhost, accel_threshold=100.0,
+accept_unmatch=True, accept_unmatched=False, announce_list=['https://tracker1.com', 'https://tracker2.com', 'https://tracker3.com'], attempts=3,
+auto_delete=False, baseDir=None, batch=1, bind=True, bindings=[('v03', 'xsarra', '#')], bufsize=1048576, bytes_per_second=None, bytes_ps=0,
+cfg_run_dir='/home/peter/.cache/sr3/sarra/download_f20', chmod=0, chmod_dir=509, chmod_log=384, config='download_f20', currentDir=None, debug=False,
+declare=True, declared_exchanges=['xpublic', 'xcvan01'], declared_users="...rce', 'anonymous': 'subscriber', 'ender': 'source', 'eggmeister': 'subscriber'}",
+delete=False, directory='/home/peter/sarra_devdocroot', documentRoot=None, download=False, durable=True, exchange=['xflow_public'],
+expire=25200.0, feeder=amqp://tfeed@localhost, filename=None, fixed_headers={}, flatten='/', hostdir='fractal', hostname='fractal', housekeeping=60.0,
+imports=[], inflight=None, inline=False, inlineEncoding='guess', inlineByteMax=4096, instances=1,
+logFormat='%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', logLevel='info', log_reject=True, lr_backupCount=5, lr_interval=1,
+lr_when='midnight', masks="...nia/insects/flakey_broker', None, re.compile('.*'), True, True, 0, False, '/')]", message_count_max=0, message_rate_max=0,
+message_rate_min=0, message_strategy={'reset': True, 'stubborn': True, 'failure_duration': '5m'}, message_ttl=0, mirror=True, notify_only=False,
+overwrite=True, plugins=['sample.Sample', 'sarracenia.flowcb.log.Log'], post_baseDir='/home/peter/sarra_devdocroot', post_baseUrl='http://localhost:8001',
+post_documentRoot=None, post_exchange=['xflow_public'], post_exchanges=[], prefetch=1, preserve_mode=True, preserve_time=False, program_name='sarra',
+pstrip=False, queue_filename='/home/peter/.cache/sr3/sarra/download_f20/sarra.download_f20.tfeed.qname',
+queue_name='q_tfeed_sarra.download_f20.65966332.70396990', randid='52f9', realpathPost=False, report=False, report_daemons=False, reset=False,
+resolved_exchanges=['xflow_public'], resolved_qname='q_tfeed_sarra.download_f20.65966332.70396990', settings={}, sleep=0.1, statehost=False, strip=0,
+subtopic=None, suppress_duplicates=0, suppress_duplicates_basis='path', timeout=300, tlsRigour='normal', topicPrefix='v03',
+undeclared=['announce_list'], users=False, v2plugin_options=[], v2plugins={}, vhost='/', vip=None
+
+fractal%
+
+
+
+
+
+

Logging Control

+

The method of understanding sr3 flow activity is by examining its logs. +Logging can be very heavy in sr3, so there are many ways of fine tuning it.

+
+

logLevel

+

the normal logLevel one is used to in the built-in python Log classes. It has +levels: debug, info, warning, error, and critical, where level indicates +the lowest priority message to print. Default value is info.

+

Because a simple binary switch of the logLevel can result in huge logs, for +example when polling, where every time every line is polled could generate a log line. +The monitoring of MQP protocols can be similarly verbose, so by default neither +of these are actually put into debug mode by the global logLevel setting. +some classes do not honour the global setting, and ask for explicit +enabling:

+
+
+

set sarracenia.transfer.Transfer.logLevel debug

+

Can control the logLevel used in transfer classes, to set it lower or higher +than the rest of sr3.

+
+
+

set sarracenia.moth.amqp.AMQP.logLevel debug

+

Print out debug messages specific to the AMQP message queue (sarracenia.moth.amqp.AMQP class). +used only when debugging with the MQP itself, such as dealing with broker connectivity issues. +interop diagnostics & testing.

+
+
+

set sarracenia.moth.mqtt.MQTT.logLevel debug

+

Print out debug messages specific to the MQTT message queue (sarracenia.moth.mqtt.MQTT class). +used only when debugging with the MQP itself, such as dealing with broker connectivity issues. +interop diagnostics & testing.

+
+
+

logEvents

+

default: after_accept, after_work, on_housekeeping +available: after_accept, after_work, all, gather, on_housekeeping, on_start, on_stop, post

+

implemented by the sarracenia.flowcb.log.Log class, one can select which events generate log +messages. wildcard: all generates log messages for every event known to the Log class.

+
+
+

logMessageDump

+

implemented by sarracenia.flowcb.log, at each logging event, print out the current content +of the notification message being processed.

+
+
+

logReject

+

print out a log message for each notification message rejected (normally silently ignored.)

+
+
+

messageDebugDump

+

Implemented in moth sub-classes, prints out the bytes actually received or sent +for the MQP protocol in use.

+
+
+

Debugging in callbacks

+

Pythonic logging involves having distinct logging objects per file. So adding debugging levels +requires setting debug up in each class where you need it. To turn debugging on in callback, +for example one called convert.geps_untar, in the config file place:

+
convert.geps_untar.logLevel debug
+
+
+

and in addition, if that flow_callback does not have an __init__() entry point, one will +need to add it:

+
def __init__(self,options):
+    super().__init__(options,logger)
+
+
+

This will apply the log formatting and priority to the logger in the current file.

+
+
+
+

Extending Classes

+

One can add additional functionality to Sarracenia by creating subclassing.

+
    +
  • sarra.moth - Messages Organized into Topic Hierarchies. (existing ones: rabbitmq-amqp)

  • +
  • sarra.identity - checksum algorithms ( existing ones: md5, sha512, arbitrary, random )

  • +
  • sarra.transfer - additional transport protocols (https, ftp, sftp )

  • +
  • sarra.flow - creation of new components beyond the built-in ones. (post, sarra, shovel, etc…)

  • +
  • sarra.flowcb - customization of component flows using callbacks.

  • +
  • sarra.flowcb.poll - customization of poll callback for non-standard sources.

  • +
+

One would start with the one of the existing classes, copy it somewhere else in the python path, +and build your extension. These classes are added to Sarra using the import option +in the configuration files. the __init__ files in the source directories are the good +place to look for information about each class’s API.

+
+
+

The Simplest Flow_Callback

+
+
+

Sample Extensions

+

Below is a minimal flowCallback sample class, that would be in a sample.py +file placed in any directory in the PYTHONPATH:

+
import logging
+import sarracenia.flowcb
+
+# this logger declaration  must be after last import (or be used by imported module)
+logger = logging.getLogger(__name__)
+
+class Sample(sarracenia.flowcb.FlowCB):
+
+    def __init__(self, options):
+
+        super().__init__(options,logger)
+        # declare a module specific setting.
+        options.add_option('announce_list', list )
+
+    def on_start(self):
+
+        logger.info('announce_list: %s' % self.o.announce_list )
+
+
+

All it does is add a setting called ‘announce-list’ to the configuration +file grammar, and then print the value on start up.

+

In a configuration file one, would expect to see:

+
flowCallback sample.Sample
+
+announce_list https://tracker1.com
+announce_list https://tracker2.com
+announce_list https://tracker3.com
+
+
+

And on startup, the logger message would print:

+
021-02-21 08:27:16,301 [INFO] sample on_start announce_list: ['https://tracker1.com', 'https://tracker2.com', 'https://tracker3.com']
+
+
+

Developers can add additional Transfer protocols for notification messages or +data transport using the import directive to make the new class +available:

+
import torr
+
+
+

would be a reasonable name for a Transfer protocol to retrieve +resources with bittorrent protocol. import can also be used +to import arbitrary python modules for use by callbacks.

+
+
+

Fields in Messages

+

callbacks receive the parsed sarracenia.options as a parameter. +self is the notification message being processed. variables variables most used:

+
+
msg[‘exchange’]

The exchange through which the notification message is being posted or consumed.

+
+
msg[‘isRetry’]

If this is a subsequent attempt to send or download a notification message.

+
+
msg[‘new_dir’]

The directory which will contain msg[‘new_file’]

+
+
msg[‘new_file’]

A popular variable in on_file and on_part plugins is: msg[‘new_file, +giving the file name the downloaded product has been written to. When the +same variable is modified in an after_accept plugin, it changes the name of +the file to be downloaded. Similarly another often used variable is +parent.new_dir, which operates on the directory to which the file +will be downloaded.

+
+
msg[‘new_inflight_file’]

in download and send callbacks this field will be set with the temporary name +of a file used while the transfer is in progress. Once the transfer is complete, +the file should be renamed to what is in msg[‘new_file’].

+
+
msg[‘pubTime’]

The time the notification message was originally inserted into the network (first field of a notice.)

+
+
msg[‘baseUrl’]

The root URL of the publication tree from which relative paths are constructed.

+
+
msg[‘relPath’]

The relative path from the baseURL of the file. +concatenating the two gives the complete URL.

+
+
msg[‘fileOp’]

for non data download file operations, such as creation of symbolic links, file renames and removals. +content described in sr_post(7)

+
+
msg[‘identity’]

The checksum structure, a python dictionary with ‘method’ and ‘value’ fields.

+
+
msg[‘subtopic’], msg[‘new_subtopic’]

list of strings (with the topic prefix stripped off) +do not use, as it will be generated from msg[‘new_relPath’] when the message is published.

+
+
msg[‘_deleteOnPost’]

when state needs to be stored in messages, one can declare additional temporary fields +for use only within the running process. To mark them for deletion when forwarding, +this set valued field is used:

+
msg['my_new_field'] = my_temporary_state
+msg['_deleteOnPost'] |= set(['my_new_field'])
+
+
+

For example, all of the new_ fields are in the _deleteOnPost by default.

+
+
msg[‘onfly_checksum’], msg[‘data_checksum’]

the value of an Identity checksum field calculated as data is downloaded. +In the case where data is modified while downloading, the onfly_checksum +is to verify that the upstream data was correctly received, while the +data_checksum is calculated for downstream consumers.

+
+
+

These are the notification message fields which are most often of interest, but many other +can be viewed by the following in a configuration:

+
logMessageDump True
+callback log
+
+
+

Which ensures the log flowcb class is active, and turns on the setting +to print rawish notification messages during processing.

+
+
+

Accessing Options

+

The settings resulting from parsing the configuration files are also readily available. +Plugins can define their own options by calling:

+
FIXME: api incomplete.
+Config.add_option( option='name_of_option', kind, default_value  )
+
+
+

Options so declared just become instance variables in the options passed to init. +By convention, plugins set self.o to contain the options passed at init time, so that +all the built-in options are similarly processing. If consult the sr_subscribe(1) +manual page, and most of the options will have a corresponing instance variable.

+

Some examples:

+
+
self.o.baseDir

the base directory for where files are when consuming a post.

+
+
self.o.suppress_duplicates

Numerical value indicating the caching lifetime (how old entries should be before they age out.) +Value of 0 indicates caching is disabled.

+
+
self.o.inflight

The current setting of inflight (see Delivery Completion

+
+
self.o.overwrite

setting which controls whether to files already downloaded should be overwritten unconditionally.

+
+
self.o.discard

Whether files should be removed after they are downloaded.

+
+
+
+
+

Flow Callback Points

+

Sarracenia will interpret the names of functions as indicating times in processing when +a given routine should be called.

+

View the FlowCB source +for detailed information about call signatures and return values, etc…

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Name

When/Why it is Called

ack

acknowledge notification messages from a broker.

after_accept +(self,worklist)

very freqently used.

+

can just modify messages in worklist.incoming. +adding a field, or changing a value.

+

Move messages among lists of messages in worklist. +to reject a message, it is moved from +worklist.incoming -> worklist.rejected. +(will be acknowledged and discarded.)

+

To indicate a message has been processed, move +worklist.incoming -> worklist.ok +(will be acknowledged and discarded.)

+

To indicate failure to process, move: +worklist.incoming -> worklist.failed +(will go on retry queue for later.)

+

Examples: msg_* in the examples directory

+

msg_delay - make sure messages are old before +processing them.

+

msg_download - change messages to use different +downloaders based on file size (built-in for small +ones, binary downloaders for large files.)

+

after_work +(self,worklist)

called after When a transfer has been attempted.

+

All messages are acknowledged by this point. +worklist.ok contains successful transfers +worklist.failed contains failed transfers +worklist.rejected contains transfers rejected +during transfer.

+

usually about doing something with the file after +download has completed.

+

destfn(self,msg):

called when renaming the file from inflight to +permanent name.

+

return the new name for the downloaded/sent file.

+

download(self,msg)

replace built-in downloader return true on success +takes message as argument.

gather(self)

gather messages from a source, returns a list of +messages. +can also return a tuple where the first element +is a boolean flag keep_going indicating whether +to stop gather processing.

on_housekeeping +(self)

Called every housekeeping interval (minutes) +used to clean cache, check for occasional issues. +manage retry queues.

+

return False to abort further processing +return True to proceed

+

on_start(self)

when a componente (e.g. sr_subscribe) is started. +Can be used to read state from files.

+

state files in self.o.user_cache_dir

+

return value ignored

+

example: file_total_save.py [1]

+

on_stop(self)

when a component (e.g. sr_subscribe) is stopped. +can be used to persist state.

+

state files in self.o.user_cache_dir

+

return value ignored

+

poll(self)

replace the built-in poll method. +return a list of notification messages.

post(self,worklist)

replace the built-in post routine.

send(self,msg)

replace the built-in send routine.

+
+

DESTFNSCRIPTS

+

As a compatibility layer with the ancestor MetPX Sundew, Sarracenia implements +Destination File Naming Scripts, where the one can create a flowcallback +class with a destfn entry point, and then use that to set the name of +the file that will be downloaded.

+

In the configuration file, one can use the filename option like so:

+
filename DESTFNSCRIPT=destfn.sample
+
+
+

To identify a class containing the destfn entry point to be applied. +using the filename directive applies it to all files. One can also +do it selectively in the configuration file’s accept clause:

+
accept k.* DESTFNSCRIPT=destfn.sample
+
+
+

which has it call the routine to rename only selected files (starting with k +as per the accept clause)

+

The destfn routine takes the notification message as an argument and should return +the new file name as a string.

+
+
+

Callbacks that need Python Modules

+

Some callbacks need to use other python modules. While normal imports +are fine, one can integrate them better for sr3 users by supporting +the features mechism:

+
from sarracenia.featuredetection import features
+#
+# Support for features inventory mechanism.
+#
+features['clamd'] = { 'modules_needed': [ 'pyclamd' ], 'Needed': True,
+        'lament' : 'cannot use clamd to av scan files transferred',
+        'rejoice' : 'can use clamd to av scan files transferred' }
+
+try:
+    import pyclamd
+    features['clamd']['present'] = True
+except:
+    features['clamd']['present'] = False
+
+
+

This lets users know which features are available in their installetion +so when they run sr3 features it provides an easily understood list of missing +libraries:

+
fractal% sr3 features
+2023-08-07 13:18:09,219 1993037 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'dcpflow', 'log', 'post.message', 'clamav']
+2023-08-07 13:18:09,224 1993037 [INFO] dcpflow __init__ really I mean hi
+2023-08-07 13:18:09,224 1993037 [WARNING] sarracenia.config add_option multiple declarations of lrgs_download_redundancy=['Yes', 'on'] choosing last one: on
+2023-08-07 13:18:09,225 1993037 [INFO] dcpflow __init__  lrgs_download_redundancy is True
+2023-08-07 13:18:09,225 1993037 [INFO] sarracenia.flowcb.log __init__ flow initialized with: {'post', 'on_housekeeping', 'after_work', 'after_accept', 'after_post'}
+2023-08-07 13:18:09,226 1993037 [CRITICAL] sarracenia.flow loadCallbacks flowCallback plugin clamav did not load: 'pyclamd'
+
+Status:    feature:   python imports:      Description:
+Installed  amqp       amqp                 can connect to rabbitmq brokers
+Installed  appdirs    appdirs              place configuration and state files appropriately for platform (windows/mac/linux)
+Installed  filetypes  magic                able to set content headers
+Installed  ftppoll    dateparser,pytz      able to poll with ftp
+Installed  humanize   humanize             humans numbers that are easier to read.
+Absent     mqtt       paho.mqtt.client     cannot connect to mqtt brokers
+Installed  redis      redis,redis_lock     can use redis implementations of retry and nodupe
+Installed  sftp       paramiko             can use sftp or ssh based services
+Installed  vip        netifaces            able to use the vip option for high availability clustering
+Installed  watch      watchdog             watch directories
+Installed  xattr      xattr                on linux, will store file metadata in extended attributes
+MISSING    clamd      pyclamd              cannot use clamd to av scan files transferred
+
+ state dir: /home/peter/.cache/sr3
+ config dir: /home/peter/.config/sr3
+
+fractal%
+
+
+

You can see that that clamd feature is disabled because the pyclamd python library is not installed.

+
+
+

Flow Callback Poll Customization

+

A built-in subclass of flowcb, sarracenia.flowcb.poll.Poll implements the bulk +of sr3 polling. There are many times different types resources to poll, and +so many options to customize it are needed. Customization is accomplished +via sub-classing, so the top of such an callback looks like:

+
...
+from sarracenia.flowcb.poll import Poll
+....
+
+class Nasa_mls_nrt(Poll):
+
+
+

Rather than implementing a flowcb class, one subclasses the +flowcb.poll.Poll class. Here are the common poll +subclass specific entry points usually implemented in sub-classes:

+ + + + + + + + + +

handle_data

in sr_poll if you only want to change how the +downloaded html URL is parsed, override this

+

action: +parse parent.entries to make self.entries

+

Examples: html_page* in the examples directory

+

on_line

in sr_poll if sites have different remote formats +called to parse each line in parent.entries.

+

Work on parent.line

+

return False to abort further processing +return True to proceed

+

Examples: line_* in the examples directory

+
+

Examination of the built-in flowcb Poll +class is helpful

+ +
+
+
+
+

Better File Reception

+

For example, rather than using the file system, sr_subscribe could indicate when each file is ready +by writing to a named pipe:

+
blacklab% sr_subscribe edit dd_swob.conf
+
+broker amqps://anonymous@dd.weather.gc.ca
+subtopic observations.swob-ml.#
+
+flowcb sarracenia.flowcb.work.rxpipe.RxPipe
+rxpipe_name /tmp/dd_swob.pipe
+
+directory /tmp/dd_swob
+mirror True
+accept .*
+
+# rxpipe is a builtin on_file script which writes the name of the file received to
+# a pipe named '.rxpipe' in the current working directory.
+
+
+

With the flowcb option, one can specify a processing option such as rxpipe. With rxpipe, +every time a file transfer has completed and is ready for post-processing, its name is written +to the linux pipe (named .rxpipe) in the current working directory. So the code for post-processing +becomes:

+
do_something <.rxpipe
+
+
+

No filtering out of working files by the user is required, and ingestion of partial files is +completely avoided.

+
+

Note

+

In the case where a large number of sr_subscribe instances are working +on the same configuration, there is slight probability that notifications +may corrupt one another in the named pipe. +We should probably verify whether this probability is negligeable or not.

+
+
+

Advanced File Reception

+

The after_work entry point in a sarracenia.flowcb class is an action to perform +after receipt of a file (or after sending, in a sender.) The RxPipe module is an example +provided with sarracenia:

+
import logging
+import os
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+class RxPipe(FlowCB):
+
+    def __init__(self,options):
+
+        super().__init__(options,logger)
+        self.o.add_option( option='rxpipe_name', kind='str' )
+
+    def on_start(self):
+        if not hasattr(self.o,'rxpipe_name') and self.o.file_rxpipe_name:
+            logger.error("Missing rxpipe_name parameter")
+            return
+        self.rxpipe = open( self.o.rxpipe_name, "w" )
+
+    def after_work(self, worklist):
+
+        for msg in worklist.ok:
+            self.rxpipe.write( msg['new_dir'] + os.sep + msg['new_file'] + '\n' )
+        self.rxpipe.flush()
+        return None
+
+
+

With this fragment of Python, when sr_subscribe is first called, it ensures that +a pipe named npipe is opened in the specified directory by executing +the __init__ function within the declared RxPipe python class. Then, whenever +a file reception is completed, the assignment of self.on_file ensures that +the rx.on_file function is called.

+

The rxpipe.on_file function just writes the name of the file downloaded to +the named pipe. The use of the named pipe renders data reception asynchronous +from data processing. As shown in the previous example, one can then +start a single task do_something which processes the list of files fed +as standard input to it, from a named pipe.

+

In the examples above, file reception and processing are kept entirely separate. If there +is a problem with processing, the file reception directories will fill up, potentially +growing to an unwieldy size and causing many practical difficulties. When a plugin such +as on_file is used, the processing of each file downloaded is run before proceeding +to the next file.

+

If the code in the on_file script is changed to do actual processing work, then +rather than being independent, the processing could provide back pressure to the +data delivery mechanism. If the processing gets stuck, then the sr_subscriber +will stop downloading, and the queue will be on the server, rather than creating +a huge local directory on the client. Different models apply in different +situations.

+

An additional point is that if the processing of files is invoked +in each instance, providing very easy parallel processing built +into sr_subscribe.

+
+

Using Credentials in Plugins

+

To implement support of additional protocols, one often needs credentials +value in the script with the code :

+
    +
  • ok, details = self.o.credentials.get(msg.urlcred)

  • +
  • if details : url = details.url

  • +
+

The details options are element of the details class (hardcoded):

+
    +
  • print(details.ssh_keyfile)

  • +
  • print(details.passive)

  • +
  • print(details.binary)

  • +
  • print(details.tls)

  • +
  • print(details.prot_p)

  • +
+

For the credential that defines protocol for download (upload), +the connection, once opened, is kept open. It is reset +(closed and reopened) only when the number of downloads (uploads) +reaches the number given by the batch option (default 100).

+

All download (upload) operations use a buffer. The size, in bytes, +of the buffer used is given by the bufsize option (default 8192).

+
+
+

Why v3 API should be used whenever possible

+
    +
  • uses importlib from python, much more standard way to register plugins. +now syntax errors will be picked up just like any other python module being imported, +with a reasonable error message.

  • +
  • no strange decoration at end of plugins (self.plugin = , etc… just plain python.) +Entirely standard python modules, just with known methods/functions

  • +
  • The strange choice of parent as a place for storing settings is puzzling to people. +parent instance variable becomes options, self.parent becomes self.o

  • +
  • plural event callbacks replace singular ones. after_accept replaces on_message

  • +
  • notification messages are just python dictionaries. fields defined by json.loads( v03 payload format ) +notification messages only contain the actual fields, no settings or other things… +plain data.

  • +
  • what used to be called plugins, are now only a type of plugins, called flowCallbacks. +They now move notification messages between worklists.

  • +
+

With this API, dealing with different numbers of input and output files becomes much +more natural, when unpacking a tar file, notification messages for the unpacked files can be appended +to the ok list, so they will be posted when the flow arrives there. +Similarly a large number of small files may be bucketed together to make one +large file. so rather than transferring all the incoming files to the list, +only the resulting tar bucket will be placed in ok.

+

The import mechanism described below provides a straightforward means +of extending Sarracenia by creating children of the main classes

+
    +
  • moth (messages organized in topic hierarchies) for dealing with new message protocols.

  • +
  • transfer … for adding new protocols for file transfers.

  • +
  • flow .. new components with different flow from the built-in ones.

  • +
+

In v2, there was no equivalent extension mechanism, and adding protocols +would have required re-working of core code in a custom way for every addition.

+
+
+
+
+

File Notification Without Downloading

+

If the data pump exists in a large shared environment, such as +a Supercomputing Centre with a site file system, +the file might be available without downloading. So just +obtaining the file notification and transforming it into a +local file is sufficient:

+
blacklab% sr_subscribe edit dd_swob.conf
+
+broker amqps://anonymous@dd.weather.gc.ca
+subtopic observations.swob-ml.#
+document_root /data/web/dd_root
+download off
+flowcb msg_2local.Msg2Local
+flowcb do_something.DoSomething
+
+accept .*
+
+
+

There should be two files in the PYTHONPATH somewhere containing +classes derived from FlowCB with after_accept routines declared. +The processing in those routines will be done on receipt of a batch +of notification messages. A notification message will correspond to a file.

+

the after_accept routins accept a worklist as an argument.

+
+

Warning

+

FIXME: perhaps show a way of checking the parts header to +with an if statement in order to act on only the first part notification message +for long files.

+
+
+

Extension Ideas

+

Examples of things that would be fun to do with plugins:

+
    +
  • Common Alerting Protocol (CAP), is an XML format that provides a warnings +for many types of events, indicating the area of coverage. There is a +‘polygon’ field in the warning, that the source could add to messages using +an on_post plugin. Subscribers would have access to the ‘polygon’ header +through use of an after_accept plugin, enabling them to determine whether the +alert affected an area of interest without downloading the entire warning.

  • +
  • A source that applies compression to products before posting, could add a +header such as ‘uncompressed_size’ and ‘uncompressed_sum’ to allow +subscribers with an after_accept plugin to compare a file that has been locally +uncompressed to an upstream file offered in compressed form.

  • +
  • add Bittorrent, S3, IPFS as transfer protocols (sub-classing Transfer)

  • +
  • add additional message protocols (sub-classing Moth)

  • +
  • additional checksums, subclassing Identity. For example, to get GOES DCP +data from sources such as USGS Sioux Falls, the reports have a trailer +that shows some antenna statistics from the reception site. So if one +receives GOES DCP from Wallops, for example, the trailer will be different +so checksumming the entire content will have different results for the +same report.

  • +
+
+
+
+

Polling

+

To implement a customized poll, declare it as a subclass of Poll +(sarracenia.flowcb.poll.Poll), and only the needed The routine (in this case +the html parsing “handle_data”) need be written to override the behaviour provided +by the parent class.

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/__init__.py )

+

The plugin has a main “parse” routine, which invokes the html.parser class, in which +the data_handler is called for each line, gradually building the self.entries +dictionary where each entry with an SFTPAttributes structure describing one file being polled.

+

So the work in handle_data is just to fill an paramiko.SFTPAttributes structure. +Since the web site doesn’t actually provide any metadata, it is just filled in with sensible +default info, that provides enough information to build a notification message and run it through +duplicate suppression.

+

Here it the complete poll callback:

+
import logging
+import paramiko
+import sarracenia
+from sarracenia import nowflt, timestr2flt
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+class Nasa_mls_nrt(Poll):
+
+    def handle_data(self, data):
+
+        st = paramiko.SFTPAttributes()
+        st.st_mtime = 0
+        st.st_mode = 0o775
+        st.filename = data
+
+        if 'MLS-Aura' in data:
+               logger.debug("data %s" %data)
+               self.entries[data]=st
+
+               logger.info("(%s) = %s" % (self.myfname,st))
+        if self.myfname == None : return
+        if self.myfname == data : return
+
+
+

The file is here:

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/nasa_mls_nrt.py )

+

and matching config file provided here:

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/examples/poll/nasa-mls-nrt.conf )

+
+
+

Accessing Messages from Python

+

So far, we have presented methods of writing customizations of Sarracenia +processing, where one writes extensions, via either callbacks or extension +classes to change what sarracenia flow instances do.

+

Some may not want to use the Sarracenia and configuration language at all. +They may have existing code, that they want call some sort of data ingesting code from. +One can call sarracenia related functions directly from existing python programs.

+

For now, best to consult the Tutorials included with Sarracenia, +which have some examples of such use.

+
+

Warning

+

FIXME, link to amqplib, or java bindings, and a pointer to the sr3_post and sr_report section 7 man pages.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Sundew_Migration/filter_conversion.html b/Explanation/Sundew_Migration/filter_conversion.html new file mode 100644 index 000000000..e20b934e6 --- /dev/null +++ b/Explanation/Sundew_Migration/filter_conversion.html @@ -0,0 +1,469 @@ + + + + + + + Sundew filter migration to sarracenia (PXATX) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sundew filter migration to sarracenia (PXATX)

+
+
Manual section:
+

1

+
+
Date:
+

@Date@

+
+
Version:
+

@Version@

+
+
Manual group:
+

MetPx Sarracenia Suite

+
+
+ +
+

DESCRIPTION

+

This document suppose that the reader is familiar with the concepts and usage +of sundew and sarracenia.

+

sundew filters supports a plugin mechanism that allows processing onto +received products in order to generate derivated products and insert them +in the sundew product flow. The pxFilter process reads its configuration +where an fx_script is declared, installed and used on all ingested products.

+

There are 2 types of such filters. The first type, most common, is a one to +one filter where one received product is converted into one filtered product. +An example is where a GIF image is converted into a PNG one. The second type +is where on received product generates a bunch of filtered products we will +call it a one to many filter. An example if the collected wmo bulletins we +get from UKMET. A filter split each file into several individual files. We +might have to do a third type for example if we collect a bunch of individual +files into one product all of this into a sarra plugin…

+

sarracenia supports a lot of possible plugins in all of its programs. +If we take the simpler form of filter one to one, and we want to translated +this into a sarra process we need to receive the posting of a local product, +generate a resulting product and post the notification for it. It is easy to +see that can be done using an sr-sarra process.

+
+
+

ONE TO ONE FILTER

+

I will present one way that I used to implement a one to one filter. +There could be other alternatives… but this one worked nicely for me.

+

Lets go through the steps of making a one to one filter plugin. +The sarra configuration will start similar to an sr_sender one +because both require the announced products to be local to the server. +So typically something like:

+
# broker is localhost
+
+broker amqp://feeder@localhost/
+exchange   xpublic
+
+# queueing local
+
+prefetch   10
+
+# the root of the tree implicit in baseUrl's.
+
+baseDir /apps/sarra/public_data
+
+# only the selected product
+acceptUnmatch False
+
+
+

Of course this is only an example. You can narrow down the products +with more precise subtopic and even different exchanges. And of course, +we want to post the newly created product hence our config will also +have something like:

+
# posting
+
+post_broker   amqp://feeder@localhost/
+post_exchange xpublic
+post_baseUrl http://${HOSTNAME}
+post_baseDir /apps/sarra/public_data
+
+
+

In between these two sections we need to set the plugin to convert the +products and also define where the products will be placed. I will +pretend that my filter converts images to PNG format images. The config +could look like:

+
# converting the products
+
+callback convert.png
+
+# example for directory and product selection
+
+directory ${PBD}/${YYYYMMDD}/SSC-DATAINTERCHANGE/CHARTS/PNG/${HH}
+
+subtopic  *.SSC-DATAINTERCHANGE.CHARTS.IMV6.#
+accept   .*/SSC-DATAINTERCHANGE/CHARTS/IMV6/.*
+
+
+

Now lets explain the converting part of this configuration. As you guest, the +plugin cvt_topng is where the image will be converted. Here is how it is +implemented. Our converting class needs to register itself as a replacement +for the http protocol. Why ? because all the local product will be announced +as http://hostname and we want to catch what should be an http download and +turn it into a converting process.:

+
from sarracenia.flowcb import FlowCB
+
+class Cvt_Topng(FlowCB):
+
+
+

Next, it is very important to give a new name to the converted product. +If you leave the target name as is, sarra will match the notice +with the local product and will skip this message as an already downloaded +product. The next function in our class will be:

+
def after_accept(self,worklist):
+
+    for msg in worklist.incoming:
+        fname = msg['new_file']
+        fname = fname.replace('.imv6','')
+
+        msg['new_file'] = fname + '.png'
+        msg.updatePaths(self.o)
+
+
+

Now this new_file is unavailable on the localhost, we can use a do_download +or a download function to proceed with our conversion. I have implemented +my one to one filters with a download and in our case it looks like:

+
import logging
+import subprocess
+
+logger = logging.getLogger( __name__ )
+
+def download(self, msg ) -> bool:
+
+    ipath = self.o.base_dir + '/' + msg['relPath']
+    opath = msg['new_dir'] + '/' + msg['new_file']
+
+    logger.info("converting %s to %s" % (os.path.basename(ipath),os.path.basename(opath)))
+
+    # here an example of command
+
+    cmd = 'topng ' + ipath + ' ' + opath
+
+    try :
+            outp = subprocess.check_output( cmd, shell=True )
+            return True
+    except:
+            logger.info('Exception details: ', exc_info=True)
+            logger.error("Unable to convert file %s" % ipath)
+
+    return False
+
+
+

There is more work left with the existance of the new product. Each one to one +filter needs to adjust the message that will be posted. Since this is a common +task to all one to one filters, I made it a plugin itself and it is called +on_file_converted. Basically it contains an on_file function for the +task:

+
# once the file converted, adjust message
+import os,stat
+import sarracenia
+
+def after_work(self, worklist ):
+
+    if self.o.component != 'sarra' : return
+
+    new_ok=[]
+    for msg in worklist.ok:
+        path    = msg['new_dir'] + '/' + msg['new_file']
+
+        lstat   = os.stat(path)
+
+        new_message = sarracenia.fromFileData( path, self.o, lstat )
+        new_ok.append(new_message)
+    worklist.ok=new_ok
+
+
+

It is nice to think that, should there be changes in the message, this plugin +could be modified without having to modify all one to one filters.

+
+
+

CONSIDERATIONS WITH ONE TO ONE FILTERS

+

I wrote some of the migrated filters and there are some considerations +to be taken while implementing filters from sundew.

+

I have tried to make the less use of the sundew-extension but when +required for some clients, a filter must change this inforemation too. +In our example, I also have this function:

+
def correct_extension(self,msg) :
+
+    if  not 'sundew_extension' in parent.msg.headers : return
+
+    ext   = msg['sundew_extension']
+    parts = ext.split(':')
+    ext    = ":".join(parts[:3]) + ':PNG'
+
+    msg['sundew_extension'] = ext
+
+
+

And in the code, it is called right after the conversion:

+
try :
+         outp = subprocess.check_output( cmd, shell=True )
+         self.correct_extension(msg)
+         return True
+...
+
+
+

It might also be required, depending on the products and the clients, +to add (or update) to the extension a datetime suffix for the new products.

+
+
+

FINAL REMARKS ON ONE TO ONE FILTER

+

Usually a converter, say topng, will add the extension .png to the end product. +This was not the case in sundew where the whatfn was kept as is but +part of the sundew_extension was modified to show the new format.

+

Examining on_file_converted you will find an after_accept function +that removes filter extensions from the filename. This was required because +old sundew clients needed to receive sarracenia converted products without +their specific extension name. When this is required, the on_file_converted +plugin can be added to the sender config. So example, a converted product +to PNG, in sarra would have a .png extension. Should it be required to send +it to a sundew client with option filename NONE without the plugin +the client would receive WHATFN.png:…:… with the plugin, it receives +the correct WHATFN:…:…

+

Note also that the on_file function of the on_file_converted plugin +is restricted to an sr_sarra process while the after_accept function +is restricted to an sr_sender process.

+

If part of this document needs to be clarified please let me know

+
+
+

ONE TO MANY FILTER

+

I will present one way that I have used to implement a one to many filter. +Most of what was said earlier in the one to one filter still holds. +The configuration of such an sarra process follows the same rules. +The plugin requires the same http registering. An after_accept function +needs to change the value of msg[‘new_file’] (the value may not be +relevant to the filename you will give to the extracted individual files.

+

Each file extracted will require an individual message to be posted. +Use a message constructor, as presented above (sarracnie.fromFileData()) +to build a new message, and then append that to the list of messages +being processed.

+

Many things could be considered in this function (parts?) but for the +general usage it should be ok. I used the after_work function to +do the extraction, and publishing as follow

+
def after_work(self, worklist ):
+
+    new_ok=[]
+    for msg in worklist.ok:
+        ipath  = self.o.base_dir + '/' + msg['relPath']
+
+        logger.info("splitting %s" % os.path.basename(ipath) )
+
+        # HERE IS A FUNCTION THAT EXTRACTS/GENERATES THE FILES
+        # AND RETURNS A LIST CONTAINING THE ABSOLUTE PATH FOR
+        # THE FILES GENERATED
+
+        opaths = self.FILE_PARSER(msg, ipath)
+
+        # if it did not work it is an error
+
+        if not opaths or len(opaths) <= 0 : return False
+
+        # publish all parsed files but last
+
+        for p in opaths :
+            new_msg = sarracenia.fromFileData(p, self.o, lstat(p) )
+            new_ok.append(new_msg)
+
+    # replace worklist.ok if you don't wont to republish the inputs.
+    worklist.ok = new_ok
+    # OR could append if you do...
+    # worklist.extend(new_ok)
+
+
+

From the template plugin, one should implement the extraction of the files. +Each file will get its uniq name. All generated product absolute filepath +are collected in the opaths python +list. This list is returned and the after_work function will take care +of publishing these new products. A snippet of code, just as a reference +is provided in the template

+
# file parsing here
+
+def FILE_PARSER(self, msg, ipath ):
+
+    opaths = []
+
+    # PARSE THE FILE HERE
+
+    # EACH GENERATED FILE SHOULD HAVE A DIFFERENT PATH
+    # THAT SHOULD LOOK LIKE
+
+    # opath  = msg['new_dir'] + '/' + new_extracted_filename
+
+    # EACH SUCCESSFULL PATH IS APPENDED TO THE LIST
+
+    # opaths.append(opath)
+
+    # RETURN THE LIST OF ALL GENERATED FILES
+
+    return opaths
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Sundew_Migration/index.html b/Explanation/Sundew_Migration/index.html new file mode 100644 index 000000000..760ac79d7 --- /dev/null +++ b/Explanation/Sundew_Migration/index.html @@ -0,0 +1,185 @@ + + + + + + + Sundew Migration Guide — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sundew Migration Guide

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Sundew_Migration/sundew_pull_migration.html b/Explanation/Sundew_Migration/sundew_pull_migration.html new file mode 100644 index 000000000..e89f6119b --- /dev/null +++ b/Explanation/Sundew_Migration/sundew_pull_migration.html @@ -0,0 +1,579 @@ + + + + + + + Sundew pull migration to sarracenia (PXATX) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sundew pull migration to sarracenia (PXATX)

+
+
Manual section:
+

1

+
+
Date:
+

@Date@

+
+
Version:
+

@Version@

+
+
Manual group:
+

MetPx Sarracenia Suite

+
+
+ +
+

DESCRIPTION

+

This document suppose that the reader is familiar with the concepts and usage +of sundew and sarracenia.

+

sundew receiver supports a pull mechanism that allow querying a remote +server for new products to be ingested in the sundew product flow. There are +two types namely pull-file and pull-bulletin. The difference between them +is the way in which the product is processed/routed once downloaded.

+

sarracenia supports the same mechanism. sr-poll announces the +new products from the remote site and sr-sarra makes the products +available, downloading and annoncing the product locally.

+

It is fairly straight forward to convert a pull reveiver configuration file +to both an sr-poll and sr-sarra that play the same role. There +is a new concept in sarracenia where the source of the product +needs to be specified in the path of the file tree.

+

Fortunately, in the context of this PXATX conversion, in our sundew system, +the products are placed properly in a sarracenia tree and announced with amqp +under a defined source directory. We will use these sources informations.

+

This document is solely based on the PXATX experience and so one should take +the ideas and apply them to his/her context.

+
+
+

METHODOLOGY

+

I did this document using a very simple sundew pull receiver to make +sure to put just the right amount of details.

+

First set up a conversion environment. Where sarracenia is downloaded, +make sure you can use the bash script pull_2_pollsarra.sh by updating +your PATH environment variable. Use git clone to get an updated version +of sarracenia ( See Dev ). Make sarracenia tools available +for direct shell commands:

+
export PATH=...wherever.../sarracenia/tools:$PATH
+
+
+

Define a place where you want to convert pxatx pull:

+
mkdir -p convert/pxatx
+cd convert/pxatx
+
+
+

Get all the sundew configurations of sundew pxatx:

+
scp -r px@pxatx1-ops:/apps/px/etc .
+
+
+

Here we pick the configuration pull-BC-ENV_AQ-WAMR.conf, and proceed +to its converstion:

+
cd etc/rx
+pull_2_pollsarra.sh pull-BC-ENV_AQ-WAMR.conf
+
+
+

The original file looks like this

+
#
+# STATUS:       Operational
+#
+# DESCRIPTION:  pull Wamr data from British Columbia Ministry of Environment (BC MoE)
+#
+# CONTACT:      BC MoE contact:  AQHIDSI@Victoria1.gov.bc.ca
+#
+#
+
+type pull-file
+
+routemask        true
+routing_version  1
+routingTable     /apps/px/etc/pdsRouting.conf
+
+protocol ftp
+host     fake.host.gc.ca
+user     fakeuser
+password fakepass
+
+delete False
+timeout_get 90
+pull_sleep  180
+
+extension pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII
+
+directory /pub/outgoing/WAMR/EarthNetworks2/
+get .*.lsi
+
+# generate key with accept
+accept .*(lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII).*
+
+
+

The script created these files:

+
ls credentials *BC-ENV_AQ-WAMR.conf
+credentials
+pull-BC-ENV_AQ-WAMR.conf
+poll_pull-BC-ENV_AQ-WAMR.conf
+sarra_get_pull-BC-ENV_AQ-WAMR.conf
+
+
+

My personal renaming convention is to rename the files

+
mv poll_pull-BC-ENV_AQ-WAMR.conf BC_ENV_AQ_WAMR.conf
+mv sarra_get_pull-BC-ENV_AQ-WAMR.conf get_BC_ENV_AQ_WAMR.conf
+
+
+

So now we have the sr_poll BC_ENV_AQ_WAMR.conf and +the sr_sarra get_BC_ENV_AQ_WAMR.conf.

+
+
+

SR_POLL CONFIG

+

The generated sr_poll config looks like this: +cat BC_ENV_AQ_WAMR.conf:

+
#
+# STATUS:       Operational
+#
+# DESCRIPTION:  pull Wamr data from British Columbia Ministry of Environment (BC MoE)
+#
+# CONTACT:      BC MoE contact:  AQHIDSI@Victoria1.gov.bc.ca
+#
+#
+
+# on doit avoir le vip de ddsr.cmc.ec.gc.ca
+
+vip 142.135.12.146
+
+# post_broker is DDSR spread the poll messages
+
+post_broker amqp://SOURCE@ddsr.cmc.ec.gc.ca/
+post_exchange xs_SOURCE
+
+# options
+
+sleep 180
+timeout 90
+
+# to useless... left for backward compat
+to DDSR.CMC,DDI.CMC,CMC,SCIENCE,EDM
+
+# where to get the products
+
+pollUrl ftp://fakeuser:fakepass@fakehost.gc.ca
+
+#where/how to get the products
+path /pub/outgoing/WAMR/EarthNetworks2/
+
+# generate key with accept
+accept .*(lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII).*
+
+# usually no accept... in sr_poll
+reject .*
+
+
+

The follows all the original option of the sundew pull as a reference. +To continue we need to know what product is ingested by that pull:

+
ssh px@pxatx1-ops grep Ingested /apps/px/log/rx_pull-BC-ENV_AQ-WAMR.log
+
+
+

We find that one of the product “today” is +29_05_2019_04_25.lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII +Lets try to find it on pxatx sarracenia side how it is announced:

+
ssh sarra@data-lb-ops1 'cd master/pxatx; srl grep 29_05_2019_04_25.lsi \*.log'
+
+
+

Just picking one of the notice leads us to this place

+
20190529/PROVINCIAL/BC-ENV_AQ-WAMR/12/29_05_2019_04_25.lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII
+
+
+

By convention the directory after the date is the name of the SOURCE +for these products. So here PROVINCIAL is used as an amqp source user +for announcement and as one of the top directory leaf for its products +With theses informations we can finalized the sr_poll config

+
vi BC_ENV_AQ_WAMR.conf
+
+change
+post_broker amqp://SOURCE@ddsr.cmc.ec.gc.ca
+post_exchange xs_SOURCE**
+
+for
+post_broker amqp://PROVINCIAL@ddsr.cmc.ec.gc.ca
+post_exchange xs_PROVINCIAL
+
+
+

The destination put by the script always contain all the credentials. +So we just edit to keep protocol://user#host:

+
change
+pollUrl ftp://fakeuser:fakepass@fake.host.gc.ca
+
+for
+pollUrl ftp://fakeuser@fake.host.gc.ca
+
+
+

Starting at comment # where to get the products +down to the end of the file, the script attempted to reproduce +the directory, get and accept/reject options as in the original. +And finally it placed all the options of the original file as reference. +Make sure the sr_poll config is reflecting the original sundew one +Get rid of duplicated options, scrutening the rest of the file. +It is not our case here but if there are reject options in this config +keep them. For accept option, you dont really need them since option +get plays the same role:

+
remove
+accept .*(lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:3:ASCII).*
+
+
+

After, change the get for accept. +So a cleaned version of the last lines of the sr_poll config would be:

+
# where to get the products
+
+pollUrl ftp://fakeuser@fake.host.gc.ca
+
+# product source directories
+
+path /pub/outgoing/WAMR/EarthNetworks2/
+accept .*\.lsi
+
+
+
+
+

SR_SARRA CONFIG

+

The generated sr_sarra config looks like this: +cat get_BC_ENV_AQ_WAMR.conf:

+
#
+# STATUS:       Operational
+#
+# DESCRIPTION:  pull Wamr data from British Columbia Ministry of Environment (BC MoE)
+#
+# CONTACT:      BC MoE contact:  AQHIDSI@Victoria1.gov.bc.ca
+#
+#
+
+# source
+
+instances 1
+
+# receives messages from same DDSR queue spreads the messages
+
+broker amqp://feeder@ddsr.cmc.ec.gc.ca/
+exchange   xs_SOURCE
+
+# listen to spread the poll messages
+
+prefetch  10
+queue_name q_feeder.${PROGRAM}.${CONFIG}.SHARED
+
+sourceFromExchange True
+
+# what to do with product
+
+mirror        False
+preserve_time False
+
+# MG CHECK DELETE
+#delete False
+delete False
+
+# directories
+
+directory ${PBD}/${YYYYMMDD}/${SOURCE}/--${0}-- to be determined ----
+accept    .*(something).*
+
+# destination
+
+post_broker   amqp://feeder@localhost/
+post_exchange xpublic
+post_baseUrl http://${HOSTNAME}
+post_baseDir /apps/sarra/public_data
+
+
+

Again we need to adjust to the SOURCE value which is PROVINCIAL:

+
vi get_BC_ENV_AQ_WAMR.conf
+
+change
+exchange   xs_SOURCE
+
+for
+exchange   xs_PROVINCIAL
+
+
+

A special attention must be given to the delete option. +If the sundew pull configuration is deleting the products once +downloaded, to test our sr_sarra process we must not delete +products. By default, the script writes

+
# MG CHECK DELETE
+#delete value
+delete False
+
+
+

Where value is the setting of the delete option in the sundew pull. +The sr_sarra configuration, when ready, can be tested without deletion. +When placed in operation, and the sundew pull withdrawn, if the delete +option should be true just delete the ‘delete False’ and uncomment the +‘delete True’.

+

To have the proper directory, accept settings (there might be more than +one), we want to search how the products are disposed on the sarracenia side. +Because it is sundew processes that mimic sarracenia we find theses informatios +in the sundew senders:

+
% grep PROVINCIAL/BC-ENV_AQ-WAMR ../tx/*
+% tx/ddsr-PROVINCIAL.inc:directory //apps/sarra/public_data/${RYYYY}${RMM}${RDD}/PROVINCIAL/BC-ENV_AQ-WAMR/${RHH}
+
+
+

And looking for the conplete configuration setting for these products in +this include file we get:

+
directory //apps/sarra/public_data/${RYYYY}${RMM}${RDD}/PROVINCIAL/BC-ENV_AQ-WAMR/${RHH}
+accept .*.lsi:pull-BC-ENV_AQ-WAMR:GOV-BC:WAMR:.*
+
+
+

The final changes in our sr_poll config is to reflect that finding:

+
change**
+directory \${PBD}/\${YYYYMMDD}/\${SOURCE}/--\${0}-- to be determined ----
+accept    .*(something).*
+
+for
+directory ${PBD}/${YYYYMMDD}/${SOURCE}/BC-ENV_AQ-WAMR/${HH}
+accept .*\.lsi.*
+
+
+

And we are all set for testing.

+
+
+

TESTING

+

We install sr_poll BC_ENV_AQ_WAM.conf and sr_sarra get_BC_ENV_AQ_WAM.conf +on DDSR_DEV. (on ddsr_dev, there are various things to modify. Setting xattr_disable true, changing ddsr.cmc for ddsr_dev.cmc, in broker… document_root option in senders and perhaps more)

+

Leave the processes running and check the right disposal/announcement of the products.

+
+ +
+

MIGRATING FILTERS

+

Will do another paper for sundew filters that become sr_sarra.

+
+
+

MIGRATING SENDER

+

Will do another paper on how to migrate senders.

+
+

SEE ALSO

+

sr_poll(1) - post announcemensts of specific files.

+

sr_sarra(8) - Subscribe, Acquire, and ReAdvertise tool.

+

https://github.com/MetPX/ - sr_subscribe is a component of MetPX-Sarracenia, the AMQP based data pump.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/Sundew_Migration/sundew_sender_migration.html b/Explanation/Sundew_Migration/sundew_sender_migration.html new file mode 100644 index 000000000..858a31352 --- /dev/null +++ b/Explanation/Sundew_Migration/sundew_sender_migration.html @@ -0,0 +1,451 @@ + + + + + + + Sundew sender migration to sarracenia (PXATX) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sundew sender migration to sarracenia (PXATX)

+
+
Manual section:
+

1

+
+
Date:
+

@Date@

+
+
Version:
+

@Version@

+
+
Manual group:
+

MetPx Sarracenia Suite

+
+
+ +
+

DESCRIPTION

+

This document was written right after my presentation of August 8th. +It will basically be a summary of what was said for the sundew sender +migration part of it.

+

The scripts used in the presentation use routing tables, sender +configuration, logs and a dump of sarra database of products.

+

I used a similar setup with similar tools on my desktop. All of this, +is to be taken as a suggestion or a startup. I have heavily used some +of it during the conversion of sundew pull to sarra poll-sarra pairs.

+

Most if the time for pxatx senders, I would only use pxsender_2_sarra.sh. +It converts sundew-sender to a sarra-sender in a dummy fashion… no extra. +From the converted config, I would simply add the migrated polled products +I had just migrated and drop the rest of the sundew sender config.

+

The sender tools where an entire day of products is presented as messages +to the sarra sender migrated I used 3 times… I simply coded that while +I tried to migrate sundew-sender as part of my migration project. +As porting sundew-sender to sarra showed to be a much greater task than +I expected, I simply drop the idea. And did not use these tools anymore.

+

Peter wanted me to present my work. The goal is to engage his troup +in the project of migrating the sundew-sender to sarra.

+

All of these facts must be considered when using the presented tools. +I am not saying THEY ARE the tools to use in order to properly conduct +I am not saying the are reliable enough to be used blindly. +For myself, they would certainly be my starting point, should I be part +of that project.

+

So you the reader are part of that project now I guess if you read this. +I propose the tools as a start for your own migration. Use them as +tips, basis, ideas, or tools… make of them what you want… and +good luck for your journey in this big sundew-sender conversion +project.

+
+
+

SETUP

+

Historically, after having poke the several clusters (sundew and sarra) +with tools on data-lb-ops1 (under users px and sarra)… it was so +annoying that I decided one day to get all the information available +on all the clusters and work from my desktop. I am convinced that it +strikes you as simpler than doing “srl grep” or “pxl grep” in all clusters. +So my tools would work on a local copy of the needed files.

+

In order to use the scripts provided as is, as a start, you would need +to install the same setup as I was using when developping/using them. +Of course, this is not mandatory, and should you prefer other setups, +you can do so and modify the scripts accordingly… or write you own…

+

So my setup was:

+
mkdir ~/convert
+
+# and in this directory you place
+
+~/convert
+│
+├── tools
+│   ├── compare.sh
+│   ├── do_this_pull.sh
+│   ├── do_this_sender.sh
+│   ├── pull_2_pollsarra.sh
+│   ├── pxsender_2_sarra.sh
+│   ├── sr_sender_one_day.sh
+│   └── sundew_routing_2_sarra_subtopic.py
+│
+├── plugins
+│   ├── msg_from_file.py
+│   └── pxSender_log.py
+│
+├── config
+│   │
+│   ├── pxatx
+│   │   └── etc
+│   │       ├── rx
+│   │       ├── fx
+│   │       ├── scripts
+│   │       ├── trx
+│   │       ├── tx
+│   │       └── ...
+│   │
+│   ├── sundew
+│   │   └── etc
+│   │       ├── rx
+│   │       ├── fx
+│   │       ├── scripts
+│   │       ├── trx
+│   │       ├── tx
+│   │       └── ...
+│   │
+│   └── sarra
+│           ├── cpost
+│           ├── plugins
+│           ├── poll
+│           ├── post
+│           ├── sarra
+│           ├── sender
+│           ├── shovel
+│           └── watch
+│
+├── log
+│   │
+│   ├── pxatx
+│   │   └── ...
+│   │
+│   ├── sr_pxatx
+│   │   └── ...
+│   │
+│   ├── ddsr (sarra)
+│   │   ├── px2-ops
+│   │   ├── px3-ops
+│   │   ├── px4-ops
+│   │   ├── px5-ops
+│   │   ├── px6-ops
+│   │   ├── px7-ops
+│   │   └── px8-ops
+│   │
+│   └── sundew
+│       ├── px2-ops
+│       ├── px3-ops
+│       ├── px4-ops
+│       ├── px5-ops
+│       ├── px6-ops
+│       ├── px7-ops
+│       └── px8-ops
+│
+│
+└── data
+    └── ddsr.20190804  (sarra /apps/sarra/public_data/20190804 *)
+
+
+

The files found in the tools directory can be taken from the +sarracenia depot on github under ~/sarracania/tools. (If not in the master +they would be found in branch issue199)

+

For files found in the plugins directory directory can be taken from the +sarracenia depot on github under ~/sarracania/sarra/plugins. (If not in the +master they would be found in branch issue199)

+

The config directory is just a straight copy of all the configs +for each of the clusters… and here sr_pxatx means the sarra portion +of pxatx.

+

For the logs and the data, one would think to have a whole day and so +I would always aim at getting all of “yesterday”.

+

So one can go on each node and scp “yesterday’s log” where the setup is +installed under the proper representing directory.

+

The creation of data file (ddsr.20190804) was done as follow:

+
ssh sarra@data-lb-ops1 '. ./.bash_profile; cd ~/master/saa; srl "cd /apps/sarra/public_data; find 20190804 -type f"' >> ddsr.20190804
+
+
+

On the server where you would to the migration, you need sarracenia of course. +The fact that px1-ops was off the sarra cluster was an opportunity since it +provides the same environment as the targetted cluster. If one such node is +not available when you a migration to a cluster (in fact I would be tempted +to say any migration of any kinds) … I recommand you to have such a node +available.

+
+
+

SUNDEW SENDER CONVERSION PROCESS

+

I cannot say for sure that all my tools get everything straight. +Should you find better ways or modifications to do, dont hesitate.

+

For now, should you use them out of the box, here is how I would +proceed with the them.

+

Under ~/convert, create your own working/migrating directory… ex.:

+
mkdir SENDERS
+cd    SENDERS
+
+
+

Select one config that you would like to start working with. +(Perhaps to start, the senders with the smallest number of delivery +would be a good start… dont do them all, keep some for the other +team member to sharp their teeth too).

+

To get ready, make sure that the plugins under ~/convert/plugins are +sarra-wise available:

+
cp ~/convert/plugins/* ~/.config/sarra/plugins
+
+
+

And perhaps adjust the path to be able to call the tools easily:

+
export PATH=.~/convert/tools:$PATH
+
+
+
+
NOTE::

PAS 2023/02/08 - PAS I could not locate these scripts. +hmm…

+
+
+

Ok now, convert that sender… Here I suppose as in the presentation +that it is accessdepot-iml.conf for simplicity (or remainder):

+
# convert the sender place infos in directory ACCESSDEPOT_IML
+# The script will show an estimated of time to finish
+# that can be hours depending on the routing tables and sender configs
+
+do_this_sender.sh accessdepot-iml
+
+# access the resulting directory and have a look at the info
+# gathered by the script
+
+cd ACCESSDEPOT_IML
+vi INFO_accessdepot-iml
+
+# make sure the credential were extracted, ready for sarra
+ls credentials
+cat credentials
+
+# go check/edit/modify the configs and includes
+cd sender
+vi accessdepot-iml.conf
+
+# You think your sarra config/includes for this sender is ok
+# give it a try, run a whole day
+# *** CATCH in script sr_sender_one_day.sh
+# *** it appends to your sender config lines like
+# *** msg_file /local/home/sarra/convert/data/ddsr.20190804
+# *** THIS IS DATA DEPENDANT AND NEEDS TO BE TAKEN INTO ACCOUNT
+
+sr_sender_one_day.sh sender/accessdepot-iml.conf
+
+# check it out if this sender is done...
+# It will stop when all products of the data file are processed
+
+tail -f ~/.cache/sarra/log/sr_accessdepot-iml_01.log
+
+# When done compare the logs of the sundew sender
+# the sender's log have to be of the same date as the data product file
+
+compare.sh accessdepot-iml
+
+# IF the compare says the exact same number of products
+# and there are no product to be rejected or missing
+# the sender is ready.
+
+# If not... (and that is probably in most cases)
+# If there are no missing product... only some to be
+# rejected, would try restricting your accept/reject
+# and  you would loop doing the following until resolution
+#
+# 1- Fix the sender again
+# 2- Run through a whole day again
+# 3- check when finished
+# 4- compare
+#
+# a looping sequence like this :
+
+vi sender/accessdepot-iml.conf
+sr_sender_one_day.sh sender/accessdepot-iml.conf
+tail -f ~/.cache/sarra/log/sr_sender_accessdepot-iml_01.log
+compare.sh accessdepot-iml
+
+# missing products are more problematic
+# needs further investigation and perhaps
+# the addition of processes, or products to sarra
+
+
+

This was done, as is, in today’s presentation. I cannot say it enough… +as I mentionned, I have not done many of these sundew senders conversions +by gaving it a day of products… The few I did were enough to leave +sundew-senders migration alone and focus on pxatx-sundew. I would certainly +start from there should I be you. But again, this is a personal choice… +Your ideas and methods being as good as mine.

+

When using tools, one can trigger them on say 5 sender configs and +have more work ahead. The experience gathered during these 5 migrations +can be ported into the tools hoping to get better results for the next 5 +and so on. So always have 5 in the oven. It is important to run batches +since some may take hours to be processed.

+

If no change is done to the sarra db layout and products, the files in +the setup are accurate enought to pursue the migration.

+

On the opposite, changes to the sarra db layout and product additions, +removals or whatever changes, requieres the files “sarra db for yesterday” +and “sundew logs for yesterday” to be updated… Or else, the results from +the tools will not represent the current state of things.

+

Have fun :-)

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/index.html b/Explanation/index.html new file mode 100644 index 000000000..b1b93dd3a --- /dev/null +++ b/Explanation/index.html @@ -0,0 +1,166 @@ + + + + + + + Explanation — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Explanation

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Explanation/sftps.html b/Explanation/sftps.html new file mode 100644 index 000000000..8415e68b1 --- /dev/null +++ b/Explanation/sftps.html @@ -0,0 +1,517 @@ + + + + + + + Why SFTP is More Often Chosen than FTPS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Why SFTP is More Often Chosen than FTPS

+

There has been some discussion around where to place an S in order to +secure file transfers. Among a variety of competing technologies, there +are two that may appear fairly equivalent: SFTP and FTPS. Both of these +technologies are candidates to replace the venerable FTP method +of transferring files.

+

It is pertinent to review the reasons why SFTP was chosen in place of FTPS. +In this discussion please keep in mind:

+
    +
  • I am not a security expert, this just reflects my current understanding

  • +
  • We have not deployed FTPS, therefore we have little experience with it.

  • +
+

If there are some misapprehensions, then we would be pleased to learn +about them.

+
+

FTP

+

In the early days of the internet, before even the advent +of the world-wide web, one of the earliest applications was +file transfer, as first defined by the Internet Engineering Task +Force standard Request for Comments (original IETF RFC 114, +dating from 1971 ). It evolved over the following decades with +the same basic ideas. On the internet, every device +has an internet address, and for every address there is +on the order of 65,000 ports available (kind of sub-addresses).

+

If Alice wants to communicate with Bob, she initiates +a connection to Bob’s device on port 21. Bob indicates he +is ready to receive the file, and then they agree on a second +connection that is established over which the file data actually +flows. How the second connection is established has varied a lot +over time. The information to set up the data path naturally +passes over the control path previously set up.

+

In those early days, Alice and Bob would have two servers +directly on the internet. The internet was a collegial place, and +firewalls did not exist. Fast-forward thirty years, and the reality is +quite different.

+
+
+

Product Exchangers: Supporting Large Numbers of Transfers

+

In modern times, if one is transferring hundreds of thousands or millions +of files, there will likely be a large number of network devices +between Alice and Bob’s servers. The following diagram is a typical +simple case one is likely to meet in reality:

+../_images/ftp_proxy_today.svg

To send large numbers of files and withstand failures of +individual servers, there will typically be an array of servers +to send files. Let’s call them a product exchange array. +The purpose of such arrays of servers is to process and/or +transfer more data than could be transferred in a given time +by a single server, and to provide transparent redundancy +for component failures.

+

To make it look to outsiders as a single server, there will +be a load balancer in front of the real servers. When initiating +connections or receiving inbound ones, the load balancer +will map the real servers’ actual addresses to a public facing +one. The load balancer maintains a table of who is connected +to whom, so that when a packet is received from an external +server, it can map it back to the correct real server. +This is called Network Address Translation, or NAT.

+

Recall that there are 65,000 ports available, so several servers +could choose, from their real addresses, to use the same source +port to initiate their connection. When the load balancer +maps the connection to an publically visible address, you could +have more than one server claiming the same source port. +So the load balancer has to map ports as well as just addresses. +This is called Port Address Translation, or PAT.

+

So there is a table stored in some fashion (on higher end devices +in content addressable memory, or CAM)

+ + + + + + +

10.2.3.4:67

134.67.7.8:45

+

Which maintains the mapping of a private address & port to a public +address & port for every connection traversing a given device.

+

When we get to a firewall, especially when organizations use +private addressing, the process is repeated. So the address +and port might get re-mapped to different ones again.

+

The same operations happen at the other end, with the firewall +and the load balancer maintaining tables of who is connected +to what, and mapping addresses and port numbers in order +to allow the servers at each end to communicate.

+

With a straight-forward protocol, like Secure Shell, or Hypertext +Transfer Protocol, there is only a single channel and only a single +table entry in each of these devices is necessary per connection. +The algorithmic complexity of such operations is a small +constant amount, small in terms of memory and cpu.

+

Recall that the control path indicates the method by which the data +connection is established. In order to have the FTP data channel connect the +two servers, each firewall or load balancer between the two servers +ideally observes the control path, looks for the data port specification, +and proactively creates a mapping for it, creating a second pair of relationships +to manage in the table for every connection. This activity is termed +FTP proxying. A proxy should be run at any point where network or port +translation occurs, with at least double the (still small) memory overhead +of other protocols, but the cpu work is about the same (just using +two entries in mapping tables instead of one). Optimally, some sort of +proxy or watcher on firewalls is able to adjust firewall rules +dynamically to permit only specific data connections in use through +when they need to, minimizing exposure to the internet to only double +that of other protocols.

+

In practice, there may be many NAT/PAT firewalls between A +and B, and neither Alice nor Bob will be in charge of the configuration +of all of the intervening devices. Because the FTP control path +is easily inspected, there is just a bit of proxy code commonly +implemented on network devices to cover this special case.

+
+
+

FTPS

+

FTPS - is FTP with Transport Level Security added to it. +The control path is now encrypted, and thus not available +to proxies on each of the firewalls and load balancers (such +as LVS (linux virtual server), or standard products from F5, barracuda or +a number of others). The straightforward answer to that problem is to +terminate the encryption on each firewall and load balancer, so that +the control traffic can be viewed to obtain the needed port numbers, +and then re-encrypted. Essentially one would need to decrypt and +re-encrypt the control information at least four times in between +the source and destination servers.

+

Ideally one would decrypt only the control path, the router would intercept +connection requests and return a local encryption response. But if the data path +is not also intercepted, then the server will have one encryption key for the control +path and another for the data path, which will break TLS. So most likely, +each intervening router and firewall is obligated to decrypt and re-encrypt +all the data going through as well.

+

Essentially, this means that Alice and Bob accept that every +NAT/PAT device between them is allowed to impersonate the other +entity (performing what is known as a man-in-the-middle attack). +This is the only way to traverse a long chain of NAT/PAT devices.

+

As the above is quite onerous, my supposition is that those deploying FTPS +do not do correct proxying as described above. Instead, they +reserve a port range for these types of traffic and put some static NAT/PAT +rules in place, likely reserving a port range per node behind a load balancer. +That is cumbersome and difficult to manage, and works for one level deep, but +it does not generalize. Further, the port reservation increases the surface area of +attack to outsiders, as ports are opened permanently, rather than +mapping specific ports at specific times, because the firewall +cannot read the control path traffic.

+

In the original active case of FTPS, the client initiates the control +connection, and the server initiates the data connection, requiring the +client firewall to permit an arbitrary inbound connection. This method +is basically limited to functioning where there no NAT at all, and extremely +limited firewalling in both directions for a transfer to occur at all.

+

The passive case, where the client initiates both control and data +connections, is much more common in modern environments. That one +complicates NAT/PAT and the use of load balancers on the +destination server side. When setting up two way exchanges, +FTPS complicates both sides’ use of load balancers or NAT +and reduces the effectiveness of firewalling measures available.

+

FTPS is fundamentally more difficult to configure for many common configurations. One +has to build a cluster differently, and arguably worse, because standard +mechanisms used for other protocols do not work. That lowers a variety of +configuration choices available only to support FTPS, with less protection +than is afforded when using other protocols.

+

Lastly, all of the peers one exchanges traffic with will face the same +issues and will find it difficult to deploy. It is rare to +find a peer that prefers FTPS.

+
+
+

Cost

+

If one assumes that the control path alone can be intercepted, leaving +the data path alone, then from the point of view of computational complexity, +the control path, rather than simply passing packets through each NAT/PAT device +must be decrypted, and re-encrypted, which is likely still small, but much larger overhead +than other protocols require. This places a higher load on load balancers and firewalls, +which are more complex to parallellize and generally more expensive than +the general purpose servers used in a product exchange array. This effect will +be more pronounced for short sessions (primarily related to connection +establishment, rather than sustained transfer).

+

In actuality, it is more likely that the data must be re-encrypted as well +as the control path, in which case the capacity for encryption of an array +of servers must be equalled by the network device to prevent a bottleneck forming. +The purpose of a product exchange array is to distribute computational load +across a variety of low cost servers. The processing power in the commodity +servers is several orders of magnitude beyond what is available to +network devices. In the encryption offload case, the load imposed on the +network devices is exponentially higher than what is required for +other protocols.

+

Network equipment vendors may tout encryption offloading, but what that amounts +to in practice, is offloading cpu work from an array of cheap redundant +servers, to a large relatively expensive and proprietary box.

+

One can avoid the cost of encryption and decryption on network devices +by limiting all configuration to being placed in a DMZ with no load balancer, +or a load balancer with static port maps per real server, and lesser firewall +protection. In which case the cost of equipment is likely no different, +but the maintenance load will be slightly heavier (more frequent credential +updates, need to maintain additional static maps, more firewall monitoring).

+
+
+

Functionality: Byte Ranges

+

In addition to the much greater complexity of firewall support for +FTPS, and the added load on expensive firewalls, there is also reduced +functionality available when compared to newer protocols, +such as SSH transfer protocol, and HTTP(S). Both of these are secured +using the same encryption algorithms as FTPS, but are single channel +protocols, and they both provide the ability to retrieve byte ranges +within files. Some newer transfer technologies use byte ranges provided +by HTTP and SFTP to permit files to be transferred by multiple parallel +streams, which is not possible with FTP or FTPS.

+
+
+

Security/Vulnerability

+

FTPS, like FTP, is usually authenticated using passwords which are secret. +If the peer in an exchange suffers an intrusion, the hash of the password, +which can be used to obtain the password itself using so-called brute +force methods in a reasonable period of time because of its limited +complexity. Most passwords are much shorter than the keys typical of +SSH.

+

One also faces the problem of intercepting the secret when it is shared. +This problem is compounded by modern security standards which force +changing of these secrets at frequent intervals, increasing the opportunity +for interception, as well as imposing extra workload on the staff +for maintenance.

+

There are configurations where passwords are permitted with SSH/SFTP, but for +data transfer applications these options are routinely disabled, which +is possible in a clean and simple manner. Usually, data exchange +involves the exchange of public keys (no secrets need be exchanged at all.) +Public keys are stronger than passwords, and most security organizations +permit much longer intervals before a change of credentials is required.

+

With SSH, if a remote server is compromised, the malicious party +obtains only the public key. Since it is already public, the attacker has +gained nothing of value. As the keys are substantially longer +than a password, the traffic is more likely to be secure in transit (though +in practice there are many details which may render this point moot.)

+

As per IETF RFC 2228, FTPS servers can be FTP servers with enhanced +security available when explicitly requested, so called explicit mode. +It is therefore possible to connect to FTPS servers and transfer in FTP +(unsecured mode). Careful configuration of servers is required to +ensure this is not inadvertantly permitted.

+

On receiving systems, it is true that a default OpenSSH configuration permits +shell level access, however the use of restricted shells and chroot jails is +commonplace in both FTP and SFTP based configurations. There is no practical +difference between FTPS and SFTP from the server account point of view.

+

In terms of firewalling, assuming the static port mapping method is used, then +a relatively simple attack on an FTPS server with that sort of configuration +would be to DDOS the data ports. Assuming the ability to watch the traffic at +some point between the ends points, an evildoer could determine the port range +mapped, and then constantly send traffic to the data ports with either incorrect +data, or to close the connection immediately preventing actual data transfer. +This is additional surface area to defend when compared to other protocols.

+

The use of the encrypted second port, where the port range used is variable +from site to site, means that most normal firewalls operating at the TCP level +will less easily distinguish file transfer traffic from web or other traffic +as there is no specific port number involved. For example, note this +bug report from checkpoint which says that to permit FTPS to traverse it, +one must disable various checks:

+
"FTP over SSL is not supported.
+
+Since FTP over SSL is encrypted, there is no way to inspect the port command
+to decide what port to open and therefore the traffic is blocked.  ...
+
+If you still cannot get this traffic through the gateway, there are several
+ways to disable FTP enforcement. Usually this is done through SmartDefense/IPS,
+by disabling the FTP Bounce attack protection.
+Note that this is NOT recommended.  [*]_
+
+
+ +
+
+

Reliability/Complexity

+

There are several modes of FTP: ascii/binary, active/passive, that create more cases to allow for. +FTPS adds more cases: explicit/implicit to the number to allow for. Encryption can be +enabled and disabled at various points in the control and data paths.

+

Example of the mode causing additional complexity: active or passive? Very common issue. Yes, the question +can be answered in practice, but one must ask: why does this question need to be answered? No other protocol +needs it.

+

Example of mode causing complexity from a decade ago: a common FTP server on linux systems is set by +default to ignore the ‘ascii’ setting on ftp sessions for performance reasons. It took quite a +while to understand why data acquisition from VAX/VMS machines were failing.

+

The inherent requirement for all the intervening NAT/PAT devices to be configured just so +to support FTPS makes it, in practice less likely to be reliable. Even in cases when +everything is correctly configured, there is room for difficulties. Recall that for FTP and FTPS, +tables need to be maintained to associate control and data connections with the correct end points. +When connections are closed, the entries have to be shutdown.

+

Example of correct configuration still having issues: in our experience, very rarely, the mapping tables get +confused. At the main Canadian weather data product exchange array, occasionally with one file out of many millions, +the file name would not match the file content. Although neither the file name, nor the content was corrupted, +the data set did not correspond to the name given the file. Many possible sources were examined, but the suspected +cause was some sort of timing issue with ports being re-used and the mapping on load balancers, where the +file name flows over the control path, and the data flows over the other port. As a test, the transfers +were migrated to SFTP, and the symptoms disappeared.

+
+
+

Summary

+

Either FTPS proxying is done in a fully general manner:

+
    +
  • the intervening devices must perform man-in-the-middle +decryption on at least the control path, which is quite undesirable from +a security perspective. Decryption of only the control path is likely not +possible without breaking TLS, so the entire data stream must +be decrypted and re-encrypted at each firewall or load balancer.

  • +
  • FTPS requires complex configuration of all intervening devices +that are common in modern configurations. In many cases, the +owners of the intervening devices will refuse to support the technology.

  • +
  • FTPS imposes a higher computational load on all intervening +devices than most alternatives available. By imposing an increased load +on specialized devices, it is generally more expensive to deploy at scale.

  • +
  • Since the above is impractical and undesirable, it is rarely done. +There are therefore commonplace situations where one simply cannot deploy +the protocol.

  • +
+

Or, if only static port mapping is done:

+
    +
  • Usual FTPS firewall configurations leave a larger surface of attack for +evildoers because the lack of visibility into the control path forces +the firewall to open more ports than is strictly necessary, increasing +surface area for attack.

  • +
  • The static data port mapping per real-server on load balancers is more +complex to maintain than what is required for other protocols.

  • +
+

In either case:

+
    +
  • One generally uses passwords, which tend to be of limited length, reducing +the overall security when compared to SSH/SFTP where use of long public/private key pairs +is commonplace, and lengthening the key length requirement is straight-forward.

  • +
  • FTPS does not support byte ranges which are useful in some applications, +and is supported by SFTP and HTTP (with or without (S)).

  • +
  • In the event of a compromise of a remote server, the password of the account +is easily determined. While best practice would mean this password is of little +or no value, some bad habits, such as password re-use, may mean the password has +some value. Contrast with SFTP: only already public information is disclosed.

  • +
  • Some FTPS server software has fall-back mechanisms and options that may cause +users or administrators to unintentionally send unencrypted information. +This could result in revealing passwords. In SFTP, the passwords are usually +not sent, the keys are an element of encryption, so there are no passwords +to intercept.

  • +
  • FTPS is inherently more complex making it more difficult to deploy and operate.

  • +
  • The limitations of supported configurations constrains firewalling approaches, +likely reducing the protection afforded internet facing servers.

  • +
+

In contrast to FTPS, SFTP:

+
    +
  • will traverse any number of NAT/PAT points on an intervening network without difficulty.

  • +
  • works behind any type of load balancers, making scaling of product exchange arrays simple.

  • +
  • does not require any intervening party to decrypt anything.

  • +
  • puts less load (both cpu and memory) on intervening network devices.

  • +
  • has similar commonplace methods for securing accounts on servers (e.g. restricted shells in chroot jails).

  • +
  • supports byte ranges, which are useful.

  • +
  • is simpler, with fewer options, therefore more reliable.

  • +
  • is simpler to monitor and firewall, and permits more constrained firewall configurations.

  • +
  • is much more common (e.g. Microsoft announcing built-in support in an upcoming Windows version [*] ).

  • +
  • normally uses public/private key pairs, which are usually considered stronger than passwords.

  • +
  • does not require any shared secrets (or a mechanism to send them.), and usually the credentials need to be replaced less +often.

  • +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/Admin.html b/How2Guides/Admin.html new file mode 100644 index 000000000..20671e070 --- /dev/null +++ b/How2Guides/Admin.html @@ -0,0 +1,1351 @@ + + + + + + + Administering AMQP Data Pumps — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Administering AMQP Data Pumps

+
+

Note

+

FIXME: Missing sections are highlighted by FIXME. What is here is accurate.

+
+
+

Overview

+

Describes setup and operations of a MetPX-Sarracenia Data Pump using +Rabbitmq as the message queueing protocol broker. For administration, +most tasks are different, depending on the broker used. If using +another broker, there needs to be another admin guide.

+
+
+

Pre-Requisites

+

Ideally, one should be familiar with user-level access to existing pumps +as either a subscriber or a source before proceeding to administration. +This manual aims to be prescriptive, rather than explanatory. For the reasons why things are +built as they are see Concepts.rst

+
+

Minimum Requirements

+

The AMQP broker is extremely light on today’s servers. The examples in +this manual were implemented on a commercial virtual private server (VPS) +with 256 MB of RAM, and 700MB of swap taken from a 20 GByte disk. Such +a tiny configuration is able to keep up with almost a full feed +from dd.weather.gc.ca (which includes, all public facing weather and +environmental data from Environment and Climate Change Canada). The +large numerical prediction files (GRIB and multiple GRIB’s in tar files) +were excluded to reduce bandwidth usage, but in terms of performance +in message passing, it kept up with one client quite well.

+

Each Sarra process is around 80 mb of virtual memory, but only about 3 mb +is resident, and you need to run enough of them to keep up (on the small VPS, +ran 10 of them.) so about 30 mbytes of RAM actually used. The broker’s RAM +usage is what determines the number of clients which can be served. Slower +clients require more RAM for their queues. So running brokerage tasks and +aggressive cleaning can reduce the overall memory footprint. The broker was +configured to use 128 MB of RAM in the examples in this manual. The rest +of the RAM was used by the apache processes for the web transport engine.

+

While the above was adequate for proof of concept, it would be impractical to +be clearing out data from disk after only an hour, and the number of clients +supportable is likely quite limited. 1GB of RAM for all the sarra related +activities should be ample for many useful cases.

+
+
+
+

Operations

+

To operate a pump, there needs to be a user designated as the pump administrator. +The administrator is different from the others mostly in the permission granted +to create arbitrary exchanges, and the ability to run processes that address the common +exchanges (xpublic, xreport, etc…) All other users are limited to being able to +access only their own resources (exchange and queues).

+

The administrative user name is an installation choice, and exactly as for any other +user, the configuration files are placed under ~/.config/sarra/, with the +defaults under admin.conf, and the configurations for components under +directories named after each component. In the component directories, +configuration files have the .conf suffix.

+

The administrative processes perform validation of postings from sources. Once +they are validated, forward the postings to the public exchanges for subscribers to access. +The processes that are typically run on a broker:

+
    +
  • poll - for sources without notification messages, revert to explicit polling for initial injection.

  • +
  • sarra - various configurations to pull data from other pumps to make it available from the local pump.

  • +
  • sender - send data to clients or other pumps that cannot pull data (usually because of firewalls.)

  • +
  • winnow - when there are multiple redundant sources of data, select the first one to arrive, and feed sarra.

  • +
  • shovel - copy notification messages from pump to another, usually to feed winnow.

  • +
  • flow - for gathering from different sorts of sources.

  • +
+

As for any other user, there may be any number of configurations +to set up, and all of them may need to run at once. To do so easily, one can invoke:

+
sr3 start
+
+
+

to start all the files with named configurations of each component (sarra, subscribe, winnow, log, etc…) +There are two users/roles that need to be set to use a pump. They are the admin and feeder options. +They are set in ~/.config/sarra/admin.conf like so:

+
feeder amqp://pumpUser@localhost/
+admin  amqps://adminUser@boule.example.com/
+
+
+

Then the report and audit components are started as well. It is standard practice to use a different +AMQP user for administrative tasks, such as exchange or user creation, which are performed by the admin +user, from data flow tasks, such as pulling and posting data, performed by the feeder user. +Normally one would place credentials in ~/.config/sarra/credentials.conf +for each account, and the various configuration files would use the appropriate account.

+
+

Housekeeping - sr3 sanity

+

When a client connects to a broker, it creates a queue which is then bound to an exchange. The user +can choose to have the client self-destruct when disconnected (auto-delete), or it can make +it durable which means it should remain, waiting for the client to connect again, even across +reboots. Clients often want to pick up where they left off, so the queues need to stay around.

+

The rabbitmq broker will never destroy a queue that is not in auto-delete (or durable). This means +they will build up over time. We have a script that looks for unused queues, and cleans them out. +Currently, the default is set that any unused queue having more than 25000 notification messages will be deleted. +One can change this limit by having option max_queue_size 50000 in default.conf.

+
+
+

Excess Queueing/Performance

+

When rabbitmq has hundreds of thousands of notification messages queued, broker performance can suffer. Such +accumulations can occur when the destination of a sender is down for a prolonged period, or a +subscriber is unavailable for some reason. In many cases, one can simply shutdown the sender, +and delete the queue on the broker. While that solves the broker performance issue, the user +will not receive the notifications.

+

On the other hand, one can just let leave it alone, and let Sarracenia take care of it using it’s +disk based retry queues. Essentially it will store records related to failed transfers on disk, +and try them again at reasonable intervals, without getting stuck on any particular item.

+

When a destination returns to service, current data is a higher priority, and it will sent +retry data, that is already late, only when there is room to do soe in the current data feed. +( https://github.com/MetPX/sarracenia/issues/620 )

+

If one gets to the point where traffic through a queue is excessive (several hundred notification messages +per second to a single queue), especially if there are many instances sharing the same queue +(if more than 40 instances to service a single queue) then one can run into a point where +adding instances gives no improvement in the overall throughput. For example, rabbitmq uses +only a single cpu to serve a queue. In such cases, creating multiple configurations, +(each with their own queue) dividing the traffic among them will allow further improvements +in throughput.

+

winnow is used to suppress duplicates. +Note that the duplicate suppresion cache is local to each instance. When N instances share a queue, the +first time a posting is received, it could be picked by one instance, and if a duplicate one is received +it would likely be picked up by another instance. For effective duplicate suppression with instances, +one must deploy two layers of subscribers. Use a first layer of subscribers (shovels) with duplicate +suppression turned off and output with post_exchangeSplit, which route posts by checksum to +a second layer of subscribers (sr_winnow) whose duplicate suppression caches are active.

+
+
+
+

Routing

+

The inter-connection of multiple pumps is done, on the data side, by daisy-chaining +sarra and/or sender configurations from one pump to the next.

+

The to_clusters, and source headers are used for routing decisions +implemented in the msg_to_clusters, and msg_by_source plugins respectively +to be user by sender or sarra components to limit data transfers between pumps.

+

For report routing, the from_cluster header is interpreted by the +msg_from_cluster plugin. Report messages are defined in the report(7) man +page. They are emitted by consumers at the end, as well as feeders as the +notification messages traverse pumps. Report messages are posted to the xs_<user> exchange, +and after validation sent to the xreport exchange by the shovel component +configurations created by sr3 declare.

+

Messages in xreports destined for other clusters are routed to destinations by +manually configured shovels. See the Reports section for more details.

+
+
+

What is Going On?

+

The sr3 declare report command can be invoked to bind to ‘xreport’ instead of the +default user exchange to get report information for an entire broker.

+

Canned report configuration with an on_message action can be configured to +gather statisical information.

+
+

Note

+

FIXME: first canned sr_report configuration would be speedo… +speedo: total rate of posts/second, total rate of logs/second. +question: should posts go to the log as well? +before operations, we need to figure out how Nagios will monitor it.

+

Is any of this needed, or is the rabbit GUI enough on it’s own?

+
+
+

Init Integration

+

By default, when sarracenia is installed, it is done as a user tool and not a system-wide resource. +The tools/ sub-directory directory allows for integration with tools for different usage scenarios.

+
+

Note

+

tools/sr.init – a sample init script suitable for sysv-init or upstart based systems. +tools/sarra_system.service – for systemd base systems for a ‘daemon’ style deployment. +tools/sarra_user.service – for systemd as a per user service.

+
+

Systemd installation process, by administrator:

+
groupadd sarra
+useradd sarra
+cp tools/sarra_system.service /etc/systemd/system/sarra.service  (if a package installs it, it should go in /usr/lib/systemd/system )
+cp tools/sarra_user.service /etc/systemd/user/sarra.service (or /usr/lib/systemd/user, if installed by a package )
+systemctl daemon-reload
+
+
+

It is then assumed that one uses the ‘sarra’ account to store the daemon oriented (or system-wide) sarra configuration. +Users can also run their personal configuration in sessions via:

+
systemctl --user enable sarra
+systemctl --user start sarra
+
+
+

On an upstart or sysv-init based system:

+
cp tools/sr.init /etc/init.d/sr
+<insert magic here to get that activated.>
+
+
+
+
+
+

Rabbitmq Setup

+

Sample information on setting up a rabbitmq broker for sarracenia to use. The broker does not have to +be on the same host as anything else, but there has to be one reachable from at least one of the +transport engines.

+
+

Installation

+

Generally speaking, we want to stay above 3.x version.

+

https://www.rabbitmq.com/install-debian.html

+

Briefly:

+
apt-get update
+apt-get install erlang-nox
+apt-get install rabbitmq-server
+
+
+

In upto-date distros, you likely can just take the distro version.

+
+
+

WebUI

+

Basically, from a root shell one must:

+
rabbitmq-plugins enable rabbitmq_management
+
+
+

which will enable the webUI for the broker. To prevent access to the management +interface for undesirables, use of firewalls, or listening only to localhost +interface for the management ui is suggested.

+
+
+

TLS

+

One should encrypt broker traffic. Obtaining certificates is outside the scope +of these instructions, so it is not discussed in detail. For the purposes of +the example, one method is to obtain certificates from letsencrypt

+
root@boule:~# git clone https://github.com/letsencrypt/letsencrypt
+Cloning into 'letsencrypt'...
+remote: Counting objects: 33423, done.
+remote: Total 33423 (delta 0), reused 0 (delta 0), pack-reused 33423
+Receiving objects: 100% (33423/33423), 8.80 MiB | 5.74 MiB/s, done.
+Resolving deltas: 100% (23745/23745), done.
+Checking connectivity... done.
+root@boule:~# cd letsencrypt
+root@boule:~/letsencrypt#
+root@boule:~/letsencrypt# ./letsencrypt-auto certonly --standalone -d boule.example.com
+Checking for new version...
+Requesting root privileges to run letsencrypt...
+   /root/.local/share/letsencrypt/bin/letsencrypt certonly --standalone -d boule.example.com
+IMPORTANT NOTES:
+ - Congratulations! Your certificate and chain have been saved at
+   /etc/letsencrypt/live/boule.example.com/fullchain.pem. Your
+   cert will expire on 2016-06-26. To obtain a new version of the
+   certificate in the future, simply run Let's Encrypt again.
+ - If you like Let's Encrypt, please consider supporting our work by:
+
+   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
+   Donating to EFF:                    https://eff.org/donate-le
+
+root@boule:~# ls /etc/letsencrypt/live/boule.example.com/
+cert.pem  chain.pem  fullchain.pem  privkey.pem
+root@boule:~#
+
+
+

This process produces key files readable only by root. To make the files +readable by the broker (which runs under the rabbitmq user’s name) one will have +to adjust the permissions to allow the broker to read the files. +probably the simplest way to do this is to copy them elsewhere:

+
root@boule:~# cd /etc/letsencrypt/live/boule*
+root@boule:/etc/letsencrypt/archive# mkdir /etc/rabbitmq/boule.example.com
+root@boule:/etc/letsencrypt/archive# cp -r * /etc/rabbitmq/boule.example.com
+root@boule:~# cd /etc/rabbitmq
+root@boule:~# chown -R rabbitmq.rabbitmq boule*
+
+
+

Now that we have proper certificate chain, configure rabbitmq to disable +tcp, and use only the RabbitMQ TLS Support (see +also RabbitMQ Management ):

+
root@boule:~#  cat >/etc/rabbitmq/rabbitmq.config <<EOT
+
+[
+  {rabbit, [
+     {tcp_listeners, [{"127.0.0.1", 5672}]},
+     {ssl_listeners, [5671]},
+     {ssl_options, [{cacertfile,"/etc/rabbitmq/boule.example.com/fullchain.pem"},
+                    {certfile,"/etc/rabbitmq/boule.example.com/cert.pem"},
+                    {keyfile,"/etc/rabbitmq/boule.example.com/privkey.pem"},
+                    {verify,verify_peer},
+                    {fail_if_no_peer_cert,false}]}
+   ]}
+  {rabbitmq_management, [{listener,
+     [{port,     15671},
+           {ssl,      true},
+           {ssl_opts, [{cacertfile,"/etc/rabbitmq/boule.example.com/fullchain.pem"},
+                          {certfile,"/etc/rabbitmq/boule.example.com/cert.pem"},
+                          {keyfile,"/etc/rabbitmq/boule.example.com/privkey.pem"} ]}
+     ]}
+  ]}
+].
+
+EOT
+
+
+

Now the broker and management interface are both configured to encrypt all traffic +passed between client and broker. An unencrypted listener was configured for localhost, +where encryption on the local machine is useless, and adds cpu load. But management only +has a single encrypted listener configured.

+
+

Note

+

Currently, sr_audit expects the Management interface to be on port 15671 if encrypted, +15672 otherwise. Sarra has no configuration setting to tell it otherwise. Choosing another +port will break sr_audit. FIXME.

+
+
+
+

Change Defaults

+

In order to perform any configuration changes the broker needs to be running. +One needs to start up the rabbitmq broker. On older ubuntu systems, that would be done by:

+
service rabbitmq-server start
+
+
+

On newer systems with systemd, the best method is:

+
systemctl start rabbitmq-server
+
+
+

By default, an installation of a rabbitmq-server makes user guest the administrator… with password guest. +With a running rabbitmq server, one can now change that for an operational implementation… +To void the guest user we suggest:

+
rabbitmqctl delete_user guest
+
+
+

Some other administrator must be defined… let’s call it bunnymaster, setting the password to MaestroDelConejito …:

+
root@boule:~# rabbitmqctl add_user bunnymaster MaestroDelConejito
+Creating user "bunnymaster" ...
+...done.
+root@boule:~#
+
+root@boule:~# rabbitmqctl set_user_tags bunnymaster administrator
+Setting tags for user "bunnymaster" to [administrator] ...
+...done.
+root@boule:~# rabbitmqctl set_permissions bunnymaster ".*" ".*" ".*"
+Setting permissions for user "bunnymaster" in vhost "/" ...
+...done.
+root@boule:~#
+
+
+

Create a local linux account under which sarra administrative tasks will run (say Sarra). +This is where credentials and configuration for pump level activities will be stored. +As the configuration is maintained with this user, it is expected to be actively used +by humans, and so should have a proper interactive shell environment. Some administrative +access is needed, so the user is added to the sudo group:

+
root@boule:~# useradd -m sarra
+root@boule:~# usermod -a -G sudo sarra
+root@boule:~# mkdir ~sarra/.config
+root@boule:~# mkdir ~sarra/.config/sarra
+
+
+

You would first need entries in the credentials.conf and admin.conf files:

+
root@boule:~# echo "amqps://bunnymaster:MaestroDelConejito@boule.example.com/" >~sarra/.config/sarra/credentials.conf
+root@boule:~# echo "admin amqps://bunnymaster@boule.example.com/" >~sarra/.config/sarra/admin.conf
+root@boule:~# chown -R sarra.sarra ~sarra/.config
+root@boule:~# passwd sarra
+Enter new UNIX password:
+Retype new UNIX password:
+passwd: password updated successfully
+root@boule:~#
+root@boule:~# chsh -s /bin/bash sarra  # for comfort
+
+
+

When Using TLS (aka amqps), verification prevents the use of localhost. +Even for access on the local machine, the fully qualified hostname must be used. +Next:

+
root@boule:~#  cd /usr/local/bin
+root@boule:/usr/local/bin# wget https://boule.example.com:15671/cli/rabbitmqadmin
+--2016-03-27 23:13:07--  https://boule.example.com:15671/cli/rabbitmqadmin
+Resolving boule.example.com (boule.example.com)... 192.184.92.216
+Connecting to boule.example.com (boule.example.com)|192.184.92.216|:15671... connected.
+HTTP request sent, awaiting response... 200 OK
+Length: 32406 (32K) [text/plain]
+Saving to: ‘rabbitmqadmin’
+
+rabbitmqadmin              100%[=======================================>]  31.65K  --.-KB/s   in 0.04s
+
+2016-03-27 23:13:07 (863 KB/s) - ‘rabbitmqadmin’ saved [32406/32406]
+
+root@boule:/usr/local/bin#
+root@boule:/usr/local/bin# chmod 755 rabbitmqadmin
+
+
+

It is necessary to download rabbitmqadmin, a helper command that is included in RabbitMQ, but not installed automatically. +One must download it from the management interface, and place it in a reasonable location in the path, so +that it will be found when it is called by sr_admin:

+
root@boule:/usr/local/bin#  su - sarra
+
+
+

From this point root will not usually be needed, as all configuration can be done from the +un-privileged sarra account.

+
+

Note

+

Out of scope of this discussion, but aside from file system permissions, +it is convenient to provide the sarra user sudo access to rabbitmqctl. +With that, the entire system can be administered without system administrative access.

+
+
+
+

Managing Users on a Pump Using Sr_audit

+

To set up a pump, one needs a broker administrative user (in the examples: sarra) +and a feeder user (in the examples: feeder). Management of other users is done with +the sr3 program.

+

First, write the correct credentials for the admin and feeder users in +the credentials file .config/sarra/credentials.conf

+
amqps://bunnymaster:MaestroDelConejito@boule.example.com/
+amqp://feeder:NoHayPanDuro@localhost/
+amqps://feeder:NoHayPanDuro@boule.example.com/
+amqps://anonymous:anonyomous@boule.example.com/
+amqps://peter:piper@boule.example.com/
+
+
+

Note that the feeder credentials are presented twice, once to allow un-encrypted access via +localhost, and a second time to permit access over TLS, potentially from other hosts (necessary +when a broker is operating in a cluster, with feeder processes running on multiple transport +engine nodes.) Next step is to put roles in .config/sarra/admin.conf

+
admin  amqps://root@boule.example.com/
+feeder amqp://feeder@localhost/
+
+
+

Specify all known users that you want to implement with their roles +in the file .config/sarra/admin.conf

+
declare subscriber anonymous
+declare source peter
+
+
+

Now to configure the pump, execute the following:

+
sr3 --users declare
+
+
+

Sample run:

+
fractal% sr3 --users declare
+2020-09-06 23:28:56,211 [INFO] sarra.rabbitmq_admin add_user permission user 'ender' role source  configure='^q_ender.*|^xs_ender.*' write='^q_ender.*|^xs_ender.*' read='^q_ender.*|^x[lrs]_ender.*|^x.*public$'
+...
+020-09-06 23:32:50,903 [INFO] root declare looking at cpost/pelle_dd1_f04
+2020-09-06 23:32:50,907 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,908 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan01 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,908 [INFO] root declare looking at cpost/veille_f34
+2020-09-06 23:32:50,912 [INFO] sarra.moth.amqp __putSetup exchange declared: xcpublic (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,912 [INFO] root declare looking at cpost/pelle_dd2_f05
+2020-09-06 23:32:50,916 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+...
+020-09-06 23:32:50,973 [INFO] root declare looking at post/shim_f63
+2020-09-06 23:32:50,973 [INFO] root declare looking at post/test2_f61
+2020-09-06 23:32:50,973 [INFO] root declare looking at report/tsarra_f20
+2020-09-06 23:32:50,978 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_report.tsarra_f20.76069129.80068939 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,978 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_report.tsarra_f20.76069129.80068939 with v02.post.# to xsarra (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,978 [INFO] root declare looking at sarra/download_f20
+2020-09-06 23:32:50,982 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_sarra.download_f20.01191787.94585787 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,982 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_sarra.download_f20.01191787.94585787 with v03.# to xsarra (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,982 [INFO] root declare looking at sender/tsource2send_f50
+2020-09-06 23:32:50,987 [INFO] sarra.moth.amqp __getSetup queue declared q_tsource.sr_sender.tsource2send_f50.60675197.29220410 (as: amqp://tsource@localhost/)
+
+
+

The sr3 program:

+
    +
  • uses the admin account from .config/sarra/admin.conf to authenticate to broker.

  • +
  • creates exchanges xpublic and xreport if they don’t exist.

  • +
  • reads roles from .config/sarra/admin.conf

  • +
  • obtains a list of users and exchanges on the pump

  • +
  • for each user in a declare option:

    +
    declare the user on the broker if missing.
    +set    user permissions corresponding to its role (on creation)
    +create user exchanges   corresponding to its role
    +
    +
    +
  • +
  • users which have no declared role are deleted.

  • +
  • user exchanges which do not correspond to users’ roles are deleted (‘xl_*,xs_*’)

  • +
  • exchanges which do not start with ‘x’ (aside from builtin ones) are deleted.

  • +
+

One can inspect whether the sr3 command did all it should using either the Management GUI +or the command line tool:

+
sarra@boule:~$ sudo rabbitmqctl  list_exchanges
+Listing exchanges ...
+      direct
+amq.direct    direct
+amq.fanout    fanout
+amq.headers   headers
+amq.match     headers
+amq.rabbitmq.log      topic
+amq.rabbitmq.trace    topic
+amq.topic     topic
+xl_peter      topic
+xreport       topic
+xpublic       topic
+xs_anonymous  topic
+xs_peter      topic
+...done.
+sarra@boule:~$
+sarra@boule:~$ sudo rabbitmqctl  list_users
+Listing users ...
+anonymous     []
+bunnymaster   [administrator]
+feeder        []
+peter []
+...done.
+sarra@boule:~$ sudo rabbitmqctl  list_permissions
+Listing permissions in vhost "/" ...
+anonymous     ^q_anonymous.*  ^q_anonymous.*|^xs_anonymous$   ^q_anonymous.*|^xpublic$
+bunnymaster   .*      .*      .*
+feeder        .*      .*      .*
+peter ^q_peter.*      ^q_peter.*|^xs_peter$   ^q_peter.*|^xl_peter$|^xpublic$
+...done.
+sarra@boule:~$
+
+
+

The above looks like sr3 did its job. +In short, here are the permissions and exchanges sr_audit manages:

+
admin user        : the only one creating users...
+admin/feeder users: have all permission over queues and exchanges
+
+subscribe user    : can write report messages to exchanged beginning with  xs_<brokerUser>
+                    can read notification messages from exchange xpublic
+                    have all permissions on queue named  q_<brokerUser>*
+
+source user       : can write notification messages to exchanges beginning with xs_<brokerUser>
+                    can read post messages from exchange  xpublic
+                    can read  report messages from exchange  xl_<brokerUser> created for him
+                    have all permissions on queue named   q_<brokerUser>*
+
+
+

To add Alice using sr_audit, one would add the following to ~/.config/sarra/admin.conf

+
declare source Alice
+
+
+

then add an appropriate amqp entry in ~/.config/sarra/credentials.conf to set the password, +then run:

+
sr3 --users declare
+
+
+

To remove users, just remove declare source Alice from the admin.conf file, and run:

+
# FIXME: functionality not present.
+
+
+

again. To delete users, one can use the existing rabbitmq management interfaces directly. +The creation is automated because the read/write/configure patterns are cumbersome to do manually.

+
    +
  • Note: By default, all users are declared. However, flows can be specified on the command line to constrain +the declared users to only those in the given flow. For example:

    +
      +
    • sr3 --users declare will declare all users

    • +
    • sr3 --users declare subscribe/dd_amis will only declare users specified in subscribe/dd_amis

    • +
    +
  • +
+
+
+

First Subscribe

+

When setting up a pump, normally the purpose is to connect it to some other pump. To set +the parameters setting up a subscription helps us set parameters for sarra later. So first +try a subscription to an upstream pump:

+
sarra@boule:~$ ls
+sarra@boule:~$ cd ~/.config/sarra/
+sarra@boule:~/.config/sarra$ mkdir subscribe
+sarra@boule:~/.config/sarra$ cd subscribe
+sarra@boule:~/.config/sarra/subscribe$ sr_subscribe edit dd.conf
+broker amqps://anonymous@dd.weather.gc.ca/
+
+mirror True
+directory /var/www/html
+
+# numerical weather model files will overwhelm a small server.
+reject .*/\.tar
+reject .*/model_giops/.*
+reject .*/grib2/.*
+
+accept .*
+
+
+

add the password for the upstream pump to credentials.conf

+
sarra@boule:~/.config/sarra$ echo "amqps://anonymous:anonymous@dd.weather.gc.ca/" >>../credentials.conf
+
+
+

then do a short foreground run, to see if it is working. Hit Ctrl-C to stop it after a few notification messages:

+
sarra@boule:~/.config/sarra$ sr_subscribe foreground dd
+2016-03-28 09:21:27,708 [INFO] sr_subscribe start
+2016-03-28 09:21:27,708 [INFO] sr_subscribe run
+2016-03-28 09:21:27,708 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2016-03-28 09:21:28,375 [INFO] Binding queue q_anonymous.sr_subscribe.dd.78321126.82151209 with key v02.post.# from exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+2016-03-28 09:21:28,933 [INFO] Received notice  20160328130240.645 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml
+2016-03-28 09:21:29,297 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWRM 20160328130240.645 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml 201 boule.example.com anonymous 1128.560235 parts=1,6451,1,0,0 sum=d,f17299b2afd78ae8d894fe85d3236488 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:29,389 [INFO] Received notice  20160328130240.646 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml
+2016-03-28 09:21:29,662 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWSK 20160328130240.646 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml 201 boule.example.com anonymous 1128.924688 parts=1,7041,1,0,0 sum=d,8cdc3420109c25910577af888ae6b617 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:29,765 [INFO] Received notice  20160328130240.647 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml
+2016-03-28 09:21:30,045 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWWA 20160328130240.647 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml 201 boule.example.com anonymous 1129.306662 parts=1,7027,1,0,0 sum=d,aabb00e0403ebc9caa57022285ff0e18 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:30,138 [INFO] Received notice  20160328130240.649 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml
+2016-03-28 09:21:30,431 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CXVG 20160328130240.649 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml 201 boule.example.com anonymous 1129.690082 parts=1,7046,1,0,0 sum=d,186fa9627e844a089c79764feda781a7 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:30,524 [INFO] Received notice  20160328130240.964 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/CA/CWAO/13/CACN00_CWAO_281300__TBO_05037
+^C2016-03-28 09:21:30,692 [INFO] signal stop
+2016-03-28 09:21:30,693 [INFO] sr_subscribe stop
+sarra@boule:~/.config/sarra/subscribe$
+
+
+

So the connection to upstream is functional. Connecting to the server means a queue is allocated on the server, +and it will continue to accumulate notification messages, waiting for the client to connect again. This was just a test, so we +want the server to discard the queue:

+
sarra@boule:~/.config/sarra/subscribe$ sr_subscribe cleanup dd
+
+
+

now let’s make sure the subscription does not start automatically:

+
sarra@boule:~/.config/sarra/subscribe$ mv dd.conf dd.off
+
+
+

and turn to a sarra set up.

+
+
+

Sarra from Another Pump

+

Sarra works by having a downstream pump re-advertise products from an upstream one. Sarra needs all the configuration of a subscription, +but also needs the configuration to post to the downstream broker. The feeder account on the broker is used for this sort +of work, and is a semi-administrative user, able to publish data to any exchange. Assume apache is set up (not covered here) with a +document root of /var/www/html. The linux account we have created to run all the sr3 processes is ‘sarra’, so we make sure +the document root is writable to those processes:

+
sarra@boule:~$ cd ~/.config/sarra/sarra
+sarra@boule:~/.config/sarra/sarra$ sudo chown sarra.sarra /var/www/html
+
+
+

Then we create a configuration:

+
sarra@boule:~$ cat >>dd.off <<EOT
+
+broker amqps://anonymous@dd.weather.gc.ca/
+exchange xpublic
+
+msg_to_clusters DD
+on_message msg_to_clusters
+
+mirror False  # usually True, except for this server!
+
+# Numerical Weather Model files will overwhelm a small server.
+reject .*/\.tar
+reject .*/model_giops/.*
+reject .*/grib2/.*
+
+directory /var/www/html
+accept .*
+
+url http://boule.example.com/
+document_root /var/www/html
+post_broker amqps://feeder@boule.example.com/
+
+EOT
+
+
+

Compared to the subscription example provided in the previous example, We have added:

+
+
exchange xpublic

sarra is often used for specialized transfers, so the xpublic exchange is not assumed, as it is with subscribe.

+
+
+

msg_to_clusters DD

+

on_message msg_to_clusters

+
+

sarra implements routing by cluster, so if data is not destined for this cluster, it will skip (not download) a product. +Inspection of the sr_subscribe output above reveals that products are destined for the DD cluster, so let’s pretend to route +for that, so that downloading happens.

+
+
+
url and document_root

these are needed to build the local posts that will be posted to the …

+
+
post_broker

where we will re-announce the files we have downloaded.

+
+
mirror False

This is usually unnecessary, when copying between pumps, it is normal to just make direct copies. +However, the dd.weather.gc.ca pump predates the day/source prefix standard, so it is necessary for +ease of cleanup.

+
+
+

So then try it out:

+
sarra@boule:~/.config/sarra/sarra$ sr_sarra foreground dd.off
+2016-03-28 10:38:16,999 [INFO] sr_sarra start
+2016-03-28 10:38:16,999 [INFO] sr_sarra run
+2016-03-28 10:38:17,000 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2016-03-28 10:38:17,604 [INFO] Binding queue q_anonymous.sr_sarra.dd.off with key v02.post.# from exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+2016-03-28 10:38:19,172 [INFO] Received v02.post.bulletins.alphanumeric.20160328.UA.CWAO.14 '20160328143820.166 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422' parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM
+2016-03-28 10:38:19,172 [INFO] downloading/copying into /var/www/html/bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422
+2016-03-28 10:38:19,515 [INFO] 201 Downloaded : v02.report.bulletins.alphanumeric.20160328.UA.CWAO.14 20160328143820.166 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422 201 boule.bsqt.example.com anonymous -0.736602 parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM message=Downloaded
+2016-03-28 10:38:19,517 [INFO] Published: '20160328143820.166 http://boule.bsqt.example.com/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422' parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM
+2016-03-28 10:38:19,602 [INFO] 201 Published : v02.report.bulletins.alphanumeric.20160328.UA.CWAO.14.UANT01_CWAO_281438___22422 20160328143820.166 http://boule.bsqt.example.com/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422 201 boule.bsqt.example.com anonymous -0.648599 parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM message=Published
+^C2016-03-28 10:38:20,328 [INFO] signal stop
+2016-03-28 10:38:20,328 [INFO] sr_sarra stop
+sarra@boule:~/.config/sarra/sarra$
+
+
+

The file has the suffix ‘off’ so that it will not be invoked by default when the entire sarra configuration is started. +One can still start the file when it is in the off setting, by specifying the path (in this case, it is in the current directory). +So initially have ‘off’ files while debugging the settings. +As the configuration is working properly, rename it to so that it will be used on startup:

+
sarra@boule:~/.config/sarra/sarra$ mv dd.off dd.conf
+sarra@boule:~/.config/sarra/sarra$
+
+
+
+
+

Reports

+

Now that data is flowing, we need to take a look at the flow of report messages, which essentially are used by each pump to tell +upstream that data has been downloaded. Sr_audit helps with routing by creating the following configurations:

+
+
    +
  • for each subscriber, a shovel configuration named rr_<user>2xreport.conf is created

  • +
  • for each source, a shovel configuration named rr_xreport2<user>user.conf is created

  • +
+
+

The 2xreport shovels subscribes to notification messages posted in each user’s xs_ exchange and posts them to the common xreport exchange. +Sample configuration file:

+
# Initial report routing configuration created by sr_audit, tune to taste.
+#     To get original back, just remove this file, and run sr_audit (or wait a few minutes)
+#     To suppress report routing, rename this file to rr_anonymous2xreport.conf.off
+
+broker amqp://tfeed@localhost/
+exchange xs_anonymous
+topicPrefix v02.report
+subtopic #
+accept_unmatch True
+on_message None
+on_post None
+report False
+post_broker amqp://tfeed@localhost/
+post_exchange xreport
+
+
+
+
Explanations:
    +
  • report routing shovels are administrative functions, and therefore the feeder user is used.

  • +
  • this configuration is to route the reports submitted by the ‘anonymous’ user.

  • +
  • on_message None, on_post None, reduce unwanted logging on the local system.

  • +
  • report False reduce unwanted reports (do sources want to understand shovel traffic?)

  • +
  • post to the xreport exchange.

  • +
+
+
+

The 2<user> shovels look at all the notification messages in the xreport exchange, and copy them to the users xr_ exchange. +Sample:

+
# Initial report routing to sources configuration, by sr_audit, tune to taste.
+#     To get original back, just remove this file, and run sr_audit (or wait a few minutes)
+#     To suppress report routing, rename this file to rr_xreport2tsource2.conf.off
+
+broker amqp://tfeed@localhost/
+exchange xreport
+topicPrefix v02.report
+subtopic #
+accept_unmatch True
+msg_by_source tsource2
+on_message msg_by_source
+on_post None
+report False
+post_broker amqp://tfeed@localhost/
+post_exchange xr_tsource2
+
+
+
+
Explanations:
    +
  • msg_by_source tsource2 selects that only the reports for data injected by the tsource2 user should be +selected.

  • +
  • the selected reports should be copied to the user’s xr_ exchange, where that user invoking sr_report will find them.

  • +
+
+
+

When a source invokes the sr_report component, the default exchange will be xr_ (eXchange for Reporting). All reports received +from subscribers to data from this source will be routed to this exchange.

+

If an administrator invokes sr_report, it will default to the xreport exchange, and show reports from all subscribers on the cluster.

+

Example:

+
blacklab% more boulelog.conf
+
+broker amqps://feeder@boule.example.com/
+exchange xreport
+accept .*
+
+blacklab%
+
+blacklab% sr_report foreground boulelog.conf
+2016-03-28 16:29:53,721 [INFO] sr_report start
+2016-03-28 16:29:53,721 [INFO] sr_report run
+2016-03-28 16:29:53,722 [INFO] AMQP  broker(boule.example.com) user(feeder) vhost(/)
+2016-03-28 16:29:54,484 [INFO] Binding queue q_feeder.sr_report.boulelog.06413933.71328785 with key v02.report.# from exchange xreport on broker amqps://feeder@boule.example.com/
+2016-03-28 16:29:55,732 [INFO] Received notice  20160328202955.139 http://boule.example.com/ radar/CAPPI/GIF/XLA/201603282030_XLA_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.040751
+2016-03-28 16:29:56,393 [INFO] Received notice  20160328202956.212 http://boule.example.com/ radar/CAPPI/GIF/XMB/201603282030_XMB_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.159043
+2016-03-28 16:29:56,479 [INFO] Received notice  20160328202956.179 http://boule.example.com/ radar/CAPPI/GIF/XLA/201603282030_XLA_CAPPI_1.0_SNOW.gif 201 blacklab anonymous 0.143819
+2016-03-28 16:29:56,561 [INFO] Received notice  20160328202956.528 http://boule.example.com/ radar/CAPPI/GIF/XMB/201603282030_XMB_CAPPI_1.0_SNOW.gif 201 blacklab anonymous -0.119164
+2016-03-28 16:29:57,557 [INFO] Received notice  20160328202957.405 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01910 201 blacklab anonymous -0.161522
+2016-03-28 16:29:57,642 [INFO] Received notice  20160328202957.406 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01911 201 blacklab anonymous -0.089808
+2016-03-28 16:29:57,729 [INFO] Received notice  20160328202957.408 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01912 201 blacklab anonymous -0.043441
+2016-03-28 16:29:58,723 [INFO] Received notice  20160328202958.471 http://boule.example.com/ radar/CAPPI/GIF/WKR/201603282030_WKR_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.131236
+2016-03-28 16:29:59,400 [INFO] signal stop
+2016-03-28 16:29:59,400 [INFO] sr_report stop
+blacklab%
+
+
+

From this listing, we can see that a subscriber on blacklab is actively downloading from the new pump on boule. +Basically, the two sorts of shovels built automatically by sr_audit will do all the routing needed within a cluster. +When there are volume issues, these configurations can be tweaked to increase the number of instances or use +post_exchangeSplit where appropriate.

+

Manual shovel configuration is also required to route notification messages between clusters. It is just a variation +of intra-cluster report routing.

+
+
+

Sarra From a Source

+

When reading posts directly from a source, one needs to turn on validation. +FIXME: example of how user posts are handled.

+
+
    +
  • set sourceFromExchange

  • +
  • set mirror False to get date/source tree prepended

  • +
  • validate that the checksum works…

  • +
+
+

anything else?

+
+
+

Cleanup

+

These are examples, the implementation of cleanup is not covered by +sarracenia. Given a reasonably small tree as given above, it can be +practical to scan the tree and prune the old files from it. A cron +job like so:

+
root@boule:/etc/cron.d# more sarra_clean
+# remove files one hour after they show up.
+# for weather production, 37 minutes passed the hour is a good time.
+# remove directories the day after the last time they were touched.
+37 4 * * *  root find /var/www/html -mindepth 1 -maxdepth 1 -type d -mtime +0  | xargs rm -rf
+
+
+

This might see a bit aggressive, but this file was on a very small virtual server that was only +intended for real-time data transfer so keeping data around for extended periods would have +filled the disk and stopped all transfers. In large scale transfers, there is always a trade +off between the practicality of keeping the data around forever, and the need for performance, +which requires us to prune directory trees regularly. File system performance is optimal with +reasonably sized trees, and when the trees get too large, the ‘find’ process to traverse it, can +become too onerous.

+

One can more easily maintain smaller directory trees by having them roll over regularly. If you +have enough disk space to last one or more days, then a single logical cron job that would operate +on the daily trees without incurring the penalty of a find is a good approach.

+

Replace the contents above with:

+
34 4 * * * root find /var/www/html -mindepth 1 -maxdepth 1  -type d -regex '/var/www/html/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' -mtime +1 | xargs rm -rf
+
+
+

where the +1 can be replaced by the number of days to retain. ( Would have preferred to +use [0-9]{8}, but it would appear that find’s regex syntax does not include repetitions. )

+

Note that the logs will clean up themselves. By default after 5 retention the oldest log will be +remove at midnight if you have always use the same default config since the first rotation. +It can be shorten to a single retention by adding logRotateCount 1 to default.conf.

+
+
+

Ensuring Things are Up

+

Processes can crash. One can have automated restart by running sr3 sanity periodically:

+
root@boule:/etc/cron.d# more sanity
+# remove files one hour after they show up.
+# for weather production, 37 minutes passed the hour is a good time.
+# remove directories the day after the last time they were touched.
+7,14,21,28,35,42,49,56 * * * sr3 sanity
+
+
+
+
+

Startup

+

The Debian package installs a systemd unit, but python3 installation does not take +care of that.

+
+
+

Sr_Poll

+

FIXME: feed the sarra from source configured with an sr_poll. set up.

+
+
+

Sr_winnow

+

FIXME: sample sr_winnow configuration explained, with some shovels also.

+
+
+

Sr_sender

+

Where firewalls prevent use of sarra to pull from a pump like a subscriber would, one can reverse the feed by having the +upstream pump explicitly feed the downstream one.

+

FIXME: elaborate sample sr_sender configuration.

+
+
+

Manually Adding Users

+

To avoid the use of sr_admin, or work around issues, one can adjust user settings manually:

+
cd /usr/local/bin
+wget -q https://boule.example.com:15671/cli/rabbitmqadmin
+chmod 755 rabbitmqadmin
+
+rabbitmqctl add_user Alice <password>
+rabbitmqctl set_permissions -p / Alice   "^q_Alice.*$" "^q_Alice.*$|^xs_Alice$" "^q_Alice.*$|^xl_Alice$|^xpublic$"
+
+rabbitmqadmin -u root -p ***** declare exchange name=xs_Alice type=topic auto_delete=false durable=true
+rabbitmqadmin -u root -p ***** declare exchange name=xl_Alice type=topic auto_delete=false durable=true
+
+
+

or, parametrized:

+
u=Alice
+rabbitmqctl add_user ${u} <password>
+rabbitmqctl set_permissions -p / ${u} "^q_${u}.$" "^q_${u}.*$|^xs_${u}$" "^q_${u}.*$|^xl_${u}$|^xpublic$"
+
+rabbitmqadmin -u root -p ***** declare exchange name=xs_${u} type=topic auto_delete=false durable=true
+rabbitmqadmin -u root -p ***** declare exchange name=xl_${u} type=topic auto_delete=false durable=true
+
+
+

Then you need to do the same work for sftp and or apache servers as required, as +authentication needed by the payload transport protocol (SFTP, FTP, or HTTP(S)) +is managed separately.

+
+
+
+

Advanced Installations

+

On some configurations (we usually call them bunny), we use a clustered rabbitmq, like so:

+
/var/lib/rabbitmq/.erlang.cookie  same on all nodes
+
+on each node restart  /etc/init.d/rabbitmq-server stop/start
+
+on one of the node
+
+rabbitmqctl stop_app
+rabbitmqctl join_cluster rabbit@"other node"
+rabbitmqctl start_app
+rabbitmqctl cluster_status
+
+
+# having high availability queue...
+# here all queues that starts with "cmc." will be highly available on all the cluster nodes
+
+rabbitmqctl set_policy ha-all "^(cmc|q_)\.*" '{"ha-mode":"all"}'
+
+
+
+

Clustered Broker Keepalived Setup

+

In this example, bunny-op is a vip that migrates between bunny1-op and bunny2-op. +Keepalived moves the vip between the two:

+
#=============================================
+# vip bunny-op 192.101.12.59 port 5672
+#=============================================
+
+vrrp_script chk_rabbitmq {
+        script "killall -0 rabbitmq-server"
+        interval 2
+}
+
+vrrp_instance bunny-op {
+        state BACKUP
+        interface eth0
+        virtual_router_id 247
+        priority 150
+        track_interface {
+                eth0
+        }
+        advert_int 1
+        preempt_delay 5
+        authentication {
+                auth_type PASS
+                auth_pass bunop
+        }
+        virtual_ipaddress {
+# bunny-op
+                192.101.12.59 dev eth0
+        }
+        track_script {
+                chk_rabbitmq
+        }
+}
+
+
+
+
+

LDAP Integration

+

To enable LDAP authentication for rabbitmq:

+
rabbitmq-plugins enable rabbitmq_auth_backend_ldap
+
+# replace username by ldap username
+# clear password (will be verified through the ldap one)
+rabbitmqctl add_user username aaa
+rabbitmqctl clear_password username
+rabbitmqctl set_permissions -p / username "^xpublic|^amq.gen.*$|^cmc.*$" "^amq.gen.*$|^cmc.*$" "^xpublic|^amq.gen.*$|^cmc.*$"
+
+
+

And you need to set up LDAP parameters in the broker configuration file: +(this sample ldap-dev test config worked when we tested it…):

+
cat /etc/rabbitmq/rabbitmq.config
+[ {rabbit, [{auth_backends, [ {rabbit_auth_backend_ldap,rabbit_auth_backend_internal}, rabbit_auth_backend_internal]}]},
+  {rabbitmq_auth_backend_ldap,
+   [ {servers,               ["ldap-dev.cmc.ec.gc.ca"]},
+     {user_dn_pattern,       "uid=${username},ou=People,ou=depot,dc=ec,dc=gc,dc=ca"},
+     {use_ssl,               false},
+     {port,                  389},
+     {log,                   true},
+     {network,               true},
+    {vhost_access_query,    {in_group,
+                             "ou=${vhost}-users,ou=vhosts,dc=ec,dc=gc,dc=ca"}},
+    {resource_access_query,
+     {for, [{permission, configure, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+            {permission, write,
+             {for, [{resource, queue,    {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, exchange, {constant, true}}]}},
+            {permission, read,
+             {for, [{resource, exchange, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, queue,    {constant, true}}]}}
+           ]
+     }},
+  {tag_queries,           [{administrator, {constant, false}},
+                           {management,    {constant, true}}]}
+ ]
+}
+].
+
+
+
+
+

Requires RABBITMQ > 3.3.x

+

Was searching on how to use LDAP strictly for password authentication +The answer I got from the Rabbitmq gurus

+
On 07/08/14 20:51, michel.grenier@ec.gc.ca wrote:
+> I am trying to find a way to use our ldap server  only for
+> authentification...
+> The user's  permissions, vhost ... etc  would already be set directly
+> on the server
+> with rabbitmqctl...  The only thing ldap would be used for would be
+> logging.
+> Is that possible... ?   I am asking because our ldap schema is quite
+> different from
+> what rabbitmq-server requieres.
+
+Yes (as long as you're using at least 3.3.x).
+
+You need something like:
+
+{rabbit,[{auth_backends,
+           [{rabbit_auth_backend_ldap, rabbit_auth_backend_internal}]}]}
+
+See http://www.rabbitmq.com/ldap.html and in particular:
+
+"The list can contain names of modules (in which case the same module is used for both authentication and authorisation), *or 2-tuples like {ModN, ModZ} in which case ModN is used for authentication and ModZ is used for authorisation*."
+
+Here ModN is rabbit_auth_backend_ldap and ModZ is rabbit_auth_backend_internal.
+
+Cheers, Simon
+
+
+
+
+

Support

+

It is now possible to enable MQTT in Sarracenia through the RabbitMQ MQTT plugin. Here is a minimal howto guide for our RabbitMQTT support:

+
    +
  • After any other MQTT service listening to port 1883 got disabled, enable RabbitMQ MQTT plugin.:

    +
    rabbitmq-plugins enable rabbitmq_mqtt
    +cat >> /etc/rabbitmq/rabbitmq.config << EOF
    +[{rabbitmq_mqtt, [{default_user,     <<"anonymous">>},
    +                  {default_pass,     <<"anonymous">>},
    +                  {allow_anonymous,  true},
    +                  {vhost,            <<"/">>},
    +                  {exchange,         <<"xmqtt_public">>},
    +                  {ssl_listeners,    []},
    +                  {tcp_listeners,    [1883]},
    +                  {tcp_listen_options, [{backlog, 4096},
    +                                        {nodelay, true}]}]}
    +].
    +EOF
    +systemctl restart rabbitmq-server
    +
    +
    +
  • +
  • Change anonymous user (rabbit_mqtt.default_user) permissions to allow partner user to subscribe to your mqtt feed (ie. using mosquitto_sub):

    +
    rabbitmqctl set_permissions -p / anonymous "^q_anonymous.*|^mqtt-subscription" "^q_anonymous.*|^xs_anonymous$|^mqtt-subscription" "^q_anonymous.*|^x[lrs]_anonymous.*|^x.*public$"
    +
    +
    +
  • +
  • Write your configurations that will publish to rabbitmqtt exchange:

    +
    # Here is a minimal shovel/myshovel.conf
    +# Subscribe from a source amqp exchange
    +broker amqp://${afeeder}@${abroker}
    +exchange ${from_exchange}
    +
    +# posting to rabbitmqtt exchange
    +post_broker amqp://${afeeder}@${abroker}
    +post_exchange xmqtt_public
    +post_topicPrefix  v03.${from_exchange}
    +report False
    +
    +
    +

    or consume from rabbitmqtt exchange:

    +
    # Here is a minimal subscribe/mysub.conf
    +broker amqp://${asub}@${abroker}/
    +exchange xmqtt_public
    +topicPrefix v03.${from_exchange}
    +
    +# Print out all msg received
    +accept .*
    +on_message msg_rawlog
    +download off
    +
    +
    +

    Note that we use xmqtt_public as the (post_)exchange which is defined as the rabbitmq_mqtt.exchange +in the rabbitmq.config file. We also append the source exchange to the (post_)topicPrefix, which will +map the source exchange and could be useful if we map multiple exchanges to mqtt.

    +
  • +
  • Start and test your configuration:

    +
    sr_shovel start myshovel.conf
    +sr_subscribe foreground mysub.conf
    +
    +
    +

    On another machine you may now run:

    +
    mosquitto_sub -h ${abroker} -t '#' -d
    +
    +
    +

    Messages received from both sr_subscribe and mosquitto_sub should be the same.

    +
  • +
+
+
+
+

Hooks from Sundew

+

This information is very likely irrelevant to almost all users. Sundew is another module of MetPX which is essentially being +replaced by Sarracenia. This information is only useful to those with an installed based of Sundew wishing to bridge +to sarracenia. The early work on Sarracenia used only the subscribe client as a downloader, and the existing WMO switch module +from MetPX as the data source. There was no concept of multiple users, as the switch operates as a single dissemination +and routing tool. This section describes the kinds of glue used to feed Sarracenia subscribers from a Sundew source. +It assumes a deep understanding of MetPX-Sundew. Currently, the dd_notify.py script creates notification messages for the +protocol exp., v00. and v02 (latest sarracenia protocol version).

+
+

Notifications on DD

+

As a higher performance replacement for Atom/RSS feeds which tell subscribers when new data is available, we put a broker +on our data dissemination server (dd.weather.gc.ca). Clients can subscribe to it. To create the notifications, we have +one Sundew Sender (named wxo-b1-oper-dd.conf) with a send script:

+
type script
+send_script sftp_amqp.py
+
+# connection info
+protocol    ftp
+host        wxo-b1.cmc.ec.gc.ca
+user        wxofeed
+password    **********
+ftp_mode    active
+
+noduplicates false
+
+# no filename validation (pds format)
+validation  False
+
+# delivery method
+lock  umask
+chmod 775
+batch 100
+
+
+

We see all the configuration information for a single-file sender, but the send_script overrides the +normal sender with something that builds AMQP messages as well. This Sundew sender config +invokes sftp_amqp.py as a script to do the actual send, but also to place the payload of an +AMQP message in the /apps/px/txq/dd-notify-wxo-b1/, queuing it up for a Sundew AMQP sender. +That sender´s config is:

+
type amqp
+
+validation False
+noduplicates False
+
+protocol amqp
+host wxo-b1.cmc.ec.gc.ca
+user feeder
+password ********
+
+exchange_name cmc
+exchange_key  v02.post.${0}
+exchange_type topic
+
+reject ^ensemble.naefs.grib2.raw.*
+
+accept ^(.*)\+\+.*
+
+
+

The key for the topic includes a substitution. The ${0} contains the directory tree where the +file has been placed on dd (with the / replaced by .) For example, here is a log file entry:

+
2013-06-06 14:47:11,368 [INFO] (86 Bytes) Message radar.24_HR_ACCUM.GIF.XSS++201306061440_XSS_24_HR_ACCUM_MM.gif:URP:XSS:RADAR:GIF::20130606144709  delivered (lat=1.368449,speed=168950.887119)
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/Admin_Rabbit_Addendum.html b/How2Guides/Admin_Rabbit_Addendum.html new file mode 100644 index 000000000..a3310a1c5 --- /dev/null +++ b/How2Guides/Admin_Rabbit_Addendum.html @@ -0,0 +1,510 @@ + + + + + + + Administering Rabbitmq Adddendum — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Administering Rabbitmq Adddendum

+

Old edits people wanted to keep?

+
+

Introduction

+

AMQP stands for Advanced Message Queuing Protocol. +It is the definition of a protocol that comes from the need to standardize an asynchronous message change system. +In AMQP jargon we will talk about message producers, message consumers and broker.

+
+
+

RABBITMQ-SERVER installation

+

On our machines that need to process AMQP messages, +we install the broker, by installing the package rabbitmq-server_3.3.5-1_all.deb. +The basic installation is done as follows on all our machines:

+
# installing package taken on the rabbitmq homepage
+# rabbitmq-server version > 3.3.x  required to use ldap for passwords verification only
+
+apt-get install erlang-nox
+dpkg -i /tmp/rabbitmq-server_3.3.5-1_all.deb
+
+# create anonymous user
+# password ********* provided in potato
+#                                          conf write read
+rabbitmqctl add_user anonymous *********
+rabbitmqctl set_permissions -p / anonymous   "^xpublic|^amq.gen.*$|^cmc.*$"     "^amq.gen.*$|^cmc.*$"    "^xpublic|^amq.gen.*$|^cmc.*$"
+rabbitmqctl list_user_permissions anonymous
+
+# create feeder user
+# password ********* provided in potato
+#                                       conf write read
+rabbitmqctl add_user feeder ********
+rabbitmqctl set_permissions -p / feeder  ".*"  ".*"  ".*"
+rabbitmqctl list_user_permissions feeder
+
+# create administrator user
+# password ********* provided in potato
+
+rabbitmqctl add_user root   *********
+rabbitmqctl set_user_tags root administrator
+
+# takeaway administrator privileges from guest
+rabbitmqctl set_user_tags guest
+rabbitmqctl list_user_permissions guest
+rabbitmqctl change_password guest *************
+
+# list users
+rabbitmqctl list_users
+
+
+# enabling management web application
+# this is important since sr_rabbit uses this management facility/port access
+# to retrieve some important info
+
+rabbitmq-plugins enable rabbitmq_management
+/etc/init.d/rabbitmq-server restart
+
+
+
+
+

RABBITMQ-SERVER cluster installation

+

On the bunny we have opted for a cluster installation. To do this we follow the following instructions:

+
Stop rabbitmq-server on all nodes....
+
+/var/lib/rabbitmq/.erlang.cookie  same on all nodes
+
+on each node restart  /etc/init.d/rabbitmq-server stop/start
+
+on one of the node
+
+rabbitmqctl stop_app
+rabbitmqctl join_cluster rabbit@"other node"
+rabbitmqctl start_app
+rabbitmqctl cluster_status
+
+
+# having high availability queue...
+# here all queues that starts with "cmc." will be highly available on all the cluster nodes
+
+rabbitmqctl set_policy ha-all "^cmc\." '{"ha-mode":"all"}'
+
+
+
+
+

RABBITMQ-SERVER ldap installation

+

On the servers where we want to have an authentication using the following instructions:

+
rabbitmq-plugins enable rabbitmq_auth_backend_ldap
+
+# replace username by ldap username
+# clear password (will be verified through the ldap one)
+rabbitmqctl add_user username aaa
+rabbitmqctl clear_password username
+rabbitmqctl set_permissions -p / username "^xpublic|^amq.gen.*$|^cmc.*$" "^amq.gen.*$|^cmc.*$" "^xpublic|^amq.gen.*$|^cmc.*$"
+
+
+

And we configure the LDAP services in the rabbitmq-server configuration file +(old test configuration of ldap-dev which worked only…):

+
cat /etc/rabbitmq/rabbitmq.config
+[
+{rabbit, [{auth_backends, [ {rabbit_auth_backend_ldap,rabbit_auth_backend_internal}, rabbit_auth_backend_internal]}]},
+{rabbitmq_auth_backend_ldap,
+    [
+    {servers,               ["ldap-dev.cmc.ec.gc.ca"]},
+    {user_dn_pattern,       "uid=${username},ou=People,ou=depot,dc=ec,dc=gc,dc=ca"},
+    {use_ssl,               false},
+    {port,                  389},
+    {log,                   true},
+    {network,               true},
+    {vhost_access_query,    {in_group,
+                            "ou=${vhost}-users,ou=vhosts,dc=ec,dc=gc,dc=ca"}},
+    {resource_access_query,
+    {for, [{permission, configure, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+            {permission, write,
+            {for, [{resource, queue,    {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, exchange, {constant, true}}]}},
+            {permission, read,
+            {for, [{resource, exchange, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, queue,    {constant, true}}]}}
+            ]
+    }},
+    {tag_queries,           [{administrator, {constant, false}},
+                            {management,    {constant, true}}]}
+    ]
+}
+].
+
+
+
+
+

Use of AMQP on DD (DDI, DD.BETA)

+

We (Peter) wanted to do an implementation of AMQP in METPX. +To do this, we use the python-amqplib library which implements the necessary functionality of AMQP in python. +We have thus developped a pxSender of type amqp which is the producer of notification messages as well as a pxReceiver of type amqp which serves as a consumer of notification messages. +As a broker, we use rabbitmq-server which is a standard debian package of an AMQP broker.

+

A pxSender of type amqp, reads the content of a file in its queue, makes a message to which it attaches a “topic” and sends it to the broker. +A pxReceiver of type amqp will announce to the broker the “topic” for which it is interested to receive notification messages, and the broker will send it each message corresponding to its choice.

+

As a message can be anything, at the level of the pxSender, we have also attached the name of the file from which the message comes. +Thus in our pxReceiver, we can insure the content of the message in the corresponding file name. +This trick is useless only for amqp changes between a sender and an amqp receiver…

+
+

Notifications for DD

+

We found in AMQP an opportunity to announce products when they arrive on DD. +So a user instead of constantly verifying if a product is present on DD. +To change it, he could subscribe (AMQP topic) to receive a message (the url of the product) that would be omitted only at the delivery of the product on DD. +We wouldn’t do this exercise for newsletters… but for other products (grib,images… etc)

+

To implement this, we used a possibility of pxSender, the sender_script. +We have written a script sftp_amqp.py that makes the deliveries to DD and for each product, it creates a file containing the URL under which the product will be present. +Here is the beginning of the configuration of wxo-b1-oper-dd.conf:

+
type script
+send_script sftp_amqp.py
+
+# connection info
+protocol    ftp
+host        wxo-b1.cmc.ec.gc.ca
+user        wxofeed
+password    **********
+ftp_mode    active
+
+noduplicates false
+
+# no filename validation (pds format)
+validation  False
+
+# delivery method
+lock  umask
+chmod 775
+batch 100
+
+
+

We see in this config that all the information for a single-file sender is there. +But because the type is script… and the send_script sftp_amqp.py is provided, we are able to instruct our sender to do more…

+

The file containing the URL is placed under the txq of an AMQP sender /apps/px/txq/dd-notify-wxo-b1 for the AMQP notification to be done. +To send the files in this queue, a sender has to have written dd-notify-wxo-b1.conf which is configured as follows:

+
type amqp
+
+validation False
+noduplicates False
+
+protocol amqp
+host wxo-b1.cmc.ec.gc.ca
+user feeder
+password ********
+
+exchange_name cmc
+exchange_key  exp.dd.notify.${0}
+exchange_type topic
+
+reject ^ensemble.naefs.grib2.raw.*
+
+accept ^(.*)\+\+.*
+
+
+

Again, the cl for the topic contains a programmed part. +The ${0} part contains the tree structure where the product is placed on dd… For example, here is a log line from dd-notify-wxo-b1.log:

+
2013-06-06 14:47:11,368 [INFO] (86 Bytes) Message radar.24_HR_ACCUM.GIF.XSS++201306061440_XSS_24_HR_ACCUM_MM.gif:URP:XSS:RADAR:GIF::20130606144709  delivered (lat=1.368449,speed=168950.887119)
+
+
+ + + + + + + + + + + + +

And so the cl would be.

exp.dd.notify.radar.24_HR_ACCUM.GIF.XSS

And the location of the file

http://dd1.weather.gc.ca/radar/24_HR_ACCUM/GIF/XSS

And the complete URL in the message

http://dd1.weather.gc.ca/radar/24_HR_ACCUM/GIF/XSS/201306061440_XSS_24_HR_ACCUM_MM.gif

+
+
+

Utilities installed on DD servers

+

When a client connects to the broker (rabbitmq-server) it must create a queue and attach it to an exchange. +We can give this queue the option that it self-destructs when it is no longer in use or that it is preserved and continues to stack products if the client is offline. +In general, we would like the queue to be preserved and thus the connection resumption restarts the product collection without loss.

+
+
queue_manager.py

The rabbitmq-server will never destroy a queue that has been created by a client if it is not in auto-delete mode (let alone if it is created with durability). +This can cause a problem for example, a client that develops a process, can change IDEs several times and crams on the server a multitude of queues that will never be used. +So we created a queue_manager.py script that verifies if the unused queues have more than X products waiting or take more than Y Mbytes… +If so, they are destroyed by the script.

+

At the time of writing this document, the limits are : 25000 messages and 50Mb.

+
+
dd-xml-inotify.py

On our public datamart, there are products that do not come directly from pds/px/pxatx. +As our notifications are made from the product delivery, we don’t have notification messages for them. +This is the case for the XML products under the directories: citypage_weather and marine_weather. +To overcome this situation, the daemon dd-xml-inotify.py has been created and installed. +This python script uses inotify to monitor the modification of products under their directories. +If a product is modified or added, an amqp notification is sent to the server. +Thus all products in the datamart are covered by the message sending.

+
+
+
+
+
+

Using AMQP with URP, BUNNY, PDS-OP

+
+

Note

+

also applies to dev…

+
+
+

From URP-1/2 announce to BUNNY-OP that a product is ready

+

On urp-1/2 a metpx rolls the sender amqp_expose_db.conf which announces that a product has just arrived in the db of metpx with a message of the form:

+
Md5sum of product name           file-size  url                        dbname
+a985c32cbdee8af2ab5d7b8f6022e781 498081     http://urp-1.cmc.ec.gc.ca/ db/20150120/RADAR/URP/IWA/201501201810~~PA,60,10,PA_PRECIPET,MM_HR,MM:URP:IWA:RADAR:META::20150120180902
+
+
+

These AMQP messages are sent to the rabbitmq server on bunny-op with an exchange key that starts with v00.urp.input followed by convention by the path from db with the ‘/’ replaced with ‘.’.

+
+

Note

+

that urp-1/2 runs apache and that the product annonce is in the db of metpx and is visible from the URL of the message.

+
+
+
+

BUNNY-OP and dd_dispatcher.py

+

bunny-op is a vip that lives on bunny1-op or bunny2-op. +It is with keepalived that we make sure that this vip resides on one of the bunny-op. +We also test that rabbitmq-server is running on the same server. +The configuration part of keepalived that deals with the vip is:

+
vip bunny-op 142.135.12.59 port 5672
+
+vrrp_script chk_rabbitmq {
+        script "killall -0 rabbitmq-server"
+        interval 2
+}
+
+vrrp_instance bunny-op {
+        state BACKUP
+        interface eth0
+        virtual_router_id 247
+        priority 150
+        track_interface {
+                eth0
+        }
+        advert_int 1
+        preempt_delay 5
+        authentication {
+                auth_type PASS
+                auth_pass bunop
+        }
+        virtual_ipaddress {
+# bunny-op
+                142.135.12.59 dev eth0
+        }
+        track_script {
+                chk_rabbitmq
+        }
+}
+
+
+

The rabbitmq-servers on these machines are installed in a cluster. +We put high availability on the queues beginnig with cmc.*. +On each of the machines run the utility dd_dispatcher.py. +This program verifies whether the vip bunny-op and proc�dera has its work only on the server where the vip lives. +(If there is a switch, auto detection in 5 seconds and the queues remain unchanged)

+

The utility dd_dispatcher.py subscribes to the notification messages v00.urp.input.# and thus redirects the notification messages from the 2 URP operative servers. +Upon reception of a first product, the product’s md5dum is placed in a cache and the message is r�exp�di� but this time with v00.urp.notify as the exchange key. +If another message arrives from v00.urp.input with the same md5sum as the first one, it is ignored, so the products announced from the exchange key v00.urp.notify are unique and represent the first arrival of the 2 operative URPs.

+
+
+

PDS-OP receptions of dispatch notification messages, wget of radar products

+

On pds-op, a pull_urp receiver, execute the fx_script pull_amqp_wget.py. +In this script, the following command:

+
# shared queue : each pull receive 1 message (prefetch_count=1)
+self.channel.basic_qos(prefetch_size=0,prefetch_count=1,a_global=False)
+
+
+

makes that the distribution of notification messages v00.urp.notify will be distributed equally across the 5 servers under pds-op. +We therefore guarantee a distributed pull. +For each message of the form:

+
a985c32cbdee8af2ab5d7b8f6022e781 498081 http://urp-1.cmc.ec.gc.ca/ db/20150120/RADAR/URP/IWA/201501201810~~PA,60,10,PA_PRECIPET,MM_HR,MM:URP:IWA:RADAR:META::20150120180902
+
+
+

the url is reb�ted from the last 2 fields of the message and a wget of the product is made and placed in the receiver queue which is then ignored/routed in an ordinary way.

+
+
+

Verification / Troubleshooting

+

In order of production

+
    +
  1. +
    On urp-1/2:
      +
    • Verify that the radar products are generated on urp-1/2.

    • +
    • Verify that notifications are generated on urp-1/2 /apps/px/log/tx_amqp_expose_db.log

    • +
    +
    +
    +
  2. +
  3. +
    On bunny1/2-op
      +
    • Check where bunny-op resides

    • +
    • Verify the logs of dd_dispatcher.py /var/log/dd_dispatcher_xxxx.log where xxxx is the process pid

    • +
    +
    +
    +
  4. +
  5. +
    On pds-op
      +
    • Check the pull_urp

    • +
    +
    +
    +
  6. +
+

Repairing the processes that are not working properly should fix the problems in general. +More details will be added here as problems are encountered and corrected.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/Email_Ingesting_With_Sarracenia.html b/How2Guides/Email_Ingesting_With_Sarracenia.html new file mode 100644 index 000000000..e40f7cf99 --- /dev/null +++ b/How2Guides/Email_Ingesting_With_Sarracenia.html @@ -0,0 +1,343 @@ + + + + + + + Email Ingesting with Sarracenia (v2) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Email Ingesting with Sarracenia (v2)

+

Email is an easy way to route data between servers. Using the Post Office Protocol (POP3) and +Internet Message Access Protocol (IMAP), email files can be disseminated through Sarracenia +by extending the polling and downloading functions.

+
+

Polling

+
+

Extending Polling Protocols

+

Out of the box, Sarracenia supports polling pollUrl with HTTP/HTTPS and SFTP/FTP protocols. +Other protocols can be supported by subclassing the sarracenia.flowcb.poll.Poll class. +Fortunately there is an existing mail poll plugin, which invokes a plugin. +start by listing available examples:

+
fractal% sr3 list examples | grep poll
+cpump/cno_trouble_f00.inc        poll/airnow.conf
+poll/aws-nexrad.conf             poll/mail.conf
+poll/nasa-mls-nrt.conf           poll/noaa.conf
+poll/soapshc.conf                poll/usgs.conf
+fractal%
+
+
+

adding the configuration:

+
fractal% sr3 add poll/mail.conf
+add: 2022-03-10 15:59:48,266 2785187 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/poll/mail.conf to /home/peter/.config/sr3/poll/mail.conf
+fractal%
+
+
+

What did we get?:

+
fractal% cat ~/.config/sr3/poll/mail.conf
+#
+# Sample poll config, used to advertise availability of new emails using either POP3/IMAP protocols.
+# To use, make sure rabbitmq is running as described in the Dev.rst documentation,
+# and a tsource user/xs_tsource exchange exist, with FLOWBROKER set to the hostname
+# rabbitmq is running on (e.g. export FLOWBROKER='localhost')
+#
+# The pollUrl is in RFC 1738 format, e.g. <scheme>://<user>@<host>:<port>/ where your full credentials,
+# <scheme>://<user>:<password>@<host>:<port>/ would be contained in your ~/.config/sarra/credentials.conf.
+# Valid schemes are pop/pops/imap/imaps, where the s denotes an SSL connection. If a port isn't
+# specified, the default port associated with the scheme will be used (IMAPS -> 993, POPS -> 995,
+# IMAP -> 143, POP -> 110).
+#
+
+post_broker amqp://tsource@${FLOWBROKER}
+post_exchange xs_tsource
+
+scheduled_interval 60
+
+pollUrl <scheme>://<user>@<host>:<port>/
+
+callback poll.mail
+
+fractal%
+
+
+

Now when the poll instance is started up with this plugin,

+
+
+

Implementing POP/IMAP

+

NOTE: not yet converted to v3.

+

With Python’s poplib and imaplib modules, the pollUrl can be parsed and the email server +connected to as per the scheme specified. Sarracenia can extract the credentials from the pollUrl +through its built-in classes, so no passwords need to be stored in the config file to connect. POP3 +uses an internal read-flag to determine if a message has been seen or not. If a message is unread, after +retrieving it with POP3 it will be marked as read, and it won’t be picked up on further polls. +POP3 offers further options like deleting the file after it’s been read, but IMAP offers more mail +management options like moving between folders and generating more complex queries. IMAP also allows +more than one client to connect to a mailbox at the same time, and supports tracking message flags like +whether the message is read/unread, replied to/not yet replied to, or deleted/still in the inbox. The +example polling plugin +poll_email_ingest.py +only retrieves unread email in the inbox and marks them as unread after retrieving them, in both the +POP and IMAP versions. This setting can be easily changed as per the end user’s intentions. If there +are any new messages from the last time a POP/IMAP client had connected, it will then advertise the file +based on the subject and a timestamp, where an sr_subscribe instance can receive the posted message, +connect individually to the server, and download the message to output into a file locally. A sample +configuration has been included under examples as pollingest.conf. Once you edit/supply the environment variables required for the +config to work, open a new terminal and run:

+
[aspymap:~]$ sr_poll foreground pollingest.conf
+
+
+

If the credentials have been included correctly, and all the variables were set, the output should look +something like this:

+
[aspymap:~/sarra_test_output]$ sr_poll foreground pollingest.conf
+2018-10-03 15:24:58,611 [INFO] poll_email_ingest init
+2018-10-03 15:24:58,617 [INFO] sr_poll pollingest startup
+2018-10-03 15:24:58,617 [INFO] log settings start for sr_poll (version: 2.18.07b3):
+2018-10-03 15:24:58,617 [INFO]  inflight=unspecified events=create|delete|link|modify use_pika=False
+2018-10-03 15:24:58,617 [INFO]  suppress_duplicates=1200 retry_mode=True retry_ttl=Nonems
+2018-10-03 15:24:58,617 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-10-03 15:24:58,617 [INFO]  heartbeat=300 default_mode=400 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-10-03 15:24:58,617 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-10-03 15:24:58,617 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-10-03 15:24:58,617 [INFO]  post_base_dir=None post_base_url=pops://dfsghfgsdfg24@hotmail.com@outlook.office365.com:995/ sum=z,d blocksize=209715200
+2018-10-03 15:24:58,617 [INFO]  Plugins configured:
+2018-10-03 15:24:58,617 [INFO]          on_line: Line_Mode
+2018-10-03 15:24:58,617 [INFO]          on_html_page: Html_parser
+2018-10-03 15:24:58,617 [INFO]          do_poll: Fetcher
+2018-10-03 15:24:58,617 [INFO]          on_message:
+2018-10-03 15:24:58,617 [INFO]          on_part:
+2018-10-03 15:24:58,618 [INFO]          on_file: File_Log
+2018-10-03 15:24:58,618 [INFO]          on_post: Post_Log
+2018-10-03 15:24:58,618 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse
+2018-10-03 15:24:58,618 [INFO]          on_report:
+2018-10-03 15:24:58,618 [INFO]          on_start:
+2018-10-03 15:24:58,618 [INFO]          on_stop:
+2018-10-03 15:24:58,618 [INFO] log_settings end.
+2018-10-03 15:24:58,621 [INFO] Output AMQP broker(localhost) user(tsource) vhost(/)
+2018-10-03 15:24:58,621 [INFO] Output AMQP exchange(xs_tsource)
+2018-10-03 15:24:58,621 [INFO] declaring exchange xs_tsource (tsource@localhost)
+2018-10-03 15:24:59,452 [INFO] post_log notice=20181003192459.452392 pops://dfsghfgsdfg24@hotmail.com@outlook.office365.com:995/ sarra%20demo20181003_15241538594699_452125 headers={'parts': '1,1,1,0,0', 'sum': 'z,d', 'from_cluster': 'localhost', 'to_clusters': 'ALL'}
+^C2018-10-03 15:25:00,355 [INFO] signal stop (SIGINT)
+2018-10-03 15:25:00,355 [INFO] sr_poll stop
+
+
+
+
+
+

Downloading

+

The email messages, once retrieved, are formatted in raw Multipurpose Internet Mail Extensions (MIME) 1.0 format, +as indicated in the first header of the file. The metadata of the email is conveyed in a series of headers, one +per line, in name:value format. This can be parsed for attachments, message bodies, encoding methods, etc. A +do_download plugin can implement the retrieval of the message to output to a file by registering the +protocol in a separate module, as in the do_poll plugin. Once a message is received with the user/host +advertised, it can then connect to the mail server using the pollUrl and the credentials as specified +in ~/.config/sarra/credentials.conf and retrieve the message locally. An example of a plugin that does this +can be found under plugins as download_email_ingest.py.

+
+

Decoding Contents

+

Once the email message is downloaded, an on_file plugin can parse the MIME formatted file and extract the attachment, usually denoted by the Content-Disposition header, or the message body/subject/address fields, to be saved as a +new file for further data refining. An example of a plugin that does this can be found under plugins as +file_email_decode.py. +A sample configuration incorporating this type of file processing is included under examples as +downloademail.conf. +Once the environment variables have been provided and the rabbitmq server is set up correctly, open a new +terminal and run:

+
[aspymap~]$ sr_subscribe foreground downloademail.conf
+
+
+

If everything was supplied correctly, the output should look something like this:

+
[aspymap:~/sarra_output_test]$ sr_subscribe foreground downloademail.conf
+2018-10-03 15:24:57,153 [INFO] download_email_ingest init
+2018-10-03 15:24:57,159 [INFO] sr_subscribe downloademail start
+2018-10-03 15:24:57,159 [INFO] log settings start for sr_subscribe (version: 2.18.07b3):
+2018-10-03 15:24:57,159 [INFO]  inflight=.tmp events=create|delete|link|modify use_pika=False
+2018-10-03 15:24:57,159 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=300000ms
+2018-10-03 15:24:57,159 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-10-03 15:24:57,159 [INFO]  heartbeat=300 default_mode=000 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-10-03 15:24:57,159 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-10-03 15:24:57,159 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-10-03 15:24:57,159 [INFO]  Plugins configured:
+2018-10-03 15:24:57,159 [INFO]          do_download: Fetcher
+2018-10-03 15:24:57,159 [INFO]          do_get     :
+2018-10-03 15:24:57,159 [INFO]          on_message:
+2018-10-03 15:24:57,159 [INFO]          on_part:
+2018-10-03 15:24:57,159 [INFO]          on_file: File_Log Decoder
+2018-10-03 15:24:57,159 [INFO]          on_post: Post_Log
+2018-10-03 15:24:57,159 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse RETRY
+2018-10-03 15:24:57,159 [INFO]          on_report:
+2018-10-03 15:24:57,159 [INFO]          on_start:
+2018-10-03 15:24:57,159 [INFO]          on_stop:
+2018-10-03 15:24:57,159 [INFO] log_settings end.
+2018-10-03 15:24:57,159 [INFO] sr_subscribe run
+2018-10-03 15:24:57,160 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-10-03 15:24:57,164 [INFO] Binding queue q_tsource.sr_subscribe.downloademail.64168876.31529683 with key v02.post.# from exchange xs_tsource on broker amqp://tsource@localhost/
+2018-10-03 15:24:57,166 [INFO] reading from to tsource@localhost, exchange: xs_tsource
+2018-10-03 15:24:57,167 [INFO] report to tsource@localhost, exchange: xs_tsource
+2018-10-03 15:24:57,167 [INFO] sr_retry on_heartbeat
+2018-10-03 15:24:57,172 [INFO] No retry in list
+2018-10-03 15:24:57,173 [INFO] sr_retry on_heartbeat elapse 0.006333
+2018-10-03 15:25:00,497 [INFO] download_email_ingest downloaded file: /home/ib/dads/map/.cache/sarra/sarra_doc_test/sarra demo20181003_15241538594699_452125
+2018-10-03 15:25:00,500 [INFO] file_log downloaded to: /home/ib/dads/map/.cache/sarra/sarra_doc_test/sarra demo20181003_15241538594699_452125
+^C2018-10-03 15:25:03,675 [INFO] signal stop (SIGINT)
+2018-10-03 15:25:03,675 [INFO] sr_subscribe stop
+
+
+
+
+
+

Use Case

+

The email ingest plugins were developed for the short burst data use case, where information would +arrive in message attachments. Previously the emails were downloaded with a fetchmail script, and a +cronjob would run every once in a while to detect and decode new files and their email attachments, +to be used for further data processing. Sarracenia now takes care of all the steps of data routing, +and allows this process to be more parallelizable.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/FlowCallbacks.html b/How2Guides/FlowCallbacks.html new file mode 100644 index 000000000..7033673b2 --- /dev/null +++ b/How2Guides/FlowCallbacks.html @@ -0,0 +1,689 @@ + + + + + + + Writing FlowCallback Plugins — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Writing FlowCallback Plugins

+

All Sarracenia components implement the Flow algorithm. +Sarracenia’s main class is sarracenia.flow and the a great +deal of core functionality is implemented using the class created to add +custom processing to a flow, the flowcb (flow callback) class.

+

For a detailed discussion of the flow algorithm itself, have a look +at Concepts manual. For any flow, one can +add custom processing at a variety of times during processing by sub-classing +the sarracenia.flowcb class.

+

Briefly, the algorithm has the following steps:

+
    +
  • gather – passively collect notification messages to be processed.

  • +
  • poll – actively collect notification messages to be processed.

  • +
  • filter – apply accept/reject regular expression matches to the notification message list.

    +
      +
    • after_accept callback entry point

    • +
    +
  • +
  • work – perform a transfer or transformation on a file.

    +
      +
    • after_work callback entry point

    • +
    +
  • +
  • post – post the result of the work done for the next step.

  • +
+

A flow callback, is a python class built with routines named to +indicate when the programmer wants them to be called.

+

There are a number of examples of flowcallback classes included +with Sarracenia, given in the +Flowcallback Reference +that can be used as the basis for building custom ones.

+

This guide describes how to build flow callback classes from scratch.

+
+

Config File Entries to use Flow_Callbacks

+

To add a callback to a a flow, a line is added to the config file:

+
flowcb sarracenia.flowcb.log.Log
+
+
+

If you follow the convention, and the name of the class is a capitalized +version (Log) of the file name (log), then a shorthand is available:

+
callback log
+
+
+

The class constructor accepts a sarracenia.config.Config class object, +called options, that stores all the settings to be used by the running flow. +Options is used to override default behaviour of both flows and callbacks. +The argument to the flowcb is a standard python class that needs to be +in the normal python path for python import, and the last element +is the name of the class in within the file that needs to be instantiated +as a flowcb instance.

+

a setting for a callback is declared as follows:

+
set sarracenia.flowcb.filter.log.Log.logLevel debug
+
+
+

(the prefix for the setting matches the type hierarchy in flowCallback)

+

when the constructor for the callback is called, it’s options +argument will contain:

+
options.logLevel = 'debug'
+
+
+

If no module specific override is present, then the more global +setting is used.

+
+
+

Worklists

+

Besides option, the other main argument to after_accept and after_work callback +routines is the worklist. The worklist is given to entry points occurring during notification message +processing, and is a number of worklists of notification messages:

+
worklist.incoming --> notification messages to process (either new or retries.)
+worklist.ok       --> successfully processed
+worklist.rejected --> notification messages to not be further processed.
+worklist.failed   --> notification messages for which processing failed.
+                      failed notification messages will be retried.
+
+
+

Initially, all notification messages are placed in worklists.incoming. +if a plugin decides:

+
    +
  • a notification message is not relevant, moved it to the rejected worklist.

  • +
  • a no further processing of the notification message is needed, move it to ok worklist.

  • +
  • an operation failed and it should be retried later, move to failed worklist.

  • +
+

Do not remove from all lists, only move notification messages between the worklists. +it is necessary to put rejected notification messages in the appropriate worklist +so that they are acknowledged as received. Messages can only removed +after the acknowledgement has been taken care of.

+
+
+

Logging

+

Python has great built-in logging, and once has to just use the module +in a normal, pythonic way, with:

+
import logging
+
+
+

After all imports in your python source file, then define a logger +for the source file:

+
logger = logging.getLogger(__name__)
+
+
+

As is normal with the Python logging module, notification messages can then +be posted to the log:

+
logger.debug('got here')
+
+
+

Each notification message in the log will be prefixed with the class and routine +emitting the log notification message, as well as the date/time.

+

One can also implement a per module override to log levels. +See sarracenia/moth/amqp.py as and example. For that module, +the notification message logging level is upped to warning by default. +One can override it with a config file setting:

+
set sarracenia.moth.amqp.AMQP.logLevel info
+
+
+

in the __init__(self,options) function of the callback, +include the lines:

+
me = "%s.%s" % ( __class__.__module__ , __class__.__name__ )
+if 'logLevel' in self.o['settings'][me]:
+             logger.setLevel( self.o['logLevel'].upper() )
+
+
+
+
+

Initialization and Settings

+

The next step is declaring a class:

+
class Myclass(FlowCB):
+
+
+

as a subclass as FlowCB. The main routines in the class are entry points +that will be called at the time their name implies. If you a class is missing a +given entry point, it will just not be called. The __init__() class is used to +initialize things for the callback class:

+
def __init__(self, options):
+
+    super().__init__(options)
+
+    logging.basicConfig(format=self.o.logFormat,
+                        level=getattr(logging, self.o.logLevel.upper()))
+    logger.setLevel(getattr(logging, self.o.logLevel.upper()))
+
+    self.o.add_option( 'myoption', 'str', 'usuallythis')
+
+
+

The logging setup lines in __init__ allow setting a specific logging level +for this flowCallback class. Once the logging boiler-plate is done, +the add_option routine to define settings to for the class. +users can include them in configuration files, just like built-in options:

+
myoption IsReallyNeeded
+
+
+

The result of such a setting is that the self.o.myoption = ‘IsReallyNeeded’. +If no value is set in the configuration, self.o.myoption will default to ‘usuallyThis’ +There are various kinds of options, where the declared type modifies the parsing:

+
'count'    integer count type.
+'duration' a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)
+           modified by a unit suffix ( m-minute, h-hour, w-week )
+'flag'     boolean (True/False) option.
+'list'     a list of string values, each succeeding occurrence catenates to the total.
+           all v2 plugin options are declared of type list.
+'size'     integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.
+'str'      an arbitrary string value, as will all of the above types, each
+           succeeding occurrence overrides the previous one.
+
+
+
+
+

Entry Points

+

Other entry_points, extracted from sarracenia/flowcb/__init__.py

+
 def ack(self,messagelist):
+     Task: acknowledge notification messages from a gather source.
+
+ """
+   application of the accept/reject clauses happens here, so after_accept callbacks
+   run on a filtered set of notification messages.
+
+ """
+
+ def after_accept(self,worklist):
+     """
+      Task: just after notification messages go through accept/reject masks,
+            operate on worklist.incoming to help decide which notification messages to process further.
+            and move notification messages to worklist.rejected to prevent further processing.
+            do not delete any notification messages, only move between worklists.
+     """
+
+ def after_work(self,worklist):
+     Task: operate on worklist.ok (files which have arrived.)
+
+ def download(self,msg) -> bool::
+
+      Task: looking at msg['new_dir'], msg['new_file'], msg['new_inflight_file']
+            and the self.o options perform a download of a single file.
+            return True on a successful transfer, False otherwise.
+
+
+ def gather(self):
+     Task: gather notification messages from a source... return either:
+           * a list of notification messages, or
+           * a tuple, (bool:keep_going, list of messages)
+           * to curtail further gathers in this cycle.
+
+     return []
+
+ def metrics_report(self) -> dict:
+
+     Return a dictionary of metrics. Example: number of messages remaining in retry queues.
+
+ def on_housekeeping(self):
+      do periodic processing.
+
+ def on_html_page(self,page):
+      Task: modify an html page.
+
+ def on_line(self,line):
+      used in FTP polls, because servers have different formats, modify to canonical use.
+
+      Task: return modified line.
+
+ def on_start(self):
+      After the connection is established with the broker and things are instantiated, but
+      before any notification message transfer occurs.
+
+ def on_stop(self):
+      cleanup processing when stopping.
+
+ def poll(self):
+     Task: build worklist.incoming, a form of gather()
+
+ def post(self,worklist):
+      Task: operate on worklist.ok, and worklist.failed. modifies them appropriately.
+            notification message acknowledgement has already occurred before they are called.
+
+def send(self,msg) -> bool::
+
+      Task: looking at msg['new_dir'], msg['new_file'], and the self.o options perform a transfer
+            of a single file.
+            return True on a successful transfer, False otherwise.
+
+      This replaces built-in send functionality for individual files.
+
+ def stop_requested(self):
+      Pre-warn a flowcb that a stop has been requested, allowing processing to wrap up
+      before the full stop happens.
+
+
+
+
+

new_* Fields

+

During processing of notification messages, the original standard field values are generally left un-changed (as-read in.) +To change fields of notification messages forwarded to downstream consumers, one modifies new_field instead +of the one from the message, as the original is necessary for successful upstream retrieval:

+
    +
  • msg[‘new_baseUrl’] … baseUrl to pass to downstream consumers.

  • +
  • msg[‘new_dir’] … the directory into which a file will be downloaded or sent.

  • +
  • msg[‘new_file’] …. final name of the file to write.

  • +
  • msg[‘new_inflight_path’] … calculated name of the temporary file to be written before renaming to msg[‘new_file’] … do not set manually.

  • +
  • msg[‘new_relPath’] … calculated from ‘new_baseUrl’, ‘post_baseDir’, ‘new_dir’, ‘new_file’ … do not set manually.

  • +
  • msg[‘post_version’] … calculated the encoding format of the message to post (from settings)

  • +
  • msg[‘new_subtopic’] … the subtopic hierarchy that will be encoded in the notification message for downstream consumers.

  • +
+
+
+

Override Fields

+

To change processing of messages, one can set overrides to change how built-in algorithms work. +For example:

+
    +
  • msg[‘nodupe_override’] = { ‘key’: …, ‘path’: … } changes how the duplicate detection operates.

  • +
  • msg[‘topic’] … defines the topic of a published message (instead of being calculated from other fields.)

  • +
  • msg[‘exchangeSplitOverride’] = int … changes how post_ExchangeSplit chooses among multiple postExchanges.

  • +
+
+
+

Customizing Duplicate Suppression

+

The built-in processing for duplicates is to use the identity field as a key, and store the path as the value. +So if a file is received with the same key, and the path is already present, then it is considered a duplicate +and dropped.

+

In some cases, we may want only the file name to be used, so if any file with the same name is received twice, +regardless of content, then it should be considered a duplicate and dropped. This is useful when multiple systems +are producing the same products, but they are not bitwise identical. The built-in flowcb that implements +that functionality is below:

+
import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+class Name(FlowCB):
+    """
+      Override the the comparison so that files with the same name,
+      regardless of what directory they are in, are considered the same.
+      This is useful when receiving data from two different sources (two different trees)
+      and winnowing between them.
+    """
+    def after_accept(self, worklist):
+        for m in worklist.incoming:
+            if not 'nodupe_override' in m:
+                m['_deleteOnPost'] \|= set(['nodupe_override'])
+                m['nodupe_override'] = {}
+
+            m['nodupe_override']['path'] = m['relPath'].split('/')[-1]
+            m['nodupe_override']['key'] = m['relPath'].split('/')[-1]
+
+
+
+
+

Customizing post_exchangeSplit

+

The exchangeSplit function allows a single flow to send outputs to different exchanges, +numbered 1…n to provide load distribution. The built-in processing does this in a +fixed way based on the hash of the identify field. The purpose of exchangeSplit is to +allow a common set of downstream paths to receive a subset of the total flow, and for +products with similar “routing” to land on the same downstream node. For example, a file +with a given checksum, for winnowing to work, has to land on the same downstream node.

+

It could be that, rather than using a checksum, one would prefer to use some other +method to decide which exchange is used:

+
import logging
+from sarracenia.flowcb import FlowCB
+import hashlib
+logger = logging.getLogger(__name__)
+
+
+class Distbydir(FlowCB):
+  """
+    Override the use of the identity field so that products can be grouped by directory in the relPath
+    This ensures that all products received from the same directory get posted to the same
+    exchange when post_exchangeSplit is active.
+  """
+  def after_accept(self, worklist):
+      for m in worklist.incoming:
+          m['_deleteOnPost'] |= set(['exchangeSplitOverride'])
+          m['exchangeSplitOverride'] = int(hashlib.md5(m['relPath'].split(os.sep)[-2]).hexdigest()[0])
+
+
+

This routine sets the exchangeSplitOverride field, which needs to be an integer +that will be used to pick which of the n exchanges in the post_exchangeSplit +exchanges defined. This routine calculates a checksum of the directory +containing the file and then converts the first character of that checksum +to an integer. If the directory is the same, the exchange chosen will be the same.

+
+
+

Sample Flowcb Sub-Class

+

This is an example callback class file (gts2wis2.py) that accepts files whose +names begin with AHL’s, and renames the directory tree to a different standard, +the evolving one for the WMO WIS 2.0 (for more information on that module: +https://github.com/wmo-im/GTStoWIS2)

+
import json
+import logging
+import os.path
+
+from sarracenia.flowcb import FlowCB
+import GTStoWIS2
+
+logger = logging.getLogger(__name__)
+
+
+class GTS2WIS2(FlowCB):
+
+  def __init__(self, options):
+
+      super().__init__(options,logger)
+      self.topic_builder=GTStoWIS2.GTStoWIS2()
+
+  def after_accept(self, worklist):
+
+      new_incoming=[]
+
+      for msg in worklist.incoming:
+
+          # fix file name suffix.
+          type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )
+          tpfx=msg['subtopic']
+
+          # input has relpath=/YYYYMMDD/... + pubTime
+          # need to move the date from relPath to BaseDir, adding the T hour from pubTime.
+          try:
+              new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]
+              t='.'.join(tpfx[0:2])+'.'+new_baseSubDir
+              new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir
+              new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])
+              msg['new_dir'] = new_baseDir + os.sep + new_relDir
+              msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, msg['new_file'] )
+
+          except Exception as ex:
+              logger.error( "skipped" , exc_info=True )
+              worklist.failed.append(msg)
+              continue
+
+          msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )
+          new_incoming.append(msg)
+
+      worklist.incoming=new_incoming
+
+
+

The after_accept routine is one of the two most common ones in use.

+

The after_accept routine has an outer loop that cycles through the entire +list of incoming notification messages. The normal processing is that is builds a new list of +incoming notification messages, appending all the rejected ones to worklist.failed. The +list is just a list of notification messages, where each notification message is a python dictionary with +all the fields stored in a v03 format notification message. In the notification message there are, +for example, baseURL and relPath fields:

+
    +
  • baseURL - the baseURL of the resource from which a file would be obtained.

  • +
  • relPath - the relative path to append to the baseURL to get the complete download URL.

  • +
+

This is happenning before transfer (download or sent, or processing) of the file +has occurred, so one can change the behaviour by modifying fields in the notification message. +Normally, the download paths (called new_dir, and new_file) will reflect the intent +to mirror the original source tree. so if you have a/b/c.txt on the source tree, and +are downloading in to directory mine on the local system, the new_dir would be +mine/a/b and new_file would be c.txt.

+

The plugin above changes the layout of the files that are to be downloaded, based on the +GTStoWIS class, which prescribes a different +directory tree on output. There are a lot of fields to update when changing file +placement, so best to use:

+
msg.updatePaths( self.o, new_dir, new_file )
+
+
+

to update all necessary fields in the notification message properly. It will update +‘new_baseURL’, ‘new_relPath’, ‘new_subtopic’ for use when posting.

+

The try/except part of the routine deals with the case that, should +a file arrive with a name from which a topic tree cannot be built, then an exception +may occur, and the notification message is added to the failed worklist, and will not be +processed by later plugins.

+
+
+

Download Renaming

+

Sometimes the URL used to obtain data from a server isn’t the same as the name +one wants to assign to the downloaded result. This occurs often when polling upstream +arbitrary web services. For such cases the message format defines the retrievePath, or +retrieval path, used as follows:

+ +

Standard subscribers will download using retrievePath but assign the download path using relPath. +When forwarding after download, the retrievePath should often be removed (to avoid downstream clients +pulling from the original source instead of the downloaded copy.)

+

While the above is a preferred way of defining messages where the download will have a different +name from the upstream source, a second method is available, the rename used as follows:

+
    +
  • msg[‘rename’] = alternate relPath … download it to here instead of using relPath.

  • +
+

again, once downloaded, the rename header should be removed from the message prior to +forwarding to downstream clients. the relPath needs to be adjusted.

+

Note that both of these methods work the same for senders as well. The term ‘download’ is +used for simplicity.

+
+
+

Web Sites with non-standard file listings

+

The poll/nasa_mls

+
+
+

Other Examples

+

Subclassing of Sarracenia.flowcb is used internally to do a lot of core work. +It’s a good idea to look at the sarracenia source code itself. For example:

+
    +
  • sr3 list fcb is a command to list all the callback classes that are +included in the metpx-sr3 package.

  • +
  • sarracenia.flowcb have a look at the __init__.py file in there, which +provides this information on a more programmatically succinct format.

  • +
  • sarracenia.flowcb.gather.file.File is a class that implements +file posting and directory watching, in the sense of a callback that +implements the gather entry point, by reading a file system and building a +list of notification messages for processing.

  • +
  • sarracenia.flowcb.gather.message.Message is a class that implements +reception of notification messages from message queue protocol flows.

  • +
  • sarracenia.flowcb.nodupe.NoDupe This modules removes duplicates from message +flows based on Identity checksums.

  • +
  • sarracenia.flowcb.post.message.Message is a class that implements posting +notification messages to Message queue protocol flows

  • +
  • sarracenia.flowcb.retry.Retry when the transfer of a file fails, +Sarracenia needs to persist the relevant notification message to a state file for +a later time when it can be tried again. This class implements +that functionality.

  • +
+
+
+

Modifying Files in Flight

+

The sarracenia.transfer class has an on_data entry point:

+
 def on_data(self, chunk) -> bytes:
+     """
+         transform data as it is being read.
+         Given a buffer, return the transformed buffer.
+         Checksum calculation is based on pre transformation... likely need
+         a post transformation value as well.
+     """
+     # modify the chunk in this body...
+     return chunk
+
+def registered_as():
+     return ['scr' ]
+
+# copied from sarracenia.transfer.https
+
+def connect(self):
+
+     if self.connected: self.close()
+
+     self.connected = False
+     self.sendTo = self.o.sendTo.replace('scr', 'https', 1)
+     self.timeout = self.o.timeout
+
+     if not self.credentials(): return False
+
+     return True
+
+
+

to perform inflight data modification, one can sub-class the relevant transfer class. +Such a class (scr - strip carriage returns) can be added by putting an import in the configuration +file:

+
import scr.py
+
+
+

then messages where the retrieval url is set to use the scr retrieval scheme will use this +custome transfer protocol.

+
+
+

Subclassing Flow

+

If none of the built-in components ( poll, post, sarra, shovel, subscribe, watch, winnow ) have the +behaviour desired, one can build a custom component to do the right thing by sub-classing flow.

+

Copy one of the flow sub-classes from the source code, and modify to taste. In the configuration +file, add the line:

+
flowMain myComponent
+
+
+

to have the flow use the new component.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/Hydro_Examples.html b/How2Guides/Hydro_Examples.html new file mode 100644 index 000000000..667e5c08c --- /dev/null +++ b/How2Guides/Hydro_Examples.html @@ -0,0 +1,363 @@ + + + + + + + Using Plugins to Grab Hydrometric Data (v2) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Using Plugins to Grab Hydrometric Data (v2)

+

Several different environmental data websites use APIs to communicate data. In order to advertise the +availability of new files and integrate them seamlessly into the Sarracenia stack, a few plugins are +needed to extend the polling functionality.

+
+

Polling Protocols Natively Supported

+

Out of the box, Sarracenia supports polling of HTTP/HTTPS and SFTP/FTP sources where the filename +gets appended to the end of the base URL. For example, if you’re trying to access the water level +data of Ghost Lake Reservoir near Cochrane in Alberta, which can be accessed by navigating to +http://environment.alberta.ca/apps/Basins/data/figures/river/abrivers/stationdata/L_HG_05BE005_table.json, +the base URL in this case would be considered the http://environment.alberta.ca/apps/Basins/data/figures/river/abrivers/stationdata/ part, and the filename the L_HG_05BE005_table.json part. Since the base URL doesn’t +contain a nice directory with all the JSON files, if you wanted to check if new water level data has +been added at the locator above, since it’s a JSON file, you could check the last-modified header to +see if it has been modified since you last polled. From there, you would need to set the new_baseurl to the +first part, and the new_file set to the second, and an sr_subscribe instance would know how to assemble +them to locate the file and download it.

+
+

Extending Polling Protocols

+

If the data source doesn’t abide to this convention (see NOAA CO-OPS API and USGS Instantaneous Values +Web Service for examples of two data sources that don’t), a module registered_as can be included at +the bottom of a plugin file to define the list of protocols being extended or implemented:

+
def registered_as(self):
+        return ['http','https']
+
+
+

It would then overload the method of transfer and use the one as described in the plugin.

+
+
+
+

Examples of Integrating APIs into Plugins

+
+

NOAA CO-OPS API

+

The National Oceanic and Atmospheric Administration Tides and Currents Department releases their CO-OPS +station observations and predictions data through a GET RESTful web service, available at the NOAA Tides +and Currents website. For example, if you want to access the +water temperature data from the last hour in Honolulu, you can navigate to https://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv. +A new observation gets recorded every six minutes, so if you wanted to advertise solely new data through +Sarracenia, you would configure an sr_poll instance to connect to the API, set a one hour scheduled_interval , and build +it a GET request to announce every time it woke up (this operates under the potentially misguided assumption +that the data source is maintaining their end of the bargain). To download this shiny new file, you would connect +an sr_subscribe to the same exchange it got announced on, and it would retrieve the URL, which a do_download +plugin could then take and download. An example polling plugin which grabs all water temperature and water level +data from the last hour, from all CO-OPS stations, and publishes them is included under plugins as +poll_noaa.py. +A corresponding do_download plugin for a sarra instance to download this file is included +as download_noaa.py +. Example configurations for both sr_poll and sr_subscribe are included under +examples, named pollnoaa.conf +and subnoaa.conf. +To run, add both plugins and configurations using the add action, edit the proper variables in the +config (the flowbroker, sendTo among others. If running off a local RabbitMQ server, some of the +documentation under doc/Dev.rst +on how to set up the server might be useful). If everything was configured correctly, the output should +look something like this:

+
[aspymap:~]$ sr_poll foreground pollnoaa.conf
+2018-09-26 15:26:57,704 [INFO] sr_poll pollnoaa startup
+2018-09-26 15:26:57,704 [INFO] log settings start for sr_poll (version: 2.18.07b3):
+2018-09-26 15:26:57,704 [INFO]  inflight=unspecified events=create|delete|link|modify use_pika=False
+2018-09-26 15:26:57,704 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=Nonems
+2018-09-26 15:26:57,704 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-09-26 15:26:57,705 [INFO]  heartbeat=300 default_mode=400 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-09-26 15:26:57,705 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-09-26 15:26:57,705 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-09-26 15:26:57,705 [INFO]  post_base_dir=None post_base_url=http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station={0:}&product={1:}&units=metric&time_zone=gmt&application=web_services&format=csv/ sum=z,d blocksize=209715200
+2018-09-26 15:26:57,705 [INFO]  Plugins configured:
+2018-09-26 15:26:57,705 [INFO]          on_line: Line_Mode
+2018-09-26 15:26:57,705 [INFO]          on_html_page: Html_parser
+2018-09-26 15:26:57,705 [INFO]          do_poll: NOAAPoller
+2018-09-26 15:26:57,705 [INFO]          on_message:
+2018-09-26 15:26:57,705 [INFO]          on_part:
+2018-09-26 15:26:57,705 [INFO]          on_file: File_Log
+2018-09-26 15:26:57,705 [INFO]          on_post: Post_Log
+2018-09-26 15:26:57,705 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse
+2018-09-26 15:26:57,705 [INFO]          on_report:
+2018-09-26 15:26:57,705 [INFO]          on_start:
+2018-09-26 15:26:57,706 [INFO]          on_stop:
+2018-09-26 15:26:57,706 [INFO] log_settings end.
+2018-09-26 15:26:57,709 [INFO] Output AMQP broker(localhost) user(tsource) vhost(/)
+2018-09-26 15:26:57,710 [INFO] Output AMQP exchange(xs_tsource)
+2018-09-26 15:26:57,710 [INFO] declaring exchange xs_tsource (tsource@localhost)
+2018-09-26 15:26:58,403 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv
+2018-09-26 15:26:58,403 [INFO] post_log notice=20180926192658.403634 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv CO-OPS__1611400__wt.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,554 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND
+2018-09-26 15:26:58,554 [INFO] post_log notice=20180926192658.554364 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND CO-OPS__1611400__wl.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,691 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv
+2018-09-26 15:26:58,691 [INFO] post_log notice=20180926192658.691466 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv CO-OPS__1612340__wt.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,833 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND
+2018-09-26 15:26:58,834 [INFO] post_log notice=20180926192658.833992 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND CO-OPS__1612340__wl.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+^C2018-09-26 15:26:58,965 [INFO] signal stop (SIGINT)
+2018-09-26 15:26:58,965 [INFO] sr_poll stop
+
+
+

for the polling and:

+
[aspymap:~]$ sr_subscribe foreground subnoaa.conf
+2018-09-26 15:26:53,473 [INFO] sr_subscribe subnoaa start
+2018-09-26 15:26:53,473 [INFO] log settings start for sr_subscribe (version: 2.18.07b3):
+2018-09-26 15:26:53,473 [INFO]  inflight=.tmp events=create|delete|link|modify use_pika=False
+2018-09-26 15:26:53,473 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=300000ms
+2018-09-26 15:26:53,473 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-09-26 15:26:53,473 [INFO]  heartbeat=300 default_mode=000 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-09-26 15:26:53,473 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-09-26 15:26:53,473 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=False
+2018-09-26 15:26:53,473 [INFO]  Plugins configured:
+2018-09-26 15:26:53,473 [INFO]          do_download: BaseURLDownloader
+2018-09-26 15:26:53,473 [INFO]          do_get     :
+2018-09-26 15:26:53,473 [INFO]          on_message:
+2018-09-26 15:26:53,474 [INFO]          on_part:
+2018-09-26 15:26:53,474 [INFO]          on_file: File_Log
+2018-09-26 15:26:53,474 [INFO]          on_post: Post_Log
+2018-09-26 15:26:53,474 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse RETRY
+2018-09-26 15:26:53,474 [INFO]          on_report:
+2018-09-26 15:26:53,474 [INFO]          on_start:
+2018-09-26 15:26:53,474 [INFO]          on_stop:
+2018-09-26 15:26:53,474 [INFO] log_settings end.
+2018-09-26 15:26:53,474 [INFO] sr_subscribe run
+2018-09-26 15:26:53,474 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-09-26 15:26:53,478 [INFO] Binding queue q_tsource.sr_subscribe.subnoaa.90449861.55888967 with key v02.post.# from exchange xs_tsource on broker amqp://tsource@localhost/
+2018-09-26 15:26:53,480 [INFO] reading from to tsource@localhost, exchange: xs_tsource
+2018-09-26 15:26:53,480 [INFO] report suppressed
+2018-09-26 15:26:53,480 [INFO] sr_retry on_heartbeat
+2018-09-26 15:26:53,486 [INFO] No retry in list
+2018-09-26 15:26:53,488 [INFO] sr_retry on_heartbeat elapse 0.007632
+2018-09-26 15:26:58,751 [INFO] download_noaa: file noaa_20180926_1926_1611400_TP.csv
+2018-09-26 15:26:58,751 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1611400__wt.csv
+2018-09-26 15:26:58,888 [INFO] download_noaa: file noaa_20180926_1926_1611400_WL.csv
+2018-09-26 15:26:58,889 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1611400__wl.csv
+2018-09-26 15:26:59,026 [INFO] download_noaa: file noaa_20180926_1926_1612340_TP.csv
+2018-09-26 15:26:59,027 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1612340__wt.csv
+2018-09-26 15:26:59,170 [INFO] download_noaa: file noaa_20180926_1926_1612340_WL.csv
+2018-09-26 15:26:59,171 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1612340__wl.csv
+^C2018-09-26 15:27:00,597 [INFO] signal stop (SIGINT)
+2018-09-26 15:27:00,597 [INFO] sr_subscribe stop
+
+
+

for the downloading.

+
+
+

SHC SOAP Web Service

+

A SOAP web service (Simple Object Access Protocol) uses an XML-based messaging system to supply requested +data over a network. The client can specify parameters for a supported operation (for example a search) on +the web service, denoted with a wdsl file extension, and the server will return an XML-formatted SOAP +response. The Service Hydrographique du Canada (SHC) uses this web service as an API to get hydrometric +data depending on the parameters sent. It only supports one operation, search, which accepts the following +parameters: dataName, latitudeMin, latitudeMax, longitudeMin, longitudeMax, depthMin, depthMax, dateMin, +dateMax, start, end, sizeMax, metadata, metadataSelection, order. For example, a search will return all the +water level data available from Acadia Cove in Nunavut on September 1st, 2018 if your search contains +the following parameters: ‘wl’, 40.0, 85.0, -145.0, -50.0, 0.0, 0.0, ‘2018-09-01 00:00:00’, +‘2018-09-01 23:59:59’, 1, 1000, ‘true’, ‘station_id=4170, ‘asc’. The response can then be converted into a +file and dumped, which can be advertised, or the parameters can be advertised themselves in the report +notice, which a sarra do_download plugin could then decipher and process the data into a file user-side. +In order to only advertise new data from SHC, a polling instance could be configured to sleep every 30 minutes, +and a do_poll plugin could set the start-end range to the last half hour before forming the request. +Each request is returned with a status message confirming if it was a valid function call. The plugin could +then check the status message is ok before posting the message advertising new data to the exchange. +A do_download plugin takes these parameters passed in the message, forms a SOAP query with them, and +extracts the data/saves it to a file. Examples of plugins that do both of these steps can be found under +plugins, named poll_shc_soap.py +and download_shc_soap.py. +Example configurations for running both are included under examples, named +pollsoapshc.conf and +subsoapshc.conf.

+
+
+

USGS Instantaneous Values Web Service

+

The United States Geological Survey publishes their water data through their Instantaneous Values RESTful +Web Service, which uses HTTP GET requests to filter their data. It returns the data in XML files once +requested, and can support more than one station ID argument at a time (bulk data download). More info on +the service can be found at the water services website. +They have a long list of parameters to specify based on the type of water data you would like to retrieve as well, +which is passed through the parameterCd argument. For example, if you wanted to fetch water discharge, level, and +temperature data from the last three hours from North Fork Vermilion River near Bismarck, IL, you would use +the following URL: +https://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site=03338780&period=PT3H&parameterCd=00060,00065,00011. +A list of parameter codes to use to tailor your results can be found here. +The plugins for any GET web service can be generalized for use, so the plugins used for the NOAA CO-OPS API +can be reused in this context as well. By default, the station IDs to pass are different, as well as the +method of passing them, so the plugin code that determines which station IDs to use differs, but the method +conceptually is still the same. You would pass a generalized version of the URL in as the sendTo in the +config, e.g. https://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site={0}&period=PT3H&parameterCd=00060,00065,00011 +and in the plugin you would replace the ‘{0}’ (Python makes this easy with string formatting) with the sites +you’re interested in, and if any other parameters need to be varied they can be replaced in a similar way. +If a station site ID file wasn’t passed as a plugin config option, then the plugin defaults to grabbing all +the registered site IDs from the USGS website. +The IV Web Service supports queries with multiple site IDs specified (comma-separated). If the plugin option +poll_usgs_nb_stn was specified to the chunk size in the config, it’ll take groups of stations’ data based on +the number passed (this reduces web requests and speeds up the data collection if collecting in bulk).

+

To run this example, the configs and plugins can be found under plugins +(poll_usgs.py +and download_usgs.py) +and examples (pollusgs.conf +and subusgs.conf).

+
+
+
+

Use Case

+

The hydrometric plugins were developed for the Environment Canada canhys use case, where files containing +station metadata would be used as input to gather the hydrometric data. Each plugin also works by generating +all valid station IDs from the water authority itself and plugging those inputs in. This alternative option can be +toggled by omitting the plugin config variable that would otherwise specify the station metadata file. +The downloader plugins also rename the file according to the specific convention of this use case.

+

Most of these sources have disclaimers that this data is not quality assured, but it is gathered in soft +realtime (advertised seconds/minutes from when it was recorded).

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/UPGRADING.html b/How2Guides/UPGRADING.html new file mode 100644 index 000000000..8be42c840 --- /dev/null +++ b/How2Guides/UPGRADING.html @@ -0,0 +1,858 @@ + + + + + + + UPGRADE GUIDE — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

UPGRADE GUIDE

+

This document describes changes in behaviour to provide guidance to those upgrading +from a previous version. Sections are titled to indicate changes needed when +upgrading to that version. To upgrade across several versions, one needs to start +at the version after the one installed, and heed all notifications for interim +versions. While configuration language stability is an important +goal, on occasion changes cannot be avoided. This file does not document new +features, but only changes that cause concern during upgrades. The notices +take the form:

+
+
CHANGE

Indicates where configurations files must be changed to get the same behaviour as prior to release.

+
+
ACTION

Indicates a maintenance activity required as part of an upgrade process.

+
+
BUG

Indicates a bug serious to indicate that deployment of this version is not recommended.

+
+
NOTICE

A behaviour change that will be noticeable during upgrade, but is no cause for concern.

+
+
SHOULD

Indicates recommended interventions that are recommended, but not mandatory. If prescribed activity is not done, +the consequence is either a configuration line that has no effect (wasteful) or the application +may generate notification messages.

+
+
+

The sections in are entitled by the changes taking place at the level in question.

+
+

Installation Instructions

+

Installation Guide

+
+
+

git

+
+
+

3.0.54

+

CHANGE: sr3 sanity only restarts missing instances, not stopped ones. +this is considered more in accordance with analyst expectations (POLA)

+
+
+

3.0.53

+

CHANGE: directory option in poll will no longer be converted to path silently. +Use path explicitly instead. It is still converted when upgrading from v2 with +sr3 convert, but in v3 configurations, directory now acts as it does in all other +components as a download specifier.

+
+
+

3.0.52

+

CHANGE: Additional messageCountMax arugment to flowcb.gather() entry point. +when implementing flow callbacks for scheduled flows, or poll overrides, the +gather entry point now takes one additional argument indicating the maximum +number of messages that the routine should return.

+

To be compatible with previous versions, one can establish a default value +on the gather:

+
def gather(self, messageMaxCount=None):
+
+
+

With the default value, plugins are downward compatible. (earlier versions +will call with only self as an argument.)

+
+
+

3.0.51

+

CHANGE: Additional action argument sarracenia.config.one_config() indicating +how the configuration will be used. When used for readonly operations (status, +show, dump) the configuration should avoid filling values that should only +be defines when used. Examples have been updated appropriately.

+

CHANGE: action setting now mandatory for the sarracenia.config.finalize().

+
+
+

3.0.47

+

CHANGE: config option, strftime options, offset grammar changed: +in v2 you had ${YYYYMMDD-70m}, in sr3 it should be ${%o-70m%Y%m%d} +in 3.0.47, moved the time offset parsing to the beginning of the pattern.

+

CHANGE: default value of filename setting is now None instead of +‘WHATFN’, which reduces compatibility with Sundew, but makes behaviour +less surprising when not using/familiar with Sundew. This None setting +is the same as used by v2, so it should improve compatibility with +sarracenia v2 configurations.

+
+
+

3.0.45

+
+
CHANGE: config option: logRotateInterval units was days, is now

a time interval (seconds) like all other intervals.

+
+
+
+
+

3.0.41

+

CHANGE: v03 post format field renamed: “integrity” is now “identity”

+
+
    +
  • current version will read messsages with integrity and map them to identity.

  • +
  • current version will post with identity, so older versions will miss them.

  • +
  • https://github.com/MetPX/sarracenia/issues/703

  • +
  • metpx-sr3c >= v3.23.06 (equivalent compatible C implementation)

  • +
  • metpx-sarracenia >= v2.23.06 (equivalent v2 compatible (legacy) version.)

  • +
+
+
+
+

3.0.40

+
+
CHANGE: the default format in which messages are posted is v03, but as of this

version, to override the format, one must use post_format v02 +prior to this version, setting of post_topicPrefix was sufficient. +Now both settings are needed.

+
+
+

CHANGE: Python API breaking changes

+
+

for sarracenia.moth, now specify broker as options[‘broker’] instead of as +a separate parameter:

+

before:

+
    +
  • Moth(broker: url, options: dict, is_subsubscriber: bool) -> Config

  • +
  • pubFactory( broker, options ) -> Config

  • +
  • subFactory( broker, options ) -> Config

  • +
+

after:

+
    +
  • Moth( options: dict, is_subscribe: bool) -> Config

  • +
  • pubFactory( options ) -> Config

  • +
  • subFactory( options ) -> Config

  • +
+

sarracenia.config API:

+
+

now should call sarracenia.config.finalize() +after having set options and before being used. +This routine reconciles the settings provided and builds +some derived ones.

+
+
+
+
+

3.0.37

+
+
BUG: sr3 cleanup does not work at all.

https://github.com/MetPX/sarracenia/issues/669

+
+
+
+
+

3.0.26

+
+
CHANGE: event options (logEvents, and fileEvents) now replace previous value

used to be unioned (or’d) with previous value. now can preface +the set elements with + to get the previous behaviour. +Also - is available to remove an element from a set option. +(sr3 convert now prefixes v2 values with +)

+
+
CHANGE: fileEvents, new events present mkdir, and rmdir, some adjustment

to fileEvents settings may now be required.

+
+
+
+
+

3.0.25

+
+
CHANGE: default value for acceptUnmatched is now True for all components.

prior to this release, default was False in subscribe component, +and True for all others.

+
+
+
+
+

3.0.23

+
+
NOTICE: now prefer strftime date specification in patterns, in place of

ones inherited from Sundew. converted by sr3 convert.

+
+
CHANGE: removed please_stop_immediately option added in 3.0.22

(all components now stop more quickly, so not needed.)

+
+
+
+
+

3.0.22

+

CHANGE: destination, when used in a poll is replaced by pollUrl

+

CHANGE: destination, when used in a sender is replaced by sendTo

+
+
ACTION: replace destination settings in affected configurations.

(automatically taken care of in v2 when converting.)

+
+
NOTICE: when a file is renamed, sr3 has always only processed one of the two messages

produced to announce it, for compatibility with v2 naming. +there is now an option: v2compatRenameDoublePost in sr3 to post only a single message +when a file is renamed. This is now the default behaviour.

+
+
+
+
+

3.0.17

+
+
CHANGE: The “Vendor” string is now “MetPX” instead of “science.gc.ca”.

This affects some file placement particularly on Windows.

+
+
CHANGE: v03 notification message encoding changed: Identity checksum is now optional.

(details: https://github.com/MetPX/sarracenia/issues/547 ) +md5sum is no longer defined, replaced with none in sr3.

+
+
CHANGE: v03 notification message encoding changed for symbolic links, and file renames

and removals. There is now a ‘fileOp’ field for these dataless file operations. +The Identity sum is now used exclusively for checksums.

+
+
+
+
+

3.0.15

+
+
NOTICE: re-instating debian and windows packages by removing hard requirements for python modules

which are difficult to satisfy. From 3.0.15, dependencies are modular.

+
+
+

CHANGE: there are now four “extras” configured for pip packages for metpx-sr3.

+
+
    +
  • amqp - ability to communicate with AMQP (rabbitmq) brokers

  • +
  • mqtt - ability to communicate with MQTT brokers

  • +
  • ftppoll - ability to poll FTP servers

  • +
  • vip - enable vip (Virtual IP) settings to implement singleton processing for high availability support.

  • +
+

with pip installation, one can include all the extras via:

+
pip install metpx-sr3[all]
+
+
+

with Linux packages, install the corresponding native packages to activate the corresponding features

+

on Ubuntu, respectively:

+
apt install python3-amqp
+apt install python3-magic
+apt install python3-paramiko
+apt install python3-paho-mqtt
+apt install python3-dateparser python3-tz
+apt install python3-netifaces
+
+
+

sr3 looks for the relevant modules on startup and automatically enables support for the relevant features.

+
+
+
CHANGE: file placement of denoting disabled configurations. it used to be that

~/.config/sr3/component/x.conf would be renamed x.conf.off when disabling. +Now instead a file called ~/.cache/sr3/component/x/disabled is created. +Configuration files are no longer changed by this sort of routine intervention.

+
+
+
+
+

3.0.14

+

initial beta.

+
+
NOTICE: only pip packages currently work. No Debian packages on launchpad.net

nor any windows packages.

+
+
+
+
+

V2 to Sr3

+
+
NOTICE: Sr3 is a very deep refactor of Sarracenia. For more detail on the nature

of the changes, go here Briefly, where v2 +is an application written in python that had a small extension facility, +Sr3 is a toolkit that naturally provides an API and is far more +pythonic. Sr3 is built with less code, more maintainable code, and +supports more features, and more naturally.

+
+
CHANGE: log messages look completely different. Any log parsing will have to be reviewed.

New log format includes a prefix with process-id and the routine generating the notification message.

+
+
+

CHANGE: default message format in sr3 is v03. in v2, the default format was v2.

+
+
CHANGE: default topicPrefix and post_topicPrefix in sr3 is ‘v03’ … in v2 it

was ‘v02.post’

+
+
NOTICE: When migrating from v2 to sr3, simple configurations will mostly “just work.”

However, cases relying on user built plugins will require effort to port. +The built-in plugins provided with Sarracenia have been ported as updated +examples.

+
+
CHANGE: file placement. On Linux: ~/.cache/sarra -> ~/.cache/sr3

~/.config/sarra -> ~/.config/sr3 +Similar change on other platforms. The different placement +allows to run both v2 and sr3 at the same time on the same server.

+
+
NOTICE: to change configurations from v2 to sr3, rather than copying the file

from one directory to the other, use of the convert directive is recommended:

+
sr3 convert subscribe/mine.conf
+
+
+

will make all mechanical conversions of directive names from v2 to sr3 automatically. +only custom plugin work need to be manually ported, as described below.

+
+
NOTICE: In sr3 the winnowing or duplicate suppression algorithm (implemented by sarracenia.flowcb.nodupe.NoDupe.py)

is separate from the data source’s checksum algorithm.

+

In v2, the checksum algorithm had to be harmonized with the +data source checksum. In sr3 one can select any checksumming method, +and still customize how message key and path are selected to allow for +full customization of duplicate suppression.

+
+
CHANGE: Command line interface (CLI) is different. There is only one main entry_point: sr3.

so most invocations are different in a pattern like so:

+
sr_subscribe start config -> sr3 start subscribe/config
+
+
+

in sr3 one can specify a series of configurations to operate on in a single +command:

+
sr3 start poll/airnow subscribe/airnow sender/cmqb
+
+
+
+
CHANGE: in sr3, use – for full word options, like –config, or –broker. In v2 you

could use -config and -broker, but single dash is reserved for single character options. +This is a result of sr3 using python standard ArgParse class:

+
-config hoho.conf  -> in v2 refers to loading the hoho.conf file as a configuration.
+
+
+

In sr3, it will be interpreted as -c (config) load the onfig.conf file, and hoho.conf +is part of some subsequent option. in sr3:

+
--config hoho.conf
+
+
+

does that as intended.

+
+
+

CHANGE: sr3 poll works very differently from v2.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

v2 behaviour

sr3 behaviour

all participants in a vip poll remote always

One node (with vip) polls remote.

all participants in a vip update ls_files

nodes subscribe to the output exchange

poll builds strings to describe files

poll builds stat(2) like paramiko.SftpAttributes()

participants rely on their ls_files for state

poll uses flowcb.nodupe module like rest of sr3

file_time_limit to ignore older files

fileAgeMax

destination gives where to poll

pollUrl

directory gives remote directory to list

path used like in post and watch

need accept per directory

need only one accept

get is a sort of remote pattern filtering

accept same as used by all other components.

do_poll plugins used to override default

poll entry point in flow callbacks

do_poll used to HTTP GET periodically

flowcb.scheduled more elegant.

+

The sr3 convert function takes care of the necessary configuration changes, but plugins +need ground up rewrites, as they work completely differently.

+

All of the changes makes poll’s use of the configuration language less different than how it is +used in other components. For example, directory was confusing because it is used to determine +the source directory to be polled. In all other components it refers to the download location. +The path option replaces it, poll uses it the same post and watch do: +to denote the paths that should be observed.

+

In sr3 when vip setting is present, poll will create a queue bound to the post_broker/post_exchange +in order to see the posts done by other participants in the queue. queue naming options are therefore +useful in sr3

+
+

CHANGE: In general, underscores in options are replaced with camelCase. e.g.:

+
+

v2 loglevel -> sr3 logLevel

+

v2 options that are renamed will be understood, but an informational message will be produced on +startup. Underscore is still use for grouping purposes. Options which have changed:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

v2 Option

v3 Option

accel_scp_threshold

accelThreshold

accel_wget_threshold

accelThreshold

accept_unmatch

acceptUnmatched

accept_unmatched

acceptUnmatched

base_dir

baseDir

basedir

baseDir

baseurl

baseUrl

bind_queue

queueBind

cache

nodupe_ttl

cache_basis

nodupe_basis

caching

nodupe_ttl

chmod

permDefault

chmod_dir

permDirDefault

chmod_log

permLog

declare_exchange

exchangeDeclare

declare_queue

queueDeclare

default_dir_mode

permDirDefault

default_log_mode

permLog

default_mode

permDefault

destination

pollUrl in Poll

destination

sendTo in Sender

document_root

documentRoot

e

fileEvents

events

fileEvents

exchange_split

exchangeSplit

file_time_limit

fileAgeMax

hb_memory_baseline_file

MemoryBaseLineFile

hb_memory_max

MemoryMax

hb_memory_multiplier

MemoryMultiplier

heartbeat

housekeeping

instance

instances

ll

logLevel

logRotate

logRotateCount

logRotate_interval

logRotateInterval

log_format

logFormat

log_reject

logReject

logdays

logRotateCount

loglevel

logLevel

no_duplicates

nodupe_ttl

post_base_dir

post_baseDir

post_base_url

post_baseUrl

post_basedir

post_baseDir

post_baseurl

post_baseUrl

post_document_root

post_documentRoot

post_exchange_split

post_exchangeSplit

post_rate_limit

messageRateMax

post_topic_prefix

post_topicPrefix

preserve_mode

permCopy

preserve_time

timeCopy

queue_name

queueName

report_back

report

source_from_exchange

sourceFromExchange

sum

identity

suppress_duplicates

nodupe_ttl

suppress_duplicates_basis

nodupe_basis

topic_prefix

topicPrefix

+
+
+
CHANGE: default topic_prefix v02.post -> topicPrefix v03

may need to change configurations to override default to get +compatible configurations.

+
+
CHANGE: v2: mirror defaults to False on all components except sarra.

sr3: mirror defaults to True on all components except subscribe.

+
+
NOTICE: The most common v2 plugins are on_message, and on_file

(as per plugin and on_ directives in v2 configuration files) which can +be honoured via the v2wrapper sr3 plugin class +Many other plugins were ported, and the the configuration module +recognizes the old configuration settings and they are interpreted +in the new style. the known conversions can be viewed by starting +a python interpreter:

+
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
+[GCC 9.3.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import sarracenia.config,pprint
+>>> pp=pprint.PrettyPrinter()
+>>> pp.pprint(sarracenia.config.convert_to_v3)
+{
+ 'do_send':   {
+                'file_email':           ['flowCallback',
+                                         'sarracenia.flowcb.send.email.Email']
+              },
+ 'ls_file_index':                       ['continue'],
+ 'no_download':                         ['download',
+                                         'False'],
+ 'notify_only':                         ['download',
+                                         'False'],
+
+ 'on_message':{
+                'msg_2http':            ['flow_callback',
+                                         'sarracenia.flowcb.accept.tohttp.ToHttp'],
+                'msg_2local':           ['flow_callback',
+                                         'sarracenia.flowcb.accept.tolocal.ToLocal'],
+                'msg_2localfile':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.tolocalfile.ToLocalFile'],
+                'msg_WMO_type_suffix':  ['flow_callback',
+                                         'sarracenia.flowcb.accept.wmotypesuffix.WmoTypeSuffix'],
+                'msg_by_source':        ['continue'],
+                'msg_by_user':          ['continue'],
+                'msg_delay':            ['flow_callback',
+                                         'sarracenia.flowcb.accept.messagedelay.MessageDelay'],
+                'msg_delete':           ['flow_callback',
+                                         'sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles'],
+                'msg_download':         ['continue'],
+                'msg_download_baseurl': ['flow_callback',
+                                         'sarracenia.flowcb.accept.downloadbaseurl.DownloadBaseUrl'],
+                'msg_dump':             ['continue'],
+                'msg_fdelay':           ['continue'],
+                'msg_from_cluster':     ['continue'],
+                'msg_gts2wistopic':     ['continue'],
+                'msg_hour_tree':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.hourtree.HourTree'],
+                'msg_http_to_https':    ['flow_callback',
+                                         'sarracenia.flowcb.accept.httptohttps.HttpToHttps'],
+                'msg_log':              ['logEvents',
+                                         'after_accept'],
+                'msg_overwrite_sum':    ['continue'],
+                'msg_print_lag':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.printlag.PrintLag'],
+                'msg_rawlog':           ['logEvents', 'after_accept'],
+                'msg_rename4jicc':      ['flow_callback',
+                                         'sarracenia.flowcb.accept.rename4jicc.Rename4Jicc'],
+                'msg_rename_dmf':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamedmf.RenameDMF'],
+                'msg_rename_whatfn':    ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamewhatfn.RenameWhatFn'],
+                'msg_renamer':          ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamer.Renamer'],
+                'msg_save':             ['flow_callback',
+                                         'sarracenia.flowcb.accept.save.Save'],
+                'msg_skip_old':         ['flow_callback',
+                                         'sarracenia.flowcb.accept.skipold.SkipOld'],
+                'msg_speedo':           ['flow_callback',
+                                         'sarracenia.flowcb.accept.speedo.Speedo'],
+                'msg_stdfiles':         ['continue'],
+                'msg_stopper':          ['continue'],
+                'msg_sundew_pxroute':   ['flow_callback',
+                                         'sarracenia.flowcb.accept.sundewpxroute.SundewPxRoute'],
+                'msg_test_retry':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.testretry.TestRetry'],
+                'msg_to_clusters':      ['flow_callback',
+                                         'sarracenia.flowcb.accept.toclusters.ToClusters'],
+                'msg_total':            ['continue'],
+                'msg_total_save':       ['continue'],
+                'post_hour_tree':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.posthourtree.PostHourTree'],
+                'post_long_flow':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.longflow.LongFLow'],
+                'post_override':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.postoverride.PostOverride'],
+                'post_total':           ['continue'],
+                'post_total_save':      ['continue'],
+                'wmo2msc':              ['flow_callback',
+                                         'sarracenia.flowcb.filter.wmo2msc.Wmo2Msc']
+               },
+ 'on_post':    {
+                'post_log':             ['logEvents', 'after_work']
+               },
+ 'plugin':     {
+                'accel_scp':            ['continue'],
+                'accel_wget':           ['continue'],
+                'msg_fdelay':           ['flowCallback',
+                                         'sarracenia.flowcb.filter.fdelay.FDelay'],
+                'msg_pclean_f90':       ['flowCallback',
+                                         'sarracenia.flowcb.filter.pclean_f90.PClean_F90'],
+                'msg_pclean_f92':       ['flowCallback',
+                                         'sarracenia.flowcb.filter.pclean_f92.PClean_F92']
+               },
+ 'windows_run':                         ['continue'],
+ 'xattr_disable':                       ['continue']
+}
+>>>
+
+
+

The options listed as ‘continue’ are obsolete ones, superceded by default processing, or rendered +unnecessary by changes in the implementation.

+
+
NOTICE: for API users and plugin writers, the v2 plugin format is entirely replaced by

the Flow Callback class. New plugin functionality +can mostly be implemented as plugins.

+
+
CHANGE: the v2 do_poll plugins must be replaced by subclassing for poll

Example in plugin porting

+
+
+

CHANGE: The v2 on_html_page plugins are also replaced by subclassing poll

+

CHANGE: v2 do_send replaced by send entrypoint in a Flowcb plugin plugin porting

+
+
NOTICE: the v2 accellerator plugins are replaced by built-in accelleration.

accel_wget_command, accel_scp_command, accel_ftpget_command, accel_ftpput_command, +accel_scp_command, are now built-in options used by the +Transfer class. +Adding new transfer protocols is done by sub-classing Transfer.

+
+
+

SHOULD: v2 on_message -> after_accept should be re-written plugin porting

+

SHOULD: v2 on_file -> after_work should be re-written plugin porting

+
+
SHOULD: v2 plugins should to be re-written. plugin porting

there are many built-in plugins that are ported and automatically +converted, but external ones must be re-written.

+

There are some performance consequences from this compatibility however, so high traffic +flows will run with less cpu and memory load if the plugins are ported to sr3. +To build native sr3 plugins, One should investigate the flowCallback (flowcb) class.

+
+
+

CHANGE: on_watch plugins entry_point becomes an sr3 after_accept entrypoint in a flowcb in a watch.

+
+
ACTION: The sr_audit component is gone. Replaced by running sr sanity as a cron

job (or scheduled task on windows.) to make sure that necessary processes continue to run.

+
+
CHANGE: obsolete settings: use_amqplib, use_pika. the new sarracenia.moth.amqp

uses the amqp library. To use other libraries, one should create new subclasses of sarracenia.moth.

+
+
CHANGE: statehost is now a boolean flag, fqdn option no longer implemented.

if this is a problem, submit an issue. It’s just not considered worthwhile for now.

+
+
CHANGE: sr_retry became retry.py.

Any plugins accessing internal structures of sr_retry.py need to be re-written. +This access is no longer necessary, as the API defines how to put notification messages on +the retry queue (move notification messages to worklist.failed. )

+
+
NOTICE: sr3 watch, with the force_polling option, is much less efficient

on sr3 than v2 for large directory trees (see issue #403 ) +Ideally, one does not use force_polling at all.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/docker.html b/How2Guides/docker.html new file mode 100644 index 000000000..9676de8d6 --- /dev/null +++ b/How2Guides/docker.html @@ -0,0 +1,161 @@ + + + + + + + Running MetPX via Docker — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Running MetPX via Docker

+
+

Introduction

+

While Sarracenia can be installed via pip, debian/Ubuntu, a Docker capability +is also provided in support of “run anywhere” containerization and cloud +native environments.

+

To provide maximum flexibility, Sarracenia’s default image does not include +a broker or an entrypoint. It is up to the user to further orchestrate their +deployment accordingly. Docker Compose is one such orchestration capability.

+

Below are various workflows to build, orchestrate and run sarracenia via Docker:

+
# clone repo
+git clone https://github.com/MetPX/sarracenia.git
+cd sarracenia
+
+# build image
+docker build -t metpx-sr3:latest .
+
+# sarracenia connected to a rabbitmq setup
+# start
+docker-compose -f docker/compose/pump/docker-compose.yml up
+# stop
+docker-compose -f docker/compose/pump/docker-compose.yml down
+
+
+
+
+

Logging

+

Norms in the docker world are to send messages to standard output, so +the option logStdout should be used in any configurations in a docker container. +This will make docker logs work as expected in a docker environment, by printing +to standard output.

+
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/index.html b/How2Guides/index.html new file mode 100644 index 000000000..bd2cb5e76 --- /dev/null +++ b/How2Guides/index.html @@ -0,0 +1,163 @@ + + + + + + + HOWTOS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

HOWTOS

+ +

# omitted as immature: +# docker

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/source.html b/How2Guides/source.html new file mode 100644 index 000000000..11cdbff1d --- /dev/null +++ b/How2Guides/source.html @@ -0,0 +1,637 @@ + + + + + + + Data Sources — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Data Sources

+
+

Injecting Data into a MetPX-Sarracenia Pump Network

+
+

Warning

+

FIXME: Missing sections are highlighted by FIXME. +Not really ready for use, too much missing for now.

+
+
+

Note

+

FIXME: known missing elements: good discussion of checksum choice. +Caveat about file update strategies. Use case of a file that is constantly updated, +rather than issuing new files.)

+
+
+

Revision Record

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+

A Sarracenia data pump is a web (or sftp) server with notifications for subscribers +to know, quickly, when new data has arrived. To find out what data is already available +on a pump, view the tree with a web browser. For simple immediate needs, one can +download data using the browser itself, or a standard tool such as wget. +The usual intent is for sr_subscribe to automatically download the data +wanted to a directory on a subscriber machine where other software +can process it. Note that this manual uses subscriptions to test +data injection, so the subscriber guide should likely be read before +this one.

+

Regardless of how it is done, injecting data means telling the pump where the data +is so that it can be forwarded to and/or by the pump. This can be done by either +using the active and explicit sr3_post command, or just using sr3 watch on a directory. +Where there are large numbers of files, and/or tight timeliness constraints, invocation +of sr3_post directly by the producer of the file is optimal, as sr3 watch may provide +disappointing performance. Another explicit, but low frequency approach is the +sr_poll command, which allows one to query remote systems to pull data +into the network efficiently.

+

While sr3 watch is written as an optimal directory watching system, there simply is no +quick way to watch large (say, more than 100,000 files) directory trees. On +dd.weather.gc.ca, as an example, there are 60 million files in about a million +directories. To walk that directory tree once takes several hours. To find new files, +the best temporal resolution is every few (say 3) hours. So on average a notification +will occur 1.5 hours after the file has showed up. Using I_NOTIFY (on Linux), it still +takes several hours to start up, because it needs to do an initial file tree walk to +set up all the watches. After that it will be instant, but if there are too many +files (and 60 million is very likely too many) it will just crash and refuse to work. +These are inherent limitations of watching directories, no matter how it is done. +If it is really neccessary to do this, there is hope. Please +consult Quickly Announcing Very Large Trees On Linux

+

With sr_post, the program that puts the file anywhere in the arbitrarily deep tree[1]_ tells +the pump (which will tell subscribers) exactly where to look. There are no system limits +to worry about. That’s how dd.weather.gc.ca works, and notifications are sub-second, with +60 million files on the disk. It is much more efficient, in general, to do direct +notifications rather than pass by the indirection of the file system, but in small +and simple cases, it makes little practical difference.

+

In the simplest case, the pump takes data from your account, wherever you have it, +providing you give it permission. We describe that case first.

+ +
+
+

SFTP Injection

+

Using the sr_post(1) command directly is the most straightforward way to inject data +into the pump network. To use sr_post, you have to know:

+
    +
  • the name of the local broker: ( say: ddsr.cmc.ec.gc.ca )

  • +
  • your authentication info for that broker ( say: user=rnd : password=rndpw )

  • +
  • your own server name (say: grumpy.cmc.ec.gc.ca )

  • +
  • your own user name on your server (say: peter)

  • +
+

Assume the goal is for the pump to access peter’s account via SFTP. Then you need +to take the pump’s public key, and place it in peter’s .ssh/authorized_keys. +On the server you are using (grumpy), one needs to do something like this:

+
cat pump.pub >>~peter/.ssh/authorized_keys
+
+
+

This will enable the pump to access peter’s account on grumpy using his private key. +So assuming one is logged in to peter’s account on grumpy, one can store the broker +credentials safely:

+
echo 'amqps://rnd:rndpw@ddsr.cmc.ec.gc.ca' >> ~/.config/sarra/credentials.conf:
+
+
+
+

Note

+

Passwords are always stored in the credentials.conf file. +to avoid doing that, it is better to use keys, which sarracenia can find by +looking at ssh configuration files. just configure ssh to work, and sarracenia +will as well.

+
+

So now the command line for sr3_post is just the url for ddsr to retrieve the +file on grumpy:

+
sr3_post -post_broker amqp://guest:guest@localhost/ -post_base_dir /var/www/posts/ \
+-post_base_url http://localhost:81/frog.dna
+
+2016-01-20 14:53:49,014 [INFO] Output AMQP  broker(localhost) user(guest) vhost(/)
+2016-01-20 14:53:49,019 [INFO] message published :
+2016-01-20 14:53:49,019 [INFO] exchange xs_guest topic v02.post.frog.dna
+2016-01-20 14:53:49,019 [INFO] notice   20160120145349.19 http://localhost:81/ frog.dna
+2016-01-20 14:53:49,020 [INFO] headers  parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c to_clusters=localhost
+
+
+

There is a sr_subscribe to subscribe to all *.dna posts the subscribe log said. Here is the config file:

+
broker amqp://guest:guest@localhost
+directory /var/www/subscribed
+subtopic #
+accept .*dna*
+
+
+

and here is the related output from the subscribe log file:

+
2016-01-20 14:53:49,418 [INFO] Received notice  20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna
+2016-01-20 14:53:49,419 [INFO] downloading/copying into /var/www/subscribed/frog.dna
+2016-01-20 14:53:49,420 [INFO] Downloads: http://grumpy:80/20160120/guest/frog.dna  into /var/www/subscribed/frog.dna 0-16
+2016-01-20 14:53:49,424 [INFO] 201 Downloaded : v02.report.20160120.guest.frog.dna 20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna 201 sarra-server-trusty guest 0.404653 parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c from_cluster=test_cluster source=guest to_clusters=test_cluster rename=/var/www/subscribed/frog.dna message=Downloaded
+
+
+

Or alternatively, here is the log from an sr_sarra instance:

+
2016-01-20 14:53:49,376 [INFO] Received v02.post.frog.dna '20160120145349.19 http://grumpy:81/ frog.dna' parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c to_cluster=ddsr.cmc.ec.gc.ca
+2016-01-20 14:53:49,377 [INFO] downloading/copying into /var/www/test/20160120/guest/frog.dna
+2016-01-20 14:53:49,377 [INFO] Downloads: http://grumpy:81/frog.dna  into /var/www/test/20160120/guest/frog.dna 0-16
+2016-01-20 14:53:49,380 [INFO] 201 Downloaded : v02.report.frog.dna 20160120145349.19 http://grumpy:81/ frog.dna 201 sarra-server-trusty guest 0.360282 parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c from_cluster=test_cluster source=guest to_clusters=test_cluster message=Downloaded
+2016-01-20 14:53:49,381 [INFO] message published :
+2016-01-20 14:53:49,381 [INFO] exchange xpublic topic v02.post.20160120.guest.frog.dna
+2016-01-20 14:53:49,381 [INFO] notice   20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna
+@
+
+
+

The command asks ddsr to retrieve the treefrog/frog.dna file by logging +in to grumpy as peter (using the pump’s private key) to retrieve it, and posting it +on the pump, for forwarding to the other pump destinations.

+

Similar to sr_subscribe, one can also place configuration files in an sr3_post specific directory:

+
blacklab% sr3_post edit dissem.conf
+
+post_broker amqps://rnd@ddsr.cmc.ec.gc.ca/
+post_base_url sftp://peter@grumpy
+
+
+

and then:

+
sr3_post -c dissem -url treefrog/frog.dna
+
+
+

If there are different varieties of posting used, configurations can be saved for each one.

+
+

Warning

+

FIXME: Need to do a real example. this made up stuff isn´t sufficiently helpful.

+

FIXME: sr3_post does not accept config files right now, says the man page. True/False?

+

sr3_post command lines can be a lot simpler if it did.

+
+

sr_post typically returns immediately as its only job is to advise the pump of the availability +of files. The files are not transferred when sr3_post returns, so one should not delete files +after posting without being sure the pump actually picked them up.

+
+

Note

+

sftp is perhaps the simplest for the user to implement and understand, but it is also +the most costly in terms of CPU on the server. All of the work of data transfer is +done at the python application level when sftp acquisition is done, which isn’t great.

+

A lower CPU version would be for the client to send somehow (sftp?) and then just +tell where the file is on the pump (basically the sr_sender2 version).

+
+

Note that this example used sftp, but if the file is available on a local web site, +then http would work, or if the data pump and the source server share a file system, +then even a file url could work.

+
+
+

HTTP Injection

+

If we take a similar case, but in this case there is some http accessible space, +the steps are the same or even simpler if no authentication is required for the pump +to acquire the data. One needs to install a web server of some kind.

+

Assume a configuration that shows all files under /var/www as folders, running under +the www-data users. Data posted in such directories must be readable to the www-data +user, to allow the web server to read it. The server running the web server +is called blacklab, and the user on the server is peter running as peter on blacklab, +a directory is created under /var/www/project/outgoing, writable by peter, +which results in a configuration like so:

+
sr3 edit watch/project.conf
+
+broker amqp://feeder@localhost/
+url http://blacklab/
+post_base_dir /var/www/project/outgoing
+
+
+

Then a watch is started:

+
sr3 start watch/project
+
+
+

While watch is running, any time a file is created in the document_root directory, +it will be announced to the pump (on localhost, ie. the server blacklab itself).:

+
cp frog.dna  /var/www/project/outgoing
+
+
+
+

Warning

+

FIXME: real example.

+
+

This triggers a post to the pump. Any subscribers will then be able to download +the file.

+
+

Warning

+

FIXME. too much broken for now to really run this easily… +so creating real demo is deferred.

+
+
+
+

Polling External Sources

+

Some sources are inherently remote, and we are unable to interest or affect them. +One can configure sr_poll to pull in data from external sources, typically web sites. +The sr_poll command typically runs as a singleton that tracks what is new at a source tree +and creates source notification messages for the pump network to process.

+

External servers, especially web servers often have different ways of posting their +product listings, so custom processing of the list is often needed. That is why sr_poll +has the do_poll setting, meaning that use of a plugin script is virtually required +to use it.

+
+

Note

+

see the poll_script included in the package plugins directory for an example. +FIXME:

+
+
+
+

Report Messages

+

If the sr3_post worked, that means the pump accepted to take a look at your file. +To find out where your data goes to afterward, one needs to examine source +log messages. It is also important to note that the initial pump, or any other pump +downstream, may refuse to forward your data for various reasons, that will only +be reported to the source in these report messages.

+

To view source report messages, the sr_report command is just a version of sr_subscribe, with the +same options where they make sense. If the configuration file (~/.config/sarra/default.conf) +is set up, then all that is needed is:

+
sr_report
+
+
+

To view report messages indicating what has happenned to the items inserted into the +network from the same pump using that account (rnd, in the example). One can trigger +arbitrary post processing of report messages by using on_message plugin.

+
+

Warning

+

FIXME: need some examples.

+
+
+
+

Large Files

+

Larger files are not sent as a single block. They are sent in parts, and each +part is fingerprinted, so that when files are updated, unchanged portions are +not sent again. There is a default threshold built into the sr_ commands, above +which partitioned notification messages will be done by default. This threshold can +be adjusted to taste using the part_threshold option.

+

Different pumps along the route may have different maximum part sizes. To +traverse a given path, the part must be no larger than the threshold setting +of all the intervening pumps. A pump will send the source an error log +message if it refuses to forward a file.

+

As each part is announced, so there is a corresponding report message for +each part. This allows senders to monitor progress of delivery of large +files.

+
+
+

Reliability and Checksums

+

Every piece of data injected into the pumping network needs to have a unique fingerprint (or checksum). +Data will flow if it is new, and determining if the data is new is based on the fingerprint. +To get reliability in a sarracenia network, multiple independent sources are provisioned. +Each source announces their products, and if they have the same name and fingerprint, then +the products are considered the same.

+

The sr_winnow component of sarracenia looks at incoming notification messages and notes which products +are received (by file name and checksum). If a product is new, it is forwarded on to other components +for processing. If a product is a duplicate, then the notification message is not forwarded further. +Similarly, when sr_subscribe or sr_sarra components receive an notification message for a product that is already +present on the local system, they will examine the fingerprint and not download the data unless it is different. +Checksum methods need to be known across a network, as downstream components will re-apply them.

+

Different fingerprinting algorithms are appropriate for different types of data, so +the algorithm to apply needs to be chosen by the data source, and not imposed by the network. +Normally, the ‘d’ algorithm is used, which applies the well-known Message-Digest 5 (md5sum) +algorithm to the data in the file.

+

When there is one origin for data, this algorithm works well. For high availability, +production chains will operate in parallel, preferably with no communication between +them. Items produced by independent chains may naturally have different processing +time and log stamps and serial numbers applied, so the same data processed through +different chains will not be identical at the binary level. For products produced +by different production chains to be accepted as equivalent, they need to have +the same fingerprint.

+

One solution for that case is, if the two processing chains will produce data with +the same name, to checksum based on the file name instead of the data, this is called ‘n’. +In many cases, the names themselves are production chain dependent, so a custom +algorithm is needed. If a custom algorithm is chosen, it needs to be published on +the network:

+
http://dd.cmc.ec.gc.ca/config/msc-radar/sums/
+
+   u.py
+
+
+

So downstream clients can obtain and apply the same algorithm to compare notification messages +from multiple sources.

+
+

Warning

+

FIXME: science fiction again: no such config directories exist yet. no means to update them. +search path for checksum algos? built-in,system-wide,per-source?

+

Also, if each source defines their own algorithm, then they need to pick the same one +(with the same name) in order to have a match.

+

FIXME: verify that fingerprint verification includes matching the algorithm as well as value.

+

FIXME: not needed at the beginning, but likely at some point. +in the mean time, we just talk to people and include their algorithms in the package.

+
+
+

Note

+

Fingerprint methods that are based on the name, rather than the actual data, +will cause the entire file to be re-sent when they are updated.

+
+
+
+

User Headers

+

What if there is some piece of metadata that a data source has chosen for some reason not to +include in the filename hierarchy? How can data consumers know that information without having +to download the file in order to determine that it is uninteresting. An example would be +weather warnings. The file names might include weather warnings for an entire country. If consumers +are only interested in downloading warnings that are local to them, then, a data source could +use the on_post hook in order to add additional headers to the notification message.

+
+

Note

+

With great flexibility comes great potential for harm. The path names should include as much information +as possible as sarracenia is built to optimize routing using them. Additional meta-data should be used +to supplement, rather than replace, the built-in routing.

+
+

To add headers to notification messages being posted, one can use header option. In a configuration +file, add the following statements:

+
header CAP_province=Ontario
+header CAP_area-desc=Uxbridge%20-%20Beaverton%20-%20Northern%20Durham%20Region
+header CAP_polygon=43.9984,-79.2175 43.9988,-79.219 44.2212,-79.3158 44.4664,-79.2343 44.5121,-79.1451 44.5135,-79.1415 44.5136,-79.1411 44.5137,-79.1407 44.5138,-79.14 44.5169,-79.0917 44.517,-79.0879 44.5169,-79.0823 44.218,-78.7659 44.0832,-78.7047 43.9984,-79.2175
+
+
+

So that when a file notification message is posted, it will include the headers with the given values. +This example is artificial in that it statically assigns the header values which is appropriate +to simple cases. For this specific case, it is likely more appropriate to implement a specialized +on_post plugin for Common Alerting Protocol files to extract the above header information and +place it in the notification message headers for each alert.

+
+

Efficiency Considerations

+

It is not recommended to put overly complex logic in the plugin scripts, as they execute synchronously with +post and receive operations. Note that the use of built-in facilities of AMQP (headers) is done to +explicitly be as efficient as possible. As an extreme example, including encoded XML into notification messages +will not affect performance slightly, it will slow processing by orders of magnitude. One will not +be able to compensate for with multiple instances, as the penalty is simply too large to overcome.

+

Consider, for example, Common Alerting Protocol (CAP) messages for weather alerts. These alerts routinely +exceed 100 KBytes in size, wheras a sarracenia notification message is on the order of 200 bytes. The sarracenia notification messages +go to many more recipients than the alert: anyone considering downloading an alert, as oppposed to just the ones +the subscriber is actually interested in, and this metadata will also be included in the report messages, +and so replicated in many additional locations where the data itself will not be present.

+

Including all the information that is in the CAP would mean just in terms of pure transport 500 times +more capacity used for a single notification message. When there are many millions of notification messages to transfer, this adds up. +Only the minimal information required by the subscriber to make the decision to download or not should be +added to the notification message. It should also be noted that in addition to the above, there is typically a 10x to +100x cpu and memory penalty parsing an XML data structure compared to plain text representation, which +will affect the processing rate.

+
+
+
+
+
+

Quickly Announcing Very Large Trees On Linux

+

To mirror very large trees (millions of files) in real time, it takes too long for tools like rsync +or find to traverse and generate lists of files to copy. On Linux, one can intercept calls for +file operations using the well known shim library technique. This technique provides virtually +real-time notification messages of files regardless of the size of the tree, with minimal overhead as +this technique imposes much less load than tree traversal mechanisms, and makes use of the +C implementation of Sarracenia, which uses very little memory or processor resources.

+

To use this technique, one needs to have the C implementation of Sarracenia installed. The Libsrshim +library is part of that package, and the environment needs to be configured to intercept calls +to the C library like so:

+
export SR_POST_CONFIG=somepost.conf
+export LD_PRELOAD=libsrshim.so.1.0.0
+
+
+

Where somepost.conf is a valid configuration that can be tested with sr3_post to manually post a file. +Any process invoked from a shell with these settings will have all calls to routines like close(2) +intercepted by libsrshim. Libsrshim will check if the file is being written, and then apply the +somepost configuration (accept/reject clauses) and post the file if it is appropriate. +Example:

+
blacklab% more pyiotest
+f=open("hoho", "w+" )
+f.write("hello")
+f.close()
+blacklab%
+
+blacklab% more test2.sh
+
+echo "called with: $* "
+if [ ! "${LD_PRELOAD}" ]; then
+   export SR_POST_CONFIG=`pwd`/test_post.conf
+   export LD_PRELOAD=`pwd`/libsrshim.so.1.0.0
+   exec $0
+   #the exec here makes the LD_PRELOAD affect this shell, as well as sub-processes.
+fi
+
+set -x
+
+echo "FIXME: exec above fixes ... builtin i/o like redirection not being posted!"
+bash -c 'echo "hoho" >>~/test/hoho'
+
+/usr/bin/python2.7 pyiotest
+cp libsrshim.c ~/test/hoho_my_darling.txt
+
+blacklab%
+
+lacklab% ./test2.sh
+called with:
+called with:
++++ echo 'FIXME: exec above fixes ... builtin i/o like redirection not being posted!'
+FIXME: exec above fixes ... builtin i/o like redirection not being posted!
++++ bash -c 'echo "hoho" >>~/test/hoho'
+2017-10-21 20:20:44,092 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,092 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,092 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,094 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,095 [DEBUG] isMatchingPattern: /home/peter/test/hoho matched mask: accept .*
+2017-10-21 20:20:44,096 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,096 [DEBUG] sr3_post file2message called with: /home/peter/test/hoho sb=0x7ffef2aae2f0 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,096 [INFO] published: 20171021202044.096 sftp://peter@localhost /home/peter/test/hoho topic=v02.post.home.peter.test sum=s,a0bcb70b771de1f614c724a86169288ee9dc749a6c0bbb9dd0f863c2b66531d21b65b81bd3d3ec4e345c2fea59032a1b4f3fe52317da3bf075374f7b699b10aa source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202002.304 atime=20171021202002.308 mode=0644 parts=1,2,1,0,0
++++ /usr/bin/python2.7 pyiotest
+2017-10-21 20:20:44,105 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,105 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,105 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,107 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,107 [DEBUG] isMatchingPattern: /home/peter/src/sarracenia/c/hoho matched mask: accept .*
+2017-10-21 20:20:44,108 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,108 [DEBUG] sr3_post file2message called with: /home/peter/src/sarracenia/c/hoho sb=0x7ffeb02838b0 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,108 [INFO] published: 20171021202044.108 sftp://peter@localhost /c/hoho topic=v02.post.c sum=s,9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043 source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202044.101 atime=20171021202002.320 mode=0644 parts=1,5,1,0,0
++++ cp libsrshim.c /home/peter/test/hoho_my_darling.txt
+2017-10-21 20:20:44,112 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,112 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,112 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,114 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,114 [DEBUG] isMatchingPattern: /home/peter/test/hoho_my_darling.txt matched mask: accept .*
+2017-10-21 20:20:44,115 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,115 [DEBUG] sr3_post file2message called with: /home/peter/test/hoho_my_darling.txt sb=0x7ffc8250d950 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,116 [INFO] published: 20171021202044.115 sftp://peter@localhost /home/peter/test/hoho_my_darling.txt topic=v02.post.home.peter.test sum=s,f5595a47339197c9e03e7b3c374d4f13e53e819b44f7f47b67bf1112e4bd6e01f2af2122e85eda5da633469dbfb0eaf2367314c32736ae8aa7819743f1772935 source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202044.109 atime=20171021202002.328 mode=0644 parts=1,15117,1,0,0
+blacklab%
+
+
+
+
Note::
+

file re-direction of i/o resulting from shell builtins (no process spawn) in the shell where +the environment variables are first set WILL NOT BE POSTED. only sub-shells are affected:

+
# will not be posted...
+echo "hoho" >kk.conf
+
+# fill be posted.
+bash -c 'echo "hoho" >kk.conf'
+
+
+

This is a limitation of the technique, as the dynamic library load order is resolved on +process startup, and cannot be modified afterward. One work-around:

+
if [ ! "${LD_PRELOAD}" ]; then
+  export SR_POST_CONFIG=`pwd`/test_post.conf
+  export LD_PRELOAD=`pwd`/libsrshim.so.1.0.0
+  exec $*
+fi
+
+
+
+

Which will activate the shim library for the calling environment by restarting it. +This particular code may have impact on command line options and may not be directly applicable.

+
+
+

As an example, we have a tree of 22 million files that is written continuously day and night. +We need to copy that tree to a second file system as quickly as possible, with an aspirational +maximum copy time being about five minutes.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/subscriber.html b/How2Guides/subscriber.html new file mode 100644 index 000000000..5e0e3b8cb --- /dev/null +++ b/How2Guides/subscriber.html @@ -0,0 +1,1186 @@ + + + + + + + Subscriber Guide — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Subscriber Guide

+
+

Receiving Data from a MetPX-Sarracenia Data Pump

+
+

Revision Record

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+
+

Introduction

+

A Sarracenia data pump is a web server with notifications +for subscribers to know, quickly, when new data has arrived. +To find out what data is already available on a pump, +view the tree with a web browser. +For simple immediate needs, one can download data using the +browser itself, or a standard tool such as wget.

+

The usual intent is to automatically download the dsesired data to a directory on a subscriber +machine where other software can process it. Please note:

+
    +
  • The tool is entirely command line driven (there is no GUI). More accurately, it is mostly configuration file driven. +Most of the interface involves using a text editor to modify configuration files.

  • +
  • While written to be compatible with other environments, the focus is on Linux usage.

  • +
  • The tool can be used as either an end-user tool, or a system-wide transfer engine. +This guide is focused on the end-user case.

  • +
  • All documentation of the package is available +at https://metpx.github.io/sarracenia

  • +
+

While Sarracenia can work with any web tree, or any URL +that sources choose to post, there is a conventional layout, for example at:

+
+
+

A data pump’s web server will just expose web accessible folders +and the root of the tree is the date, in YYYYMMDD format. +These dates do not represent anything about the data other than +when it was put into the pumping network, and since Sarracenia +always uses Universal Co-ordinated Time, the dates might not correspond to +the current date/time in the location of the subscriber:

+
Index of /
+
+Name                    Last modified      Size  Description
+Parent Directory                            -
+20151105/               2015-11-27 06:44    -
+20151106/               2015-11-27 06:44    -
+20151107/               2015-11-27 06:44    -
+20151108/               2015-11-27 06:44    -
+20151109/               2015-11-27 06:44    -
+20151110/               2015-11-27 06:44    -
+
+
+

A variable number of days are stored on each data pump; for those +with an emphasis on real-time reliable delivery, the number of days +will be shorter. For other pumps, where long term outages need +to be tolerated, more days will be kept.

+

Under the first level of date trees, there is a directory +per source. A Source in Sarracenia is an account used to inject +data into the pump network. Data can cross many pumps on its +way to the visible ones:

+
Index of /20151110
+
+Name                    Last modified      Size  Description
+Parent Directory                            -
+UNIDATA-UCAR/           2015-11-27 06:44    -
+NOAAPORT/               2015-11-27 06:44    -
+MSC-CMC/                2015-11-27 06:44    -
+UKMET-RMDCN/            2015-11-27 06:44    -
+UKMET-Internet/         2015-11-27 06:44    -
+NWS-OPSNET/             2015-11-27 06:44    -
+
+
+

The data under each of these directories was obtained from the named +source. In these examples, it is actually injected by Data Interchange +staff, and the names are chosen to represent the origin of the data.

+

The original Environment and Climate Change Canada data mart is +one “source” in this sense, showing up on hpfx as WXO-DD, or the same +tree being available at the root of:

+
https://dd.weather.gc.ca
+
+
+

Once down to the viewing the content from a given source, +products are organized in a way defined by the source:

+
Icon  Name                    Last modified      Size  Description
+[TXT] about_dd_apropos.txt    2021-05-17 13:23   1.0K
+[DIR] air_quality/            2020-12-10 14:47    -
+[DIR] alerts/                 2022-07-13 12:00    -
+[DIR] analysis/               2022-07-13 13:17    -
+[DIR] barometry/              2022-03-22 22:00    -
+[DIR] bulletins/              2022-07-13 13:19    -
+[DIR] citypage_weather/       2022-07-13 13:21    -
+[DIR] climate/                2020-09-03 16:30    -
+[DIR] doc/                    2022-09-28 20:00    -
+[DIR] ensemble/               2022-07-13 13:34    -
+[DIR] hydrometric/            2021-01-14 14:12    -
+[DIR] marine_weather/         2020-12-15 14:51    -
+[DIR] meteocode/              2022-07-13 14:01    -
+[DIR] model_gdsps/            2021-12-01 21:41    -
+[DIR] model_gdwps/            2021-12-01 16:50    -
+
+
+

Directories below that level are related to the date being sought.

+

One can run sr3 to download selected products from Data pumps like these. +The configuration files are a few lines of configuration, and sr3 +includes some examples.

+

You can list the available configurations with sr3 list:

+
$ sr3 list examples
+  Sample Configurations: (from: /usr/lib/python3/dist-packages/sarracenia/examples )
+  cpump/cno_trouble_f00.inc        poll/aws-nexrad.conf             poll/pollingest.conf             poll/pollnoaa.conf               poll/pollsoapshc.conf
+  poll/pollusgs.conf               poll/pulse.conf                  post/WMO_mesh_post.conf          sarra/wmo_mesh.conf              sender/ec2collab.conf
+  sender/pitcher_push.conf         shovel/no_trouble_f00.inc        subscribe/WMO_Sketch_2mqtt.conf  subscribe/WMO_Sketch_2v3.conf    subscribe/WMO_mesh_CMC.conf
+  subscribe/WMO_mesh_Peer.conf     subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf          subscribe/dd_all.conf            subscribe/dd_amis.conf
+  subscribe/dd_aqhi.conf           subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf       subscribe/dd_cmml.conf           subscribe/dd_gdps.conf
+  subscribe/dd_ping.conf           subscribe/dd_radar.conf          subscribe/dd_rdps.conf           subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+  subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf        subscribe/hpfx_amis.conf         subscribe/local_sub.conf
+  subscribe/pitcher_pull.conf      subscribe/sci2ec.conf            subscribe/subnoaa.conf           subscribe/subsoapshc.conf        subscribe/subusgs.conf
+  sender/ec2collab.conf            sender/pitcher_push.conf         watch/master.conf                watch/pitcher_client.conf        watch/pitcher_server.conf
+  watch/sci2ec.conf
+
+
+

AMIS, the Canadian AES (Atmospheric Environment Service) Meteorological Information Service, was a satellite +broadcast system for weather data in the 1980’s. It is a continuous stream of text messages (originally at 4800 bps!) +and each message is limited to 14000 bytes. The service was transitioned to an internet streaming feed in the early 2000’s, +and the streaming version is still fed to those interested in air and maritime navigation across the country.

+

One can download a continuous feed of such traditional weather bulletins from the original data mart using the subscribe/dd_amis.conf +configuration example:

+
$ sr3 add subscribe/dd_amis.conf
+add: 2021-01-26 01:13:54,047 [INFO] sarracenia.sr add copying: /usr/lib/python3/dist-packages/sarracenia/examples/subscribe/dd_amis.conf to /home/peter/.config/sr3/subscribe/dd_amis.conf
+
+
+

Now files in .config/ can be used directly:

+
$ sr3 list
+User Configurations: (from: /home/peter/.config/sr3 )
+subscribe/dd_amis.conf           admin.conf                       credentials.conf                 default.conf
+logs are in: /home/peter/.cache/sr3/log
+
+
+

To view a configuration, give it to sr3 list as an argument:

+
$ sr3 list subscribe/dd_amis.conf
+# this is a feed of wmo bulletin (a set called AMIS in the old times)
+
+broker amqps://dd.weather.gc.ca/
+topicPrefix v02.post
+
+# instances: number of downloading processes to run at once.  defaults to 1. Not enough for this case
+instances 5
+
+# expire, in operational use, should be longer than longest expected interruption
+expire 10m
+
+subtopic bulletins.alphanumeric.#
+
+directory /tmp/dd_amis
+accept .*
+
+
+

Then it can be run interactively with sr3 foreground subscribe/dd_amis or as a service +with sr3 start subscribe/dd_amis. In both cases, files will be downloaded from +dd.weather.gc.ca into the local machine’s /tmp/dd_amis directory.

+

More information:

+ +
+
+

Server-Side Resources Allocated for Subscribers

+

Every configuration results in corresponding resources being declared on the broker, +whose lifetime is controlled by the expire setting. The default expire is set +to 300 seconds to avoid cluttering up servers with small experiments. Set expire +to the value that makes the most sense for your application (long enough to cover +outages you may experience). In a configuration file, something like:

+
expire 3h
+
+
+

might be appropriate. When changing subtopic or queue settings, or when one +expects to not use a configuration for an extended period of time, it is best to:

+
sr3 cleanup subscribe/swob.conf
+
+
+

which will de-allocate the queue (and its bindings) on the server.

+

Why? Whenever a subscriber is started, a queue is created on the data pump, with +the topic bindings set by the configuration file. If the subscriber is stopped, +the queue keeps getting notification messages as defined by subtopic selection, and when the +subscriber starts up again, the queued notification messages are forwarded to the client. +So when the subtopic option is changed, since it is already defined on the +server, one ends up adding a binding rather than replacing it. For example, +if one has a subtopic that contains SATELLITE, and then stops the subscriber, +edits the file and now the topic contains only RADAR, when the subscriber is +restarted, not only will all the queued satellite files be sent to the consumer, +but the RADAR is added to the bindings, rather than replacing them, so the +subscriber will get both the SATELLITE and RADAR data even though the configuration +no longer contains the former.

+

Also, if one is experimenting, and a queue is to be stopped for a very long +time, it may accumulate a large number of notification messages. The total number of notification messages +on a data pump has an effect on the pump performance for all users. It is therefore +advisable to have the pump de-allocate resources when they will not be needed +for an extended period, or when experimenting with different settings.

+
+
+

Working with Multiple Configurations

+

Place all configuration files, with the .conf suffix, in a standard +directory: ~/.config/sr3/subscribe/ For example, if there are two files in +that directory: dd_amis.conf and hpfx_amis.conf, one could then run:

+
fractal% sr3 start subscribe/dd_amis.conf
+starting:.( 5 ) Done
+
+fractal%
+
+
+

to start the CMC downloading configuration. One can use the sr3 command to start/stop multiple configurations at once. +The sr3 command will go through the default directories and start up +all the configurations it finds:

+
fractal% sr3 status
+status:
+Component/Config                         State             Run  Miss   Exp Retry
+----------------                         -----             ---  ----   --- -----
+subscribe/dd_amis                        stopped             0     0     0     0
+subscribe/hpfx_amis                      stopped             0     0     0     0
+      total running configs:   0 ( processes: 0 missing: 0 stray: 0 )
+fractal% sr3 edit subscribe/hpfx_amis
+
+fractal% sr3 start
+starting:.( 10 ) Done
+
+fractal% sr3 status
+status:
+Component/Config                         State             Run  Miss   Exp Retry
+----------------                         -----             ---  ----   --- -----
+subscribe/dd_amis                        running             5     0     5     0
+subscribe/hpfx_amis                      running             5     0     5     0
+      total running configs:   2 ( processes: 10 missing: 0 stray: 0 )
+fractal%
+
+
+

will start up some sr3 processes as configured by CMC.conf and others +to match hpfx_amis.conf. Sr3 stop will also do what you would expect. As will sr3 status. +Note that there are 5 sr_subscribe processes that start with the CMC +configuration and 3 NWS ones. These are instances and share the same +download queue.

+

More information:

+ +
+
+

High Priority Delivery

+

While the Sarracenia protocol does not provide explicit prioritization, the use +of multiple queues provides similar benefits. Each configuration results +in a queue declaration on the server side. Group products at like priority into +a queue by selecting them using a common configuration. The smaller the groupings, +the lower the delay of processing. While all queues are processed at the same priority, +data passes though shorter queues more quickly. One can summarize with:

+
+

Use Multiple Configurations to Prioritize

+
+

To make the advice concrete, take the example of the Environment Canada data +mart ( dd.weather.gc.ca ), which distributes gridded binaries, GOES satellite +imagery, many thousands of city forecasts, observations, RADAR products, etc… +For real-time weather, warnings and RADAR data are the highest priority. At certain +times of the day, or in cases of backlogs, many hundreds of thousands of products +can delay receipt of high priority products if only a single queue is used.

+

To ensure prompt processing of data in this case, define one configuration to subscribe +to weather warnings (which are a very small number of products), a second for the RADARS +(a larger but still relatively small group), and a third (largest grouping) for all +the other data. Each configuration will use a separate queue. Warnings will be +processed the fastest, RADARS will queue up against each other and so experience some +more delay, and other products will share a single queue and be subject to more +delay in cases of backlog.

+

https://github.com/MetPX/sarracenia/blob/main/sarracenia/examples/subscribe/ddc_cap-xml.conf:

+
broker amqps://dd.weather.gc.ca/
+topicPrefix v02.post
+
+#expiration du file d´attende sur le serveur. doit excèder la durée maximale
+#     de panne qu´on veut tolérer sans perte. (1d un jour?)
+expire 10m
+subtopic alerts.cap.#
+
+mirror
+
+directory ${HOME}/datamartclone
+
+
+

https://github.com/MetPX/sarracenia/blob/main/sarracenia/examples/subscribe/ddc_normal.conf:

+
broker amqps://dd.weather.gc.ca/
+topicPrefix v02.post
+
+subtopic #
+
+# reject hi priority data captured by other configuration.
+reject .*alerts/cap.*
+
+#expire, needs to be longer than the longest expected interruption in service.
+expire 10m
+
+mirror
+directory ${HOME}/datamartclone
+
+
+

Where you want the mirror of the data mart to start at $(HOME)/datamartclone (presumably there is a web +server configured do display that directory.) Likely, the ddc_normal configuration +will experience a lot of queueing, as there is a lot of data to download. The ddc_hipri.conf is +only subscribed to weather warnings in Common Alerting Protocol format, so there will be +little to no queueing for that data.

+
+
+

Refining Selection

+
+

Warning

+

FIXME: Make a picture, with a:

+
    +
  • broker at one end, and the subtopic apply there.

  • +
  • client at the other end, and the accept/reject apply there.

  • +
+
+

Pick subtopics ( which are applied on the broker with no notification message downloads ) to narrow +the number of notification messages that traverse the network to get to the sarracenia client processes. +The reject and accept options are evaluated by the sr_subscriber processes themselves, +providing regular expression based filtering of the posts which are transferred. +accept operates on the actual path (well, URL), indicating what files within the +notification stream received should actually be downloaded. Look in the Downloads +line of the log file for examples of this transformed path.

+
+

Note

+

Brief Introduction to Regular Expressions

+

Regular expressions are a very powerful way of expressing pattern matches. +They provide extreme flexibility, but in these examples we will only use a +very trivial subset: The . is a wildcard matching any single character. If it +is followed by an occurrence count, it indicates how many letters will match +the pattern. the * (asterisk) character, means any number of occurrences. +so:

+
    +
  • .* means any sequence of characters of any length. In other words, match anything.

  • +
  • cap.* means any sequence of characters that starts with cap.

  • +
  • .*CAP.* means any sequence of characters with CAP somewhere in it.

  • +
  • .*cap means any sequence of characters that ends with CAP. In case where multiple portions of the string could match, the longest one is selected.

  • +
  • .*?cap same as above, but non-greedy, meaning the shortest match is chosen.

  • +
+

Please consult various internet resources for more information on the full +variety of matching possible with regular expressions:

+ +
+

back to sample configuration files:

+

Note the following:

+
$ sr3 edit subscribe/swob
+
+
+
+

broker amqps://anonymous@dd.weather.gc.ca +topicPrefix v02.post +accept .*/observations/swob-ml/.*

+

#write all SWOBS into the current working directory +#BAD: THIS IS NOT AS GOOD AS THE PREVIOUS EXAMPLE +# NOT having a “subtopic” and filtering with “accept” MEANS EXCESSIVE NOTIFICATIONS are processed.

+
+

This configuration, from the subscriber point of view, will likely deliver +the same data as the previous example. However, the default subtopic being +a wildcard means that the server will transfer all notifications for the +server (likely millions of them) that will be discarded by the subscriber +process applying the accept clause. It will consume a lot more CPU and +bandwidth on both server and client. One should choose appropriate subtopics +to minimize the notifications that will be transferred only to be discarded. +The accept (and reject) patterns is used to further refine subtopic rather +than replace it.

+

By default, the files downloaded will be placed in the current working +directory when sr_subscribe was started. This can be overridden using +the directory option.

+

If downloading a directory tree, and the intent is to mirror the tree, +then the option mirror should be set:

+
$ sr3 edit subscribe/swob
+
+  broker amqps://anonymous@dd.weather.gc.ca
+  topicPrefix v02.post
+  subtopic observations.swob-ml.#
+  directory /tmp
+  mirror True
+  #
+  # instead of writing to current working directory, write to /tmp.
+  # in /tmp. Mirror: create a hierarchy like the one on the source server.
+
+
+

One can also intersperse directory and accept/reject directives to build +an arbitrarily different hierarchy from what was on the source data pump. +The configuration file is read from top to bottom, so then sr_subscribe +finds a ‘’directory’’ option setting, only the ‘’accept’’ clauses after +it will cause files to be placed relative to that directory:

+
$ sr3 edit subscribe/ddi_ninjo_part1.conf
+
+broker amqps://ddi.cmc.ec.gc.ca/
+topicPrefix v02.post
+subtopic ec.ops.*.*.ninjo-a.#
+
+directory /tmp/apps/ninjo/import/point/reports/in
+accept .*ABFS_1.0.*
+accept .*AQHI_1.0.*
+accept .*AMDAR_1.0.*
+
+directory /tmp/apps/ninjo/import/point/catalog_common/in
+accept .*ninjo-station-catalogue.*
+
+directory /tmp/apps/ninjo/import/point/scit_sac/in
+accept .*~~SAC,SAC_MAXR.*
+
+directory /tmp/apps/ninjo/import/point/scit_tracker/in
+accept .*~~TRACKER,TRACK_MAXR.*
+acceptUnmatched off
+
+
+

In the above example, ninjo-station catalog data is placed in the +catalog_common/in directory, rather than in the point data +hierarchy used to store the data that matches the first three +accept clauses.

+
+

Note

+

Note that .* in the subtopic directive, where +it means ´match any one topic´ (ie. no period characters allowed in +topic names) has a different meaning than it does in an accept +clause, where it means match any string.

+

Yes, this is confusing. No, it cannot be helped.

+
+

more:

+ +
+
+

Data Loss

+
+

Outage

+

The expire determines how long the data pump will hold onto your queued subscription, +after a disconnection. The setting needs to be set longer than the longest outage your +feed needs to survive without data loss.

+
+
+

Too slow, Queue Too Large

+

The performance of a feed +is important, as, serving the internet, a one client´s slow download affects all the other ones, +and a few slow clients can overwhelm a data pump. Often there are server policies in place +to prevent mis-configured (i.e. too slow) subscriptions from resulting in very long queues.

+

When the queue becomes too long, the data pump may start discarding messages, and +the subscriber will perceive that as data loss.

+

To identify slow downloads, examine the lag in the download log. For example, create +a sample subscriber like so:

+
fractal% sr3 list ie
+
+Sample Configurations: (from: /home/peter/Sarracenia/sr3/sarracenia/examples )
+cpump/cno_trouble_f00.inc        flow/amserver.conf               flow/poll.inc                    flow/post.inc                    flow/report.inc                  flow/sarra.inc
+flow/sender.inc                  flow/shovel.inc                  flow/subscribe.inc               flow/watch.inc                   flow/winnow.inc                  poll/airnow.conf
+poll/aws-nexrad.conf             poll/mail.conf                   poll/nasa-mls-nrt.conf           poll/noaa.conf                   poll/soapshc.conf                poll/usgs.conf
+post/WMO_mesh_post.conf          sarra/wmo_mesh.conf              sender/am_send.conf              sender/ec2collab.conf            sender/pitcher_push.conf         shovel/no_trouble_f00.inc
+subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf          subscribe/dd_all.conf            subscribe/dd_amis.conf           subscribe/dd_aqhi.conf           subscribe/dd_cacn_bulletins.conf
+subscribe/dd_citypage.conf       subscribe/dd_cmml.conf           subscribe/dd_gdps.conf           subscribe/dd_radar.conf          subscribe/dd_rdps.conf           subscribe/dd_swob.conf
+subscribe/ddc_cap-xml.conf       subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf        subscribe/hpfxWIS2DownloadAll.conf subscribe/hpfx_amis.conf
+subscribe/hpfx_citypage.conf     subscribe/local_sub.conf         subscribe/ping.conf              subscribe/pitcher_pull.conf      subscribe/sci2ec.conf            subscribe/subnoaa.conf
+subscribe/subsoapshc.conf        subscribe/subusgs.conf           sender/am_send.conf              sender/ec2collab.conf            sender/pitcher_push.conf         watch/master.conf
+watch/pitcher_client.conf        watch/pitcher_server.conf        watch/sci2ec.conf
+fractal%
+
+
+

pick one ane add it local configuration:

+
fractal% sr3 add subscribe/hpfx_amis.conf
+missing state for subscribe/hpfx_amis
+add: 2022-12-07 12:39:15,513 3286889 [INFO] root add matched existing ['subscribe/hpfx_amis']
+2022-12-07 12:39:15,513 3286889 [ERROR] root add nothing specified to add
+fractal%
+
+
+

run it in foreground for a few seconds and stop it:

+
fractal% sr3 foreground subscribe/hpfx_amis
+.2022-12-07 12:39:37,977 [INFO] 3286919 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup queue declared q_anonymous_subscribe.hpfx_amis.67711727.37906289 (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup binding q_anonymous_subscribe.hpfx_amis.67711727.37906289 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flowcb.log __init__ subscribe initialized with: {'post', 'on_housekeeping', 'after_accept', 'after_work', 'after_post'}
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run pid: 3286919 subscribe/hpfx_amis instance: 0
+2022-12-07 12:39:38,241 [INFO] 3286919 sarracenia.flow run now active on vip None
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.20 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 3.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN20_KWAL_071739___19368
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 1.19 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SX/KWAL/17/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN20_KWAL_071739___19368
+fractal% sr3 foreground subscribe/hpfx_amis
+.2022-12-07 12:39:37,977 [INFO] 3286919 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup queue declared q_anonymous_subscribe.hpfx_amis.67711727.37906289 (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup binding q_anonymous_subscribe.hpfx_amis.67711727.37906289 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flowcb.log __init__ subscribe initialized with: {'post', 'on_housekeeping', 'after_accept', 'after_work', 'after_post'}
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run pid: 3286919 subscribe/hpfx_amis instance: 0
+2022-12-07 12:39:38,241 [INFO] 3286919 sarracenia.flow run now active on vip None
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.20 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 3.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN20_KWAL_071739___19368
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 1.19 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SX/KWAL/17/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN20_KWAL_071739___19368
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___40860
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SA/KNKA/17/SAAK41_KNKA_071739___36105
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___19641
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___40860
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SAAK41_KNKA_071739___36105
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___19641
+2022-12-07 12:39:43,924 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.40 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___44806
+2022-12-07 12:39:43,924 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.40 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/UA/CWAO/17/UANT01_CWAO_071739___24012
+2022-12-07 12:39:44,098 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___44806
+2022-12-07 12:39:44,098 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/UANT01_CWAO_071739___24012
+
+
+

The lag: numbers reported in the foreground display indicate how old the data is (in seconds, based on the time it was added to the network +by the source. If you see that lag grow unreasonably, your subscription has a performance problem.

+
+
+
+

Performance

+

There are many aspects of Performance that we won’t go into here.

+

more:

+

Minimizing the time after a file has been delivered, and before it is picked up by the next hop:

+ +

Getting file changes noticed rapidly, filtering frequent file re-writes, scheduling copies:

+ +

The most common desire when performance is raised is speed up their downloads. +the steps are as follows:

+
+

Optimize File Selection per Process

+
    +
  • Often users specif # as their subtopic, meaning the accept/rejects do all the work. In many cases, users are only interested in a small fraction of the files being published. For best performance, Make *subtopic* as specific as possible to have minimize sending notification messages that are send by the broker and arrive on the subscriber only to be rejected. (use log_reject option to find such products.)

  • +
  • Place *reject* statements as early as possible in the configuration. As rejection saves processing of any later regex’s in the configuration.

  • +
  • Have few accept/reject clauses: because it involves a regular expression +match, accept/reject clauses are expensive, but evaluating a complex +regex is not much more expensive than a simple one, so it is better to have +a few complicated ones than many simple ones. Example:

    +
    accept .*/SR/KWAL.*
    +accept .*/SO/KWAL.*
    +
    +
    +

    will run at rougly half the speed (or double the cpu overhead) compared to

    +
    accept .*/S[OR]/KWAL.*
    +
    +
    +
  • +
  • Use suppress_duplicates. In some cases, there is a risk of the same file +being announced more than once. Usually clients do not want redundant copies +of files transferred. The suppress_duplicates option sets up a cache of +checksums of the files which have gone by, and prevents their being processed +again.

  • +
  • If you are transferring small files, the built-in transfer processing is quite +good, but if there are large files in the mix, then oflloading to a C +binary is going to go faster. Use plugins such as accel_wget, accel_sftp, +accel_cp (for local files.) These plugins have threshold settings so that +the optimial python transer methods are still used for files smaller than the +threshold.

  • +
  • increasing prefetch can reduce the average latency (being amortised over +the number of notification messages prefetched.) It can improve performance over long +distances or in high notification message rates within an data centre.

  • +
  • If you control the origin of a product stream, and the consumers will want a +very large proportion of the products announced, and the products are small +(a few K at most), then consider combining use of v03 with inlining for +optimal transfer of small files. Note, if you have a wide variety of users +who all want different data sets, inlining can be counter-productive. This +will also result in larger notification messages and mean much higher load on the broker. +It may optimize a few specific cases, while slowing the broker down overall.

  • +
+
+
+

Use Instances

+

Once you have optimized what a single subscriber can do, if it is not fast enough, +then use the instances option to have more processes participate in the +processing. Having 10 or 20 instances is not a problem at all. The maximum +number of instances that will increase performance will plateau at some point +that varies depending on latency to broker, how fast the instances are at processing +each file, the prefetch in use, etc… One has to experiment.

+

Examining instance logs, if they seem to be waiting for notification messages for a long time, +not actually doing any transfer, then one might have reached queue saturation. +This often happens at around 40 to 75 instances. Rabbitmq manages a single queue +with a single CPU, and there is a limit to how many notification messages a queue can process +in a given unit of time.

+

If the queue becomes saturated, then we need to partition the subscriptions +into multiple configurations. Each configuration will have a separate queue, +and the queues will get their own CPU’s. With such partitioning, we have gone +to a hundred or so instances and not seen saturation. We don’t know when we run +out of performance.

+

We haven’t needed to scale the broker itself yet.

+
+
+

High Performance Duplicate Suppression

+

One caveat to the use of instances is that suppress_duplicates is ineffective +as the different occurrences of the same file will not be received by the same +instance, and so with n instances, roughly n-1/n duplicates will slip through.

+

In order to properly suppress duplicate file notification messages in data streams +that need multiple instances, one uses winnowing with post_exchangeSplit. +This option sends data to multiple post exchanges based on the data checksum, +so that all duplicate files will be routed to the same winnow process. +Each winnow process runs the normal duplicate suppression used in single instances, +since all files with the same checksum end up with the same winnow, it works. +The winnow processes then post to the exchange used by the real processing +pools.

+

Why is high performance duplicate suppresion a good thing? Because the +availability model of Sarracenia is to have individual application stacks +blindly produce redudant copies of products. It requires no application +adjustment from single node to participating in a cluster. Sarracenia +selects the first result we receive for forwarding. This avoids any sort +of quorum protocol, a source if great complexity in high availability +schemes, and by measuring based on output, minimizes the potential for +systems to appear up, when not actually being completely functional. The +applications do not need to know that there is another stack producing the same +products, which simplifies them as well.

+

more:

+ +
+
+
+

Plugins

+

Default file processing is often fine, but there are also pre-built customizations that +can be used to change processing done by components. The list of pre-built plugins is +in a ‘plugins’ directory wherever the package is installed (viewable with sr_subscribe list) +sample output:

+
$ sr3 list help
+blacklab% sr3 list help
+Valid things to list: examples,eg,ie flow_callback,flowcb,fcb v2plugins,v2p
+
+$ sr3 list fcb
+
+
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/accept/delete.py          flowcb/accept/downloadbaseurl.py
+flowcb/accept/hourtree.py        flowcb/accept/httptohttps.py
+flowcb/accept/longflow.py        flowcb/accept/posthourtree.py
+flowcb/accept/postoverride.py    flowcb/accept/printlag.py
+flowcb/accept/rename4jicc.py     flowcb/accept/renamedmf.py
+flowcb/accept/renamewhatfn.py    flowcb/accept/save.py
+flowcb/accept/speedo.py          flowcb/accept/sundewpxroute.py
+flowcb/accept/testretry.py       flowcb/accept/toclusters.py
+flowcb/accept/tohttp.py          flowcb/accept/tolocal.py
+flowcb/accept/tolocalfile.py     flowcb/accept/wmotypesuffix.py
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py
+flowcb/filter/pclean_f90.py      flowcb/filter/pclean_f92.py
+flowcb/filter/wmo2msc.py         flowcb/gather/file.py
+flowcb/gather/message.py         flowcb/housekeeping/hk_police_queues.py
+flowcb/housekeeping/resources.py flowcb/line_log.py
+flowcb/log.py                    flowcb/mdelaylatest.py
+flowcb/nodupe/data.py            flowcb/nodupe/name.py
+flowcb/pclean.py                 flowcb/poll/airnow.py
+flowcb/poll/mail.py              flowcb/poll/nasa_mls_nrt.py
+flowcb/poll/nexrad.py            flowcb/poll/noaa_hydrometric.py
+flowcb/poll/usgs.py              flowcb/post/message.py
+flowcb/retry.py                  flowcb/sample.py
+flowcb/script.py                 flowcb/send/email.py
+flowcb/shiftdir2baseurl.py       flowcb/v2wrapper.py
+flowcb/wistree.py                flowcb/work/delete.py
+flowcb/work/rxpipe.py
+$
+
+
+

One can browse built-in plugins via the FlowCallback Reference +Plugins are written in python, and users can create their own and place them in ~/.config/sr3/plugins, +or anywhere in the PYTHONPATH (available for import )

+

Another way view documentation and source code of any plugin, the directory containing +them is listed on the first line of the list directive above, and the rest of the path +to the plugin is in the listing, so:

+
vi /home/peter/Sarracenia/sr3/sarracenia/flowcb/nodupe/name.py
+
+
+

will start the vi editor to view the source of the plugin in question, which +also contains its documentation. Another way to view documentation, in addition +to the above, is the standard pythonic way:

+
fractal% python3
+Python 3.10.6 (main, Nov  2 2022, 18:53:38) [GCC 11.3.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import sarracenia.flowcb.run
+>>> help(sarracenia.flowcb.run)
+
+
+

Of importing the class in question, and then invoking python help() on the class.

+

Plugins can be included in flow configurations by adding ‘flow_callback’ lines like:

+
callback work.rxpipe
+
+
+

which appends the given callback to the list of callbacks to be invoked. +There is also:

+
callback_prepend work.rxpipe
+
+
+

which will prepend this callback to the list, so that is is called before the +non prepended ones.

+

To recap:

+
    +
  • To view the plugins currently available on the system sr3 list fcb

  • +
  • To view the contents of a plugin, browse the FlowCallback Reference use, +or use a text editor, or import in a python interpreter, and use python help()

  • +
  • Plugins can have option settings, just like built-in ones. They are described +in each plugin’s documentation.

  • +
  • To set them, place the options in the configuration file before the plugin call itself

  • +
  • To make your own plugins, start with Writing Flow Callbacks, and +put them in ~/.config/sr3/plugins, or anythere in your python environment’s search path.

  • +
+

more:

+ +

Even more: +* Sarracenia Programming Guide +* Writing Flow Callbacks

+
+
+

file_rxpipe

+

The file_rxpipe plugin that writes the names of files downloaded to a named pipe. +Setting this up required two lines in an flow configuration file:

+
$ mknod /home/peter/test/.rxpipe p
+$ sr3 edit subscribe/swob
+
+
+
+

broker amqps://anonymous@dd.weather.gc.ca +topicPrefix v02.post +subtopic observations.swob-ml.#

+

rxpipe_name /home/peter/test/.rxpipe

+

callback work.rxpipe

+

directory /tmp +mirror True +accept .* +# rxpipe is a builtin after_work plugin which writes the name of the file received to +# a pipe named ‘.rxpipe’ in the current working directory.

+
+

With rxpipe, every time a file transfer has completed and is ready for +post-processing, its name is written to the linux pipe (named .rxpipe.)

+
+

Note

+

In the case where a large number of subscribe instances are working +On the same configuration, there is slight probability that notifications +may corrupt one another in the named pipe.

+

FIXME We should probably verify whether this probability is negligeable or not.

+
+
+
+

Anti-Virus Scanning

+

Another example of easy use of a plugin is to achieve anti-virus scanning. +Assuming that ClamAV-daemon is installed, as well as the python3-pyclamd +package, then one can add the following to a subscriber +configuration file:

+
broker amqps://dd.weather.gc.ca
+topicPrefix v02.post
+batch 1
+callback clamav
+subtopic observations.swob-ml.#
+accept .*
+
+
+

So that each file downloaded is AV scanned. Sample run:

+
 $ sr3 foreground subscribe//dd_swob.conf
+
+ blacklab% sr3 foreground subscribe/dd_swob
+ 2022-03-12 18:47:18,137 [INFO] 29823 sarracenia.flow loadCallbacks plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.clamav.Clamav', 'sarracenia.flowcb.log.Log']
+ clam_scan on_part plugin initialized
+ 2022-03-12 18:47:22,865 [INFO] 29823 sarracenia.flowcb.log __init__ subscribe initialized with: {'after_work', 'on_housekeeping', 'after_accept'}
+ 2022-03-12 18:47:22,866 [INFO] 29823 sarracenia.flow run options:
+ _Config__admin=amqp://bunnymaster:Easter1@localhost/ None True True False False None None, _Config__broker=amqps://anonymous:anonymous@dd.weather.gc.ca/ None True True False False None None,
+ _Config__post_broker=None, accel_threshold=0, acceptSizeWrong=False, acceptUnmatched=False, action='foreground', attempts=3, auto_delete=False, baseDir=None, baseUrl_relPath=False, batch=100, bind=True,
+ bindings=[('xpublic', ['v02', 'post'], ['observations.swob-ml.#'])], bufsize=1048576, bytes_per_second=None, bytes_ps=0, cfg_run_dir='/home/peter/.cache/sr3/subscribe/dd_swob', config='dd_swob',
+ configurations=['subscribe/dd_swob'], currentDir=None, dangerWillRobinson=False, debug=False, declare=True, declared_exchanges=['xpublic', 'xcvan01'],
+.
+.
+.
+ 022-03-12 18:47:22,867 [INFO] 29823 sarracenia.flow run pid: 29823 subscribe/dd_swob instance: 0
+ 2022-03-12 18:47:30,019 [INFO] 29823 sarracenia.flowcb.log after_accept accepted: (lag: 140.22 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/COGI/2022-03-12-2344-COGI-AUTO-minute-swob.xml
+.
+.
+.  # good entries...
+
+ 22-03-12 19:00:55,347 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2347-CVPX-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,353 [INFO] 30992 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.00579023 seconds, no viruses in /tmp/dd_swob/2022-03-12-2347-CVPX-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,385 [INFO] 30992 sarracenia.flowcb.log after_accept accepted: (lag: 695.46 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/COTR/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,571 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,596 [INFO] 30992 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.0243611 seconds, no viruses in /tmp/dd_swob/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,637 [INFO] 30992 sarracenia.flowcb.log after_accept accepted: (lag: 695.71 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/CWGD/2022-03-12-2348-CWGD-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,844 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2348-CWGD-AUTO-minute-swob.xml
+
+ .
+ .
+ . # bad entries.
+
+ 2022-03-12 18:50:13,809 [INFO] 30070 sarracenia.flowcb.log after_work downloaded ok: /tmp/dd_swob/2022-03-12-2343-CWJX-AUTO-minute-swob.xml
+ 2022-03-12 18:50:13,930 [INFO] 30070 sarracenia.flowcb.log after_accept accepted: (lag: 360.72 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/CAJT/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+ 2022-03-12 18:50:14,104 [INFO] 30070 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+ 2022-03-12 18:50:14,105 [ERROR] 30070 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.0003829 not forwarding, virus detected in /tmp/dd_swob/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+
+ .
+ . # every heartbeat interval, a little summary:
+ .
+ 2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.clamav on_housekeeping files scanned 121, hits: 5
+
+
+
+
+

Logging and Debugging

+

As sr3 components usually run as a daemon (unless invoked in foreground mode) +one normally examines its log file to find out how processing is going. When only +a single instance is running, one can view the log of the running process like so:

+
sr3 log subscribe/*myconfig*
+
+
+

FIXME: not implemented properly. normally use “foreground” command instead.

+

Where myconfig is the name of the running configuration. Log files +are placed as per the XDG Open Directory Specification. There will be a log file +for each instance (download process) of an flow process running the myflow configuration:

+
in linux: ~/.cache/sarra/log/sr_subscribe_myflow_01.log
+
+
+

One can override placement on linux by setting the XDG_CACHE_HOME environment variable, as +per: XDG Open Directory Specification +Log files can be very large for high volume configurations, so the logging is very configurable.

+

To begin with, one can select the logging level throughout the entire application using +logLevel, and logReject:

+
    +
  • +
    debug

    Setting option debug is identical to use logLevel debug

    +
    +
    +
  • +
  • +
    logLevel ( default: info )

    The level of logging as expressed by python’s logging. Possible values are : critical, error, info, warning, debug.

    +
    +
    +
  • +
  • +
    log_reject <True|False> ( default: False )

    print a log message when rejecting notification messages (choosing not to download the corresponding files)

    +

    The rejection messages also indicate the reason for the rejection.

    +
    +
    +
  • +
+

At the end of the day (at midnight), these logs are rotated automatically by +the components, and the old log gets a date suffix. The directory in which +the logs and metrics are stored can be overridden by the log option, the +number of rotated logs to keep are set by the logRotate parameter. The +oldest log file is deleted when the maximum number of logs and metrics has been +reached and this continues for each rotation. An interval takes a duration of +the interval and it may takes a time unit suffix, such as ‘d|D’ for days, ‘h|H’ +for hours, or ‘m|M’ for minutes. If no unit is provided logs will rotate at midnight. +Here are some settings for log file management:

+
    +
  • +
    log <dir> ( default: ~/.cache/sarra/log ) (on Linux)

    The directory to store log files in.

    +
    +
    +
  • +
  • +
    logMetrics ( default: True )

    whether to accumulate multiple metrics files at all.

    +
    +
    +
  • +
  • +
    statehost <False|True> ( default: False )

    In large data centres, the home directory can be shared among thousands of +nodes. Statehost adds the node name after the cache directory to make it +unique to each node. So each node has it’s own statefiles and logs. +example, on a node named goofy, ~/.cache/sarra/log/ becomes ~/.cache/sarra/goofy/log.

    +
    +
    +
  • +
  • +
    logRotateCount <max_logs> ( default: 5 , alias: lr_backupCount)

    Maximum number of logs archived.

    +
    +
    +
  • +
  • +
    logRotateInterval <duration>[<time_unit>] ( default: 1, alias: lr_interval)

    The duration of the interval with an optional time unit (ie 5m, 2h, 3d)

    +
    +
    +
  • +
  • +
    permLog ( default: 0600 )

    The permission bits to set on log files.

    +
    +
    +
  • +
+
+

flowcb/log.py Debug Tuning

+

In addition to application-options, there is a flowcb that is used by default for logging, which +has additional options:

+
    +
  • logMessageDump (default: off) boolean flag +If set, all fields of a notification message are printed, at each event, rather than just a url/path reference.

  • +
  • +
    logEvents ( default after_accept,after_work,on_housekeeping )

    emit standard log messages at the given points in message processing. +other values: on_start, on_stop, post, gather, … etc…

    +
    +
    +
  • +
+

etc… One can also modify the provided plugins, or write new ones to completely change the logging.

+

more:

+ +
+
+

moth Debug Tuning

+

Turning on logLevel to debug on the entire application often results in inordinately large log files. +By default the Messages Organized into Topic Hierarchies (Moth) parent class for the messaging protocols, +ignores the application-wide debug option. To enable debugging output from these classes, there +are additional settings.

+

One can explicitly set the debug option specifically for the messaging protocol class:

+
set sarracenia.moth.amqp.AMQP.logLevel debug
+set sarracenia.moth.mqtt.MQTT.logLevel debug
+
+
+

will make the messaging layer very verbose. +Sometimes during interoperability testing, one must see the raw notification messages, before decoding by moth classes:

+
messageDebugDump
+
+
+

Either or both of these options will make very large logs, and are best used judiciously.

+

more:

+ +
+
+
+

Housekeeping Metrics

+

Flow Callbacks can implement a on_housekeeping entry point. This entry point is usually +an opportunity for callbacks to print metrics periodically. The builtin log and +resource monitoring callbacks, for example, give lines in the log like so:

+
2022-03-12 19:00:55,114 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=1.97 system=0.3
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Memory threshold set to: 161.2 MiB
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory usage: 53.7 MiB / 161.2 MiB = 33.33%
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.clamav on_housekeeping files scanned 121, hits: 0
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats messages received: 242, accepted: 121, rejected: 121  rate:    50%
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats files transferred: 0 bytes: 0 Bytes rate: 0 Bytes/sec
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats lag: average: 778.91, maximum: 931.06
+
+
+

more:

+ +
+
+

Redundant File Reception

+

In environments where high reliability is required, multiple servers +are often configured to provide services. The Sarracenia approach to +high availability is ´Active-Active´ in that all sources are online +and producing data in parallel. Each source publishes data, +and consumers obtain it from the first source that makes it available, +using checksums to determine whether the given datum has been obtained +or not.

+

This filtering requires implementation of a local dataless pump with +sr_winnow. See the Administrator Guide for more information.

+

more:

+ +
+
+

Web Proxies

+

The best method of working with web proxies is to put the following +in the default.conf:

+
declare env HTTP_PROXY http://yourproxy.com
+declare env HTTPS_PROXY http://yourproxy.com
+
+
+

Putting in default.conf ensures that all subscribers will use +the proxy, not just a single configuration.

+
+
+

API Level Access

+

Sarracenia version 3 also offers python modules that can be called +from existing python applications.

+ +

The flow API brings in all the option placement and parsing from +Sarracenia, it is a pythonic way of starting up a flow from python itself.

+

Or one may not want to use the Sarracenia configuration scheme, +perhaps one just wants to use the message protocol support, for +that:

+ +
+
+

More Information

+
    +
  • The sr3(1) is the definitive source of reference +information for configuration options. For additional information,

  • +
  • the main web site: Sarracenia Documentation

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/How2Guides/v2ToSr3.html b/How2Guides/v2ToSr3.html new file mode 100644 index 000000000..6e3de9a85 --- /dev/null +++ b/How2Guides/v2ToSr3.html @@ -0,0 +1,1524 @@ + + + + + + + Porting V2 Plugins to Sr3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Porting V2 Plugins to Sr3

+

This is a guide to porting plugins from Sarracenia version 2.X (metpx-sarracenia) to +Sarracenia version 3.x (metpx-sr3)

+ +
+

Warning

+

If you are new to Sarracenia, and have no experience or need to look at v2 plugins, +don’t read this. it will just confuse you. This guide is for those who need to take existing +v2 plugins and port them to Sr3. You are better off getting a fresh look by looking at the +jupyter notebook examples which provide an introduction to sr3 without +the confusing references to v2.

+
+
+

Warning

+

Even if you actually need to port v2 plugins to sr3, you still should likely be +familiar with sr3 plugins before attempting to port one. Resources for that:

+

writing Sarra Plugins

+
+

Another resource is the jupyter notebook examples

+

The material here describes how v2 plugins worked in detail, without necessarily +describing sr3 ones. Some knowledge of sr3 callback’s is necessary.

+
+
+

Sample Sr3 plugin

+

Generally speaking v2 plugins were bolted onto existing code to allow some modification +of behaviour. First generation V2 plugins had only single routines declared (e.g. on_message), +while second generation ones used a whole classes (e.g. plugin) were declared, but +still in a stilted way.

+

Sr3 plugins are core design elements, composed together to implement part of +Sarracenia itself. V3 plugins should be easier for Python programmers to debug +and implement, and are more flexible and powerful than the v2 mechanism.

+
+
    +
  • v3 uses standard python syntax, not v2’s strange self.plugins, parent.logger, +and oh gee why doesn’t import work?

  • +
  • Standard python imports: In v3, syntax errors are detected and reported the normal way

  • +
  • v3 classes are designed to be usable outside the CLI itself (see jupyter notebook examples) +callable by application programmers in their own code, like any other python library.

  • +
  • v3 classes can be sub-classed to add core functionality, like new notification message or file +transport protocols.

  • +
+
+
+

Tip

+
+
There are also a couple walkthrough videos on Youtube showing simple v2 -> v3 ports:
+
+
+
+
+

File Placement

+

v2 places configuration files under ~/.config/sarra, and state files under ~/.cache/sarra

+

v3 places configuration files under ~/.config/sr3, and state files under ~/.cache/sr3

+

v2 has a C implementation of sarra called sarrac. The C implementation for v3, is called sr3c, +and is the same as the v2 one, except it uses the v3 file locations.

+
+
+

Command Line Difference

+

Briefly, the sr3 entry point is used to start/stop/status things:

+
v2:  sr_*component* start config
+
+v3:  sr3 start *component*/config
+
+
+

In sr3, one can also use file globbing style specifications to ask for a command +to be invoked on a group of configurations, wheras in v2, one could only operate on one at a time.

+
+

Caution

+

sr3_post is an exception to this change in that it works like v2’s sr3_post did, being +a tool for interactive posting.

+
+
+
+

What Will Work Without Change

+

The first step in porting a configuration subscribe/X to v3, is just to copy the +configuration file from ~/.config/sarra to the corresponding location in ~/.config/sr3 and try:

+
sr3 show subscribe/X
+
+
+

The show command is new in sr3 and provides a way to view the configuration after +it has been parsed. Most of it should work, unless you have do_* plugins. +As an alternative to copying the old configuration file, one can use:

+
sr3 convert subscribe/X
+
+
+

To do all the mechanical changes of directives, and to have a more sr3 centric +configuration file that will better match current documentation.

+

Examples of things that should work:

+
    +
  • all settings from v2 config files should be recognized by the v3 option parser, and converted +to v3 equivalents, ie:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    v2 Option

    sr3 Option

    accept_scp_threshold

    accelThreshold

    heartbeat

    housekeeping

    chmod_log

    permLog

    loglevel

    logLevel

    post_base_url

    post_baseUrl

    post_rate_limit

    messageRateMax

    cache, suppress_duplicates

    nodupe_ttl

    topic_prefix

    topicPrefix

    +

    For the full list, look at the Release Notes

    +

    The topic_prefix in v2 is ‘v02.post’ in v3, the default is ‘v03’. If topic_prefix is omitted +you will need to add the line topicPrefix v02.post to get the same behaviour as v2. Could +also be placed in ~/.config/sr3/default.conf if the case is too common. +One might have to similarly override the sr3 default for post_topicPrefix.

    +
  • +
  • all on_message, on_file, on_post, on_heartbeat, routines will work, by sr3 using +the flowcb/v2wrapper.py plugin which will be automatically invoked when v2 plugins are +seen in the config file.

  • +
+
+

Note

+

Ideally, v2wrapper is used as a crutch to allow one to have a functional configuration +quickly. There is a performance hit to using v2wrapper.

+
+
+
+

What Won’t Work Without Change

+
    +
  • do_* they are just fundamentally different in v3.

  • +
+

If you have a configuration with a do_* plugin, then you need this guide, from day 1. +to set a configuration to use a plugin, in v2 one used the plugin option:

+
plugin <pluginName>
+
+
+

The equivalent to that in v3 is callback:

+
callback <pluginName>
+
+
+

For this shorthand to work there should be a file named <pluginName>.py somewhere in the +PYTHONPATH (~/.config/plugins is added for convenience.) and that python source file needs +to have a class <PluginName> declared in it (same as the file name but first letter capitalized.) +If you need to name it differently there is a longer form that allows one to violate the +convention:

+
flowCallback <pluginName>.MyFavouriteClass
+
+
+

This is equivalent to import <pluginName> followed by instantiating and instance of +the <pluginName>.MyFavoriteClass() so that the entry points get called at the right time. +The individual routine plugin declarations on_message, on_file, etc… are not a way of +doing things in v3. in v3 callbacks are declared, and they contain the entry points you need.

+
    +
  • DESTFNSCRIPT work similar in v3 to v2, but the API is made to match v3 flowCallbacks, +the new routines, one returns the new filename as output, instead of modifying a field +in the notification message.

  • +
+
+
+

Coding Differences between plugins in v2 vs. Sr3

+

The API for adding or customizing functionality in sr3 is quite different from v2. +In general, v3 plugins:

+
    +
  • are usually subclassed from sarracenia.flowcb.FlowCB.

    +

    In v2, one would declare:

    +
    class Msg_Log(object):
    +
    +
    +

    v3 plugins are normal python source files (no magic at the end.) +they are subclassed from sarracenia.flowcb:

    +
    from sarracenia.flowcb import FlowCB
    +
    +class MyPlugin(FlowCB):
    +  ...the rest of the plugin class..
    +
    +   def after_accept(self, worklist):
    +     ...code to run in callback...
    +
    +
    +

    To create an after_accept plugin in MyPlugin class, define a function +with that name, and the appropriate signature.

    +
  • +
  • v3 plugins are pythonic, not weird : +In v2, you need the last line to include something like:

    +
    self.plugin = 'Msg_Delay'
    +
    +
    +

    the first generation ones at the end had something like this to assign entry points explicitly:

    +
    msg_2localfile = Msg_2LocalFile(None)
    +self.on_message = msg_2localfile.on_message
    +
    +
    +

    either way a naive python portion of the file would invariably fail without some sort of test +harness being wrapped around it.

    +
    +

    Tip

    +

    In v3, delete these lines (usually located at the bottom of the file)

    +
    +

    In v2, there were strange issues with imports, resulting in people putting +import statements inside functions. That problem is fixed in v3, you can check your import syntax +by doing import X in any python interpreter.

    +
    +

    Tip

    +

    Put the necessary imports at the beginning of the file, like any other python module +and remove the imports located within functions when porting.

    +
    +
  • +
  • v3 plugins can be used by application programmers. The plugins aren’t +bolted on after the fact, but a core element, implementing duplicate +suppression, reception and transmission of notification messages, file monitoring, +etc.. understanding v3 plugins gives people important clues to being +able to work on sarracenia itself.

    +

    v3 plugins can be imported into existing applications to add the ability +to interact with sarracenia pumps without using the Sarracenia CLI. +see jupyter tutorials.

    +
  • +
  • v3 Plugins now use standard python logging

    +
    import logging
    +
    +
    +

    Make sure the following logger declaration is after the last _import_ in the top of the v3 plugin:

    +
    logger = logging.getLogger(__name__)
    +
    +# To log a notification message:
    +logger.debug( ... )
    +logger.info( ... )
    +logger.warning( ... )
    +logger.error( ... )
    +logger.critical( ... )
    +
    +
    +

    When porting v2 -> v3 plugins: logger.x replaces parent.logger.x. +Sometimes there is also self.logger x… dunno why… don’t ask.

    +
    +

    Tip

    +

    In VI you can use the global replace to make quick work when porting:

    +
    :%s/parent.logger/logger/g
    +
    +
    +
    +
  • +
  • in v2, parent is a mess. The self object varied depending on which entry points were +called. For example, self in __init__ is NOT the same as self in on_message. As a result, all state +is stored in parent. the parent object contained options, and settings, and instance +variables.

    +

    For actual attributes, sr3 now operates the way python programmers expect: self, is +the same self, in __init__() and all the other entry points, so one can set state +for the plugin using self.x attributes in the the plugin code.

    +
  • +
  • v3 plugins have options as an argument to the __init__(self, options): routine rather +than in v2 where they were in the parent object. By convention, in most modules the +__init__ function includes a:

    +
    super().__init__(options,logger)
    +self.o.add_option('OptionName', Type, DefaultValue)
    +
    +
    +
    +

    Tip

    +

    In vi you can use the global replace:

    +
    :%s/parent/self.o/g
    +
    +
    +
    +
  • +
  • v2 options are all lists, sr3 options are typed, and default type is str. +in v2 you will see:

    +
    parent.option[0]
    +
    +
    +

    This shows up because one needs to extract the first value given from the list. +If the option type is not list, should become:

    +
    self.o.option
    +
    +
    +

    This happens often.

    +
  • +
  • you can see what options are active by starting a component with the ‘show’ command**:

    +
    sr3 show subscribe/myconf
    +
    +
    +

    these settings can be accessed from self.o

    +
  • +
  • In sr3 settings, look for replacement of many underscores with camelCase. +Underscore is now reserved for cases where it represents a grouping of options, or +options related to a given class. For example, post_ settings retained the first underscore, but not the rest. so:

    +
      +
    • custom_setting_thing -> customSettingThing

    • +
    • post_base_dir -> post_baseDir

    • +
    • post_broker is unchanged.

    • +
    • post_base_url -> post_baseUrl

    • +
    +

    for example, in a v2 plugin, it would be parent.post_base_url, in v3, the same setting would be self.o.post_baseUrl. +See Upgrading <Upgrading.html> for a list of equivalent options. +See sr3_option(7) <../Reference/sr3_options.7.html> for reference information on each option.

    +
  • +
  • In v2, parent.msg stored the messages, with some fields as built-in attributes, and others as headers. +In v3 notification messages are now python dictionaries , so a v2 msg.relpath becomes msg[‘relPath’] in v3.

    +

    rather than being passed via parent, there is a worklist option passed to those plugin entry points that manipulate +messages. for example, an on_message(self,parent) in a v2 plugin becomes an after_accept(self,worklist) in sr3. +the worklist.incoming contains all the messages that have passed accept/reject filtering, and will be processed +(for download, send, or post) so the logic will look like:

    +
    for msg in worklist.incoming:
    +    do the same logic as in the v2 plugin.
    +    for one message at a time in the loop.
    +
    +
    +

    mappings of all the entry points are described in the Mapping v2 Entry Points to v3 Callbacks +section later in this document.

    +
  • +
  • In V2, a method called correct_extension is implemented in plugins to adapt sundew messages via parent.msg.headers[‘sundew_extension’] to the plugin +specific functionalities.

    +

    In V3, the usability of this method depends on if and how it is being used with live incoming data. Unless testing with live data, it’s best this method remains +untouched.

    +

    Each v3 notification message acts like a python dictionary. Below is a table mapping +fields from the v2 sarra representation to the one in sr3:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    v2

    sr3

    Notes

    msg.pubtime

    msg[‘pubTime’]

    when the message was originally published (standard field)

    msg.baseurl

    msg[‘baseUrl’]

    root of the url tree of posted file (standard field)

    msg.relpath

    msg[‘relPath’]

    relative path concatenated to baseUrl for canonical path

    no equivalent

    msg[‘retrievePath’]

    opaque retrieval path to override canonical one.

    msg.notice

    no equivalent

    calculated from other field on v2 write

    msg.new_subtopic

    msg[‘new_subtopic’]

    avoid in sr3… calculated from relPath

    msg.new_dir

    msg[‘new_dir’]

    name of the directory where files will be written

    msg.new_file

    msg[‘new_file’]

    name of the file to be writen in new_dir

    msg.headers

    msg

    the in memory sr3 message is a dict, includes headers

    msg.headers[‘x’]

    msg[‘x’]

    headers are dict items.

    msg.message_ttl

    msg[‘message_ttl’]

    same setting.

    msg.exchange

    msg[‘exchange’]

    the channel on which the message was received.

    msg.logger

    logger

    pythonic logging setup describe above.

    msg.parts

    msg[‘size’]

    just omit, use sarracenia.Message constructor.

    msg.sumflg

    msg[‘identity’]

    just omit, use sarracenia.Message constructor.

    msg.sumstr

    v2wrapper.sumstrFromMessage(msg)

    the literal string for a v2 checksum field.

    msg.rename

    msg[‘rename’]

    In sr3, often better to use retrievePath and relPath

    parent.msg

    worklist.incoming

    v2 is 1 message at a time, sr3 has lists or messages.

    +
  • +
  • the pubTime, baseUrl, relPath, retrievePath, size, identity, are all standard message fields +better described in sr_post(7)

  • +
  • if one needs to store per message state, then one can declare temporary fields in the message, +that will not be forwarded when the message is published. There is a set field msg[‘_deleteOnPost’]

    +
    msg['my_new_field'] = my_new_value
    +msg['_deleteOnPost'] |= set(['my_new_field'])
    +
    +
    +

    Sarracenia will delete the given field from the message before posting for downstream consumers.

    +
  • +
  • in older versions of v2 (<2.17), there was msg.local_file, and msg.remote_file, some old plugins may contain +that. They represented destination in the subscribe and sender cases, respectively. +both were replaced by new_dir concatenated with new_file to cover both cases. +separation of the directory and file name was considered an improvement.

  • +
  • in v2 parent was the sr_subscribe object, which had all of it’s instance variables, none of which +were intended for use by plugins. In plugin __init__() functions, they may be referred to +as self rather than parent:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    v2

    sr3

    Notes

    parent.cache

    none

    instance of the duplicate suppression cache.

    parent.consumer

    none

    instance of sr_consumer class …

    parent.currentDir

    msg[‘new_dir’] ?

    equivalent depends on purpose of use.

    parent.destination

    self.o.pollUrl

    in a poll

    parent.destination

    self.o.sendTo in

    a sender

    parent.masks

    none

    internals of sr_subscribe class.

    parent.program_name

    self.o.program_name

    name of the program being run e.g. ‘sr_subscribe’

    parent.publisher

    none

    instance of Publisher class from sr_amqp.py

    parent.post_hc

    none

    instance of HostConnect class from sr_amqp.py

    parent.pulls

    self.o.masks

    used in polls, example poll.cocorahs_precip.py

    parent.retry

    none

    instance of the retry queue.

    parent.msg.set_notice(b,r)

    msg[‘baseUrl’] = b, msg[‘relPath’]=r

    v2 uses v2 messages internally, sr3 uses… v3.

    parent.user_cache_dir

    self.o.cfg_run_dir

    actually one level down… new place is better.

    +

    There are dozens (hundreds?) of these attributes that were intended as internal data to the +sr_subscribe class, and should not really be available to plugins. +Most of them don’t show up, but if a developer found someting, it might be present. +Hard to predict what a plugin developer using one of these values intended.

    +
  • +
  • In v3 plugins operate on batches of notification messages. v2 on_message gets parent as a parameter, +and the notification message is in parent.message. In v3, after_accept has worklist as an +option, which is python list of messages, maximum length being fixed by the +batch option. So the general organization for after_accept, and after_work is:

    +
    new_incoming=[]
    +for message in old_list:
    +    if good:
    +       new_incoming.append(message)
    +    if bad:
    +       worklist.rejected.append(message)
    +worklist.incoming=new_incoming
    +
    +
    +
    +

    Note

    +

    plugins must be moved from the /plugins directory to the /flowcb directory, +and specifically, on_message plugins that turn into after_accept ones should be +placed in the flowcb/accept directory (so simialr plugins can be grouped together).

    +
    +

    In after_work, the replacement for v2 on_file, the operations are on:

    +
      +
    • worklist.ok (transfer succeeded.)

    • +
    • worklist.failed (transfers that failed.)

    • +
    +

    In the case of receiving a .tar file and expanding into to individual files, +the after_work routine would change the worklist.ok to contain notification messages for +the individual files, rather than the original collective .tar.

    +
    +

    Note

    +

    on_file plugins that become after_work plugins should be placed in the +/flowcb/after_work directory

    +
    +
  • +
  • Must not set notification message fields (like partstr, sumstr) in plugins. +In v2, one would need to set partstr, and sumstr for v2 notification messages in plugins. +This required an excessive understanding of notification message formats, and meant that +changing notification message formats required modifying plugins (v03 notification message format is +not supported by most v2 plugins, for example). To build a notification message from a +local file in a v3 plugin:

    +
    import sarracenia
    +
    +m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )
    +
    +
    +

    Setting partstr and sumstr are specific to v2 messages, and will not be interpreted +properly in sr3. The encoding of this information is completely different in v03 messages, +and sr3 supports alternate message encodings which may be different again. Setting of these +fields manually is actively counter-productive. The same applies with checksum logic found in v2 plugins. +The checksum is already performed when the new notification message is being generated so most likely +any message fields such as sumalgo and other algo fields can be discarded.

    +

    For an example of using the message builder, look at do_poll -> poll or gather

    +
  • +
  • v3 plugins rarely, involve subclassing of moth or transfer classes. +The sarracenia.moth class implements support for notification message queueing protocols +that support topic hierarchy based subscriptions. There are currently +two subclasses of Moth: amqp (for rabbitmq), and mqtt. It would be +great for someone to add an amq1 (for qpid amqp 1.0 support.)

    +

    It might be reasonable to add an SMTP class there for sending email, +not sure.

    +

    The sarracenia.transfer classes include http, ftp, and sftp today. +They are used to interact with remote services that provide a fileish +interface (supporting things like listing files, and downloading and/or +sending.) Other sub-classes such as S3, IPFS, or webdav, would be +great additions.

    +
  • +
+
+
+

Configuration Files

+

in v2, the primary configuration option to declare a plugin is:

+
plugin X
+
+
+

Generally speaking, there should be a file plugins/x.py +with a class X.py in that file in either ~/.config/plugins +or in the sarra/plugins directory in the package itself. +This is already a second generation style of plugin declaration +in Sarracenia. The original version, one declared individual +entry points:

+
on_message, on_file, on_post, on_..., do_...
+
+
+

In Sr3, the above entries are taken to be requests for v2 +plugins, and should only be used for continuity reasons. +Ideally, one should invoke v3 plugins like so:

+
callback x
+
+
+

Where x will be a subclass of sarracenia.flowcb, which +will contain a class X (first letter capitalized) in the +file x.py somewhere in the python search path, or in the +sarracenia/flowcb directory included as part of the package. +This is actually a shorthand version of the python import. +If you need to declare a callback that does not obey that +convention, one can also use a more flexible but longer-winded:

+
flowcb sarracenia.flowcb.x.X
+
+
+

the above two are equivalent. The flowcb version can be used to import classes +that don’t match the convention of the x.X (a file named x.py containing a class called X)

+
+
+

Configuration Upgrade

+

Once a plugin is ported, one can also arrange for the v3 option parser to recognize a v2 +plugin invocation and replace it with a v3 one. looking in /sarracenia/config.py#L144, +there is a data structure convert_to_v3. A sample entry would be:

+
.
+.
+.
+'on_message' : {
+         'msg_delete': [ 'flowCallback': 'sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles' ]
+.
+.
+.
+
+
+

A v2 config file containing a line on_message msg_delete will be replaced by the parser with:

+
flowCallback sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles
+
+
+
+
+

Options

+

In v2, one would declare settings to be used by a plugin in the __init__ routine, with +the declare_option.:

+
parent.declare_option('poll_usgs_stn_file')
+
+
+

The values are always of type list, so usually, one uses the value by picking the first value:

+
parent.poll_usgs_stn_file[0]
+
+
+

In v3, that would be replaced with:

+
self.o.add_option( option='poll_usgs_stn_file', kind='str', default_value='hoho' )
+
+
+

Where in v3 there are now types ( as seen in the sarracenia/config.py#L777 file) and default value setting included without additional +code. it would be referred to in other routines like so:

+
self.o.poll_usgs_stn_file
+
+
+
+
+

Mapping v2 Entry Points to v3 Callbacks

+

for a comprehensive look at the v3 entry points, have a look at:

+

https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/__init__.py

+

for details.

+
+

on_message, on_post –> after_accept

+

v2: receives one notification message, returns True/False

+
+
v3: receives worklist

modify worklist.incoming +transferring rejected notification messages to worklist.rejected, or worklist.failed.

+
+
+

Sample flow:

+
def after_accept(self, worklist):
+
+   ...
+
+   new_incoming=[]
+   for m in worklist.incoming:
+
+        if message is useful to us:
+           new_incoming.append(m)
+        else
+           worklist.rejected.append(m)
+
+   worklist.incoming = new_incoming
+
+
+
+
examples:

v2: plugins/msg_gts2wistopic.py +v3: flowcb/wistree.py

+
+
+
+
+

on_file –> after_work

+

v2: receives one notification message, returns True/False

+
+
v3: receives worklist

modify worklist.ok (transfer has already happenned.) +transferring rejected notification messages to worklist.rejected, or worklist.failed.

+

can also be used to work on worklist.failed (retry logic does this.)

+
+
examples:

v3: flowcb/work/age.py

+
+
+
+

Danger

+

THERE ARE NO v2 EXAMPLES?!?! +TODO: add some examples +See: Table of v2 and sr3 Equivalents

+
+
+
+

on_heartbeat -> on_housekeeping

+
+
v2: receives parent as argument.

will work unchanged.

+
+
+

v3: only receives self (which should have self.o replacing parent)

+

examples:

+
+
    +
  • v2: hb_cache.py – cleans out cache (references sr_cache.)

  • +
  • v3: flowcb/nodupe.py – implements entire caching routine.

  • +
+
+
+
+

do_poll -> poll or gather

+

v2: call do_poll from plugin.

+
+
    +
  • +
    protocol to use the do_poll routine is identified by registered_as() entry point

    which is mandatory to provide.

    +
    +
    +
  • +
  • requires manually constructing fields for notification messages, is notification message verison specific, +(generally do not support v03 notification messages.)

  • +
  • explicitly calls poll entry points.

  • +
  • runs, one must worry about whether one has the vip or not to decide what processing +to do in each plugin.

  • +
  • poll_without_vip setting available.

  • +
  • parent.pulls is a list of get directives (which are different from accept)

  • +
+
+

There is a common pattern in v2 polls, where a do_poll is paired with download_something plugins +where a partial message is built with the poll and the download (or do_download) one is specialized +to do the actual download. Often in sr3 one can craft a message that will be successfully downloaded +with the built-in processing.

+

An example of custom download processing is to build the directory tree to download into, combined with +the use of a rename header (in v2 parent.msg.rename) One can now use “retrievePath” to define the url +to issue to the server, and “relPath” to define where it will be downloaded to. RelPath includes +the whole directory tree, where rename is only for the filename. The combination of relPath and +retrievePath often provides enough functionality to obviate the need for a download entry point.

+

There is another common pattern in v2 polls where, rather than querying a remote server to find out +what new products are available, in sr3 we have the concept of a scheduled flow, where there is a fixed +list of requests done periodically. See Scheduled Flow for more on that. For typical polls, the migration +to sr3 follows:

+

v3: define poll in a flowcb class.

+
+
    +
  • poll only runs when has_vip is true. (so remove any has_vip() tests, unneeded.) +also consult section on virtual ip addresses below.

  • +
  • registered_as() entry point is moot.

  • +
  • gather runs always, and is used to subscribe to post done by node that has the vip, +allowing the nodupe cache to be kept uptodate.

  • +
  • api defined to build notification messages from file data regardless of notification message format.

  • +
  • get is gone, poll uses accept like any other component.

  • +
  • the combination with download plugins is generally replaced by a single plugin that implements +alternate naming using retrievePath field. so it is all done in one plugin.

  • +
  • returns a list of notification messages to be filtered and posted.

  • +
  • the download setting allows a poll to download in a single configuration without +requiring combination with a separate downloading configuration.

  • +
+
+

To build a notification message, without a local file, use fromFileInfo sarracenia.message factory:

+
import dateparser
+import paramiko
+import sarracenia
+
+gathered_messages=[]
+
+m = sarracenia.Message.fromFileInfo(sample_fileName, cfg)
+
+
+

builds an notification message from scratch.

+

One can also build and supply a simulated stat record to fromFileInfo factory, +using the paramiko.SFTPAttributes() class. For example, using the dateparser +routines to convert. However, the remote server lists the date and time, as well +as determines the file size and permissions in effect:

+
pollmtime = dateparser.parse( ... , settings={ ... TO_TIMEZONE='utc' } )
+mtimestamp = time.mktime( pollmtime.timetuple() )
+
+fsize = info_from_poll #about the size of the file to download
+st = paramiko.SFTPAttributes()
+st.st_mtime=mtimstamp
+st.st_atime=mtimestamp
+st.st_size=fsize
+st.st_mode=0o666
+m = sarracenia.Message.fromFileInfo(sample_fileName, cfg, st)
+
+
+

One should fill in the SFTPAttributes record if possible, since the duplicate +cache use metadata if available. The better the metadata, the better the +detection of changes to existing files.

+

Once the notification message is built, append it to the list:

+
gathered_messages.append(m)
+
+
+

and at the end:

+
return gathered_messages
+
+
+
+
+

Virtual IP processing in poll

+

In v2 if you have a vIP set, all participating nodes poll the upstream server +and maintain the list of current files, they just don’t publish the result. +So if you have 8 servers sharing a vIP, all eight are polling, kind of sad. +There is also the poll_no_vip setting, and plugins often have to check if they +have the vIP or not.

+

In v3, only the server with the vIP polls. The plugins don’t need to check. +The other participating servers subscribe to where the poll posts to, +to update their recent_files cache.

+
+
examples:
    +
  • flowcb/poll/airnow.py

  • +
+
+
+

In a v2 poll, output exchanges were sometimes quite popular exchanges (e.g. xpublic) +which would cause the duplicate_suppression queues in an sr3 poll to be much +larger than necessary.

+

When using a poll in sr3, ideally the post_exchange is one dedicated to this +poll, so that the vip participants prime their duplicate suppression cache with +only items published by the poll.

+
+
+

Scheduled Flow

+

If there is a WISKIS ( https://www.kisters.net/wiski ) server, one needs to issue +time centric queries are regular intervals. so a gather() entry point is implemented +which returns a list of messages that a downloader will use to obtain the data.

+ +

Like a poll, one can use the download option to consume the messages by downloading in the same configuration, +or publish to an exchange for downloading by a separate subscriber or sarra to scale downloading.

+
+
+

on_html_page -> subclass flowcb/poll

+

Here is a v2 plugin nsa_mls_nrt.py:

+
#!/usr/bin/env python3
+
+class Html_parser():
+
+    def __init__(self,parent):
+
+        parent.logger.debug("Html_parser __init__")
+        import html.parser
+
+        self.parent = parent
+        self.logger = parent.logger
+
+        self.parser = html.parser.HTMLParser()
+        self.parser.handle_starttag = self.handle_starttag
+        self.parser.handle_data     = self.handle_data
+
+
+    def handle_starttag(self, tag, attrs):
+        for attr in attrs:
+            c,n = attr
+            if c == "href" and n[-1] != '/':
+               self.myfname = n.strip().strip('\t')
+
+    def handle_data(self, data):
+        import time
+
+        if 'MLS-Aura' in data:
+               self.logger.debug("data %s" %data)
+               self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' +'_' + ' ' + 'Jan 1 00:01' + ' ' + data
+               self.logger.debug("(%s) = %s" % (self.myfname,self.entries[self.myfname]))
+        if self.myfname == None : return
+        if self.myfname == data : return
+        '''
+        # at this point data is a filename like
+        name = data.strip().strip('\t')
+
+        parts = name.split('_')
+        if len(parts) != 3 : return
+
+        words = parts[1].split('.')
+        sdate  = ' '.join(words[:4])
+        t      = time.strptime(sdate,'%Y %j %H %M')
+
+        # accept file if 1 month old in sec  60 sec* 60min * 24hr * 31days
+
+        epochf = time.mktime(t)
+        now    = time.time()
+        elapse = now - epochf
+
+        if elapse > self.month_in_secs : return
+
+        # build an ls line from date in file ... size set to 0  since not provided
+
+        mydate = time.strftime('%b %d %H:%M',t)
+
+        mysize = '_'
+
+        self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' + mysize + ' ' + mydate + ' ' + data
+        self.logger.debug("(%s) = %s" % (self.myfname,self.entries[self.myfname]))
+        '''
+
+    def parse(self,parent):
+        self.logger.debug("Html_parser parse")
+        self.entries = {}
+        self.myfname = None
+
+        self.logger.debug("data %s" % parent.data)
+        self.parser.feed(parent.data)
+        self.parser.close()
+
+        parent.entries = self.entries
+
+        return True
+
+html_parser = Html_parser(self)
+self.on_html_page = html_parser.parse
+
+
+

The plugin has a main “parse” routine, which invokes the html.parser class, where data_handler +is called for each line, gradually building the self.entries dictionary where each entry is +a string constructed to resemble a line of ls command output.

+

This plugin is a near exact copy of the html_page.py plugin used by default. +The on_html_page entry point for plugins is replaced by a completely different +mechanism. Most of the logic of v2 poll in sr3 is in the new sarracenia.FlowCB.Poll class. +Logic from the v2 plugins/html_page.py, used by default, is now part of this +new Poll class, subclassed from flowcb, so basic HTML parsing is built-in.

+

Another change from v2 is that there was far more string manipulation in the old +version. in sr3 polls, most string maniupulation has been replaced by filling an +paramiko.SFTPAttributes structure as soon as possible.

+

So the way to replace on_html_page in sr3 is by sub-classing Poll. Here is an +sr3 version of same plugin (nasa_mls_nrt.py):

+
import logging
+import paramiko
+import sarracenia
+from sarracenia import nowflt, timestr2flt
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+class Nasa_mls_nrt(Poll):
+
+    def handle_data(self, data):
+
+        st = paramiko.SFTPAttributes()
+        st.st_mtime = 0
+        st.st_mode = 0o775
+        st.filename = data
+
+        if 'MLS-Aura' in data:
+               logger.debug("data %s" %data)
+               #self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' +'_' + ' ' + 'Jan 1 00:01' + ' ' + data
+               self.entries[data]=st
+
+               logger.info("(%s) = %s" % (self.myfname,st))
+        if self.myfname == None : return
+        if self.myfname == data : return
+
+
+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/nasa_mls_nrt.py ) +and matching config file provided here: +( https://github.com/MetPX/sarracenia/blob/development/sarracenia/examples/poll/nasa-mls-nrt.conf )

+

The new class is declared as a subclass of Poll, and only the needed +The HTML routine (handle_data) need be written to override the behaviour +provided by the parent class.

+

This solution is less than half the size of the v2 one, and permits +all manner of flexibility by allowing replacement of any or all elements +of the poll class.

+
+
+
+

on_line -> poll subclassing

+

Similarly to on_html_page above, all uses of on_line in the previous version +were about re-formatting lines to be parseable. the on_line routine can be +similarly sub-classed to replace it. One had to modify the parent.line +string to be parseable by the built in ls style line parsing.

+

In sr3, on_line is expected to return a populated paramiko.SFTPAttributes field, similar +to the way on_html_page works (but only a single one instead of a dictionary of them.) +With the more flexible date parsing in sr3, there has been no identified need for on_line +on which to build an example.

+
+
+

do_send -> send:

+

v2: do_send could be either a standalone routine, or associated with a protocol type

+
    +
  • based on registered_as() so the destination determines whether it is used or not.

  • +
  • accepts parent as an argument.

  • +
  • returns True on success, False on failure.

  • +
  • will typically have a registered_as() entry point to say which protocols to use a sender for.

  • +
+

v3: send(self,msg)

+
    +
  • use the provided msg to do sending.

  • +
  • returns True on success, False on failure.

  • +
  • registered as is not used anymore, can be deleted.

  • +
  • The send entry_point overrides all sends, and is not protocol specific. +To add support for new protocols, subclass sarracenia.transfer instead.

  • +
+
+
examples:
    +
  • flowcb/send/email.py

  • +
+
+
+
+
+

do_download -> download:

+

create a flowCallback class with a download entry point.

+
    +
  • accepts a single notification message as an argument.

  • +
  • returns True if download succeeds.

  • +
  • if it returns False, the retry logic applies (download will be called again +then placed on the retry queue.)

  • +
  • use msg[‘new_dir’], msg[‘new_file’], msg[‘new_inflight_path’] +to respect settings such as inflight and place file properly. +(unless changing that is motivation for the plugin.)

  • +
  • might be a good idea to verify the checksum of the downloaded data. +if the checksum of the file downloaded does not agree with what is in +the notification message, duplicate suppression fails, and looping results.

  • +
  • one case of download is when retrievalURL is not a normal file download. +in v03, there is a retrievePath fields for exactly this case. This new feature +can be used to eliminate the need for download plugins. Example:

    +

    in v2:

    +
    +
    +

    is ported to sr3:

    +
    +
    +

    The ported result sets the new field retrievePath ( retrieval path ) instead of new_dir and new_file +fields, and normal processing of the retrievePath field in the notification message will do a good download, no +plugin required.

    +
  • +
  • In many poll situations (typically a plugin with a do_poll and do_download entry point), the sr3 +built-in downloading often “just works”, the sr3 poll() or gather() entry point is typically configured +with a retrievePath to indicate the URL to get, and the relPath is set to indicate the file name +to download into.

  • +
+
+

DESTFNSCRIPT

+

DESTFNSCRIPT is re-cast as a flowcb entry point, where the directive is now formatted +similarly to the flowcallback in the configuration

+

v2 configuration:

+
accept .*${HOSTNAME}.*AWCN70_CWUL.*       DESTFNSCRIPT=sender_renamer_add_date.py
+
+
+

v2 plugin code:

+
import sys, os, os.path, time, stat
+
+# this renamer takes file name like : AACN01_CWAO_160316___00009:cmcin:CWAO:AA:1:Direct:20170316031754
+# and returns :                       AACN01_CWAO_160316___00009_20170316031254
+
+class Renamer(object):
+
+  def perform(self,parent):
+
+      path = parent.new_file
+      tok=path.split(":")
+
+      datestr = time.strftime('%Y%m%d%H%M%S',time.gmtime())
+      #parent.logger.info('Valeur_path: %s' % datstr)
+
+      new_path=tok[0] + '_' + datestr
+      parent.new_file = new_path
+      return True
+
+renamer=Renamer()
+self.destfn_script=renamer.perform
+
+
+

Notes:

+
    +
  • the v2 plugin returns True, and one must set a new_file field to change the name.

  • +
  • in sr3, the return value is the name of the file, so review all return statements.

  • +
  • in sr3, there is no need to update any message fields, sinc that is taken care of. +just return the new name.

  • +
+

Turns into sr3

+

sr3 configuration:

+
accept .*${HOSTNAME}.*AWCN70_CWUL.*       DESTFNSCRIPT=sender_renamer_add_date.Sender_Renamer_Add_Date
+
+
+

In sr3, as for any flowcallback invocation, one needs to use a traditional python class invocation +and add to it the name of the class within the file. This notation is equivalent to python from +statement from sender_renamer_add_date import Sender_Renamer_Add_Date

+

flow callback code:

+
import logging,time
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+class Sender_Renamer_Add_Date(FlowCB):
+
+   def destfn(self,msg) -> str:
+
+       logger.info('before: m=%s' % msg )
+       relPath = msg["relPath"].split('/')
+       datestr = time.strftime('%Y%m%d%H%M%S',time.gmtime())
+       return relPath[-1] + '_' + datestr
+
+
+

Example of debugging sr3 destfn functions:

+
fractal% python3
+Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> from sender_renamer_add_date import Sender_Renamer_Add_Date
+>>> fb=Sender_Renamer_Add_Date(None)
+>>> msg = { 'relPath' : 'relative/path/to/file.txt' }
+>>> fb.destfn(msg)
+'file.txt_20220725130328'
+>>>
+
+
+
+
+
+

v3 only: post,gather

+

The polling/posting is actually done in flow callback (flowcb) classes. +The exit status does not matter, all such routines will be called in order.

+

The return of a gather is a list of notification messages to be appended to worklist.incoming

+

The return of post is undefined. The whole point is to create a side-effect +that affects some other process or server.

+
+
examples:
    +
  • flowcb/gather/file.py - read files from disk (for post and watch)

  • +
  • flowcb/gather/message.py - how notification messages are received by all components

  • +
  • flowcb/post/message.py - how notification messages are posted by all components.

  • +
  • flowcb/poll/nexrad.py - this polls NOAA’s AWS server for data. +install a configuration to use it with sr3 add poll/aws-nexrad.conf

  • +
+
+
+
+
+

v3 Complex Examples

+
+

flowcb/nodupe

+

duplicate suppression in v3, has:

+
    +
  • an after_accept routing the prunes duplicates from worklist.incoming. +( adding non-dupes to the reception cache.)

  • +
+
+
+

flowcb/retry

+
+
    +
  • has an after_accept function to append notification messages to the +incoming queue, in order to trigger another attempt to process them.

  • +
  • has an after_work routine doing something unknown… FIXME.

  • +
  • has a post function to take failed downloads and put them +on the retry list for later consideration.

  • +
+
+
+
+
+

Table of v2 and sr3 Equivalents

+

Here is an overview of plugins included in Sarracenia, +One can browse the two trees, and using the table below, +can review, compare and contrast the implementations.

+ +

The naming also gives an example of the name convention mapping… e.g. plugins whos v2 name start with:

+
    +
  • msg_… -> filter/… or accept/…

  • +
  • file_… -> work/…

  • +
  • poll_… -> poll/… or gather/…

  • +
  • hb_… -> housekeeping/…

  • +
+

are mapped to the sr3 conventional directories at right.

+

Relative paths from the above given folders are in the table:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

V2 plugins (all in one directory…)

Sr3 flow callbacks (treeified)

data_…

+

modify file data during transfer

+

subclass sarracenia.transfer class instead

+

no example available consult source code

+

destfn_sample.py

destfn/sample.py

file_age.py

work/age.py

file_delete.py

work/delete.py

file_email.py

send/email.py

file_rxpipe.py

work/rxpipe.py

hb_memory

housekeeping/resources.py

html_page.py

subclass sarracenia.transfer class instead.

+

no example available consult source code.

+

also see poll/nasa_mls_nrt.py for example of +parsing custom resmote server lines.

+

msg_2http.py

accept/tohttp.py

msg_2localfile.py, msg_2local.py (not sure)

accept/tolocalfile.py

msg_delete.py

filter/deleteflowfiles.py

msg_fdelay.py

filter/fdelay.py

msg_filter_wmo2msc.py

filter/wmo2msc.py

msg_log.py,file_log.py, hb_log.py, post_log.py

log.py

msg_pclean.py, msg_pclean_f90.py

+

msg_pclean_f92.py

+

pclean.py +filter/pcleanf90.py

+

filter/pcleanf92.py <../Reference/flowcb.html#module-sarracenia.flowcb.filter.pcleanf92>`_

+

post_rate_limit.py

built-in messageRateMax processing

msg_rename_dmf.py

accept/renamedmf.py

msg_rename_whatfn.py

accept/renamewhatfn.py

msg_rename4jicc.py

accept/rename4jicc.py

msg_stopper.py

built-in messageCountMax processing

msg_sundew_pxroute.py

accept/sundewpxroute.py

msg_speedo.py

accept/speedo.py

msg_to_clusters.py

accept/toclusters.py

msg_WMO_type_suffix.py

accept/wmotypesuffix.py

hard-coded built-in duplicate suppression

+

hb_cache.py

+

nodupe/__init__.py

hard-coded built-in message subscriber

gather/message.py

hard-coded built-in message poster

post/message.py

hard-coded built-in file scan or noticing.

gather/file.py

hard-coded builtin retry logic

+

hb_retry.py

+

retry.py

poll_email.py

poll/mail.py

poll_nexrad.py

poll/nexrad.py

poll_noaa.py

poll/noaa_hydrometric.py

poll_usgs.py

poll/usgs.py

spare

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/code.html b/Reference/code.html new file mode 100644 index 000000000..123da35d5 --- /dev/null +++ b/Reference/code.html @@ -0,0 +1,2639 @@ + + + + + + + Code Reference — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Code Reference

+

Internal Documentation on the full source of sr3 +Not an API. subject to change without notice. +What is missing:

+
    +
  • All flowcallbacks are here: flowcb

  • +
  • the entry points are excluded (not compatible with sphinx/autodoc.)

  • +
+ +
+

sarracenia

+
+
+class sarracenia.Message[source]
+

Bases: dict

+

A notification message in Sarracenia is stored as a python dictionary, with a few extra management functions.

+

The internal representation is very close to the v03 format defined here: https://metpx.github.io/sarracenia/Reference/sr_post.7.html

+

Unfortunately, sub-classing of dict means that to copy it from a dict will mean losing the type, +and hence the need for the copyDict member.

+
+
+__init__()[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+computeIdentity(path, o, offset=0)[source]
+

check extended attributes for a cached identity sum calculation. +if extended attributes are present, and +* the file mtime is not too new, and +* the cached sum us using the same method +then use the cached value.

+

otherwise, calculate a checksum. +set the file’s extended attributes for the new value. +the method of checksum calculation is from options.identity.

+

sets the message ‘identity’ field if appropriate.

+
+ +
+
+copyDict(d)[source]
+

copy dictionary into message.

+
+ +
+
+dumps() str[source]
+

FIXME: used to be msg_dumps. +print a message in a compact but relatively compact way. +msg is a python dictionary. if there is a field longer than maximum_field_length, +truncate.

+
+ +
+
+static fromFileData(path, o, lstat=None)[source]
+

create a message based on a given file, calculating the checksum. +returns a well-formed message, or none.

+
+ +
+
+static fromFileInfo(path, o, lstat=None)[source]
+

based on the fiven information about the file (it’s name and a stat record if available) +and a configuration options object (sarracenia.config.Config) +return an sarracenia.Message suitable for placement on a worklist.

+

A message is a specialized python dictionary with a certain set of fields in it. +The message returned will have the necessary fields for processing and posting.

+

The message is built for a file is based on the given path, options (o), and lstat (output of os.stat)

+

The lstat record is used to build ‘atime’, ‘mtime’ and ‘mode’ fields if +timeCopy and permCopy options are set.

+

if no lstat record is supplied, then those fields will not be set.

+
+ +
+
+static fromStream(path, o, data=None)[source]
+

Create a file and message for the given path. +The file will be created or overwritten with the provided data. +then invoke fromFileData() for the resulting file.

+
+ +
+
+getContent(options=None)[source]
+

Retrieve the data referred to by a message. The data may be embedded +in the messate, or this routine may resolve a link to an external server +and download the data.

+

does not handle authentication. +This routine is meant to be used with small files. using it to download +large files may be very inefficient. Untested in that use-case.

+

Return value is the data.

+

often on server where one is publishing data, the file is available as +a local file, and one can avoid the network usage by using a options.baseDir setting. +this behaviour can be disabled by not providing the options or not setting baseDir.

+
+ +
+
+setReport(code, text=None)[source]
+

FIXME: used to be msg_set_report +set message fields to indicate result of action so reports can be generated.

+

set is supposed to indicate final message dispositions, so in the case +of putting a message on worklist.failed… no report is generated, since +it will be retried later. FIXME: should we publish an interim failure report?

+
+ +
+
+updatePaths(options, new_dir=None, new_file=None)[source]
+

set the new_* fields in the message based on changed file placement. +if new_* options are ommitted updaste the rest of the fields in +the message based on their current values.

+

If you change file placement in a flow callback, for example. +One would change new_dir and new_file in the message. +This routines updates other fields in the message (e.g. relPath, +baseUrl, topic ) to match new_dir/new_file.

+

msg[‘post_baseUrl’] defaults to msg[‘baseUrl’]

+
+ +
+
+validate()[source]
+

FIXME: used to be msg_validate +return True if message format seems ok, return True, else return False, log some reasons.

+
+ +
+ +
+
+class sarracenia.Sarracenia[source]
+

Bases: object

+

Core utilities of Sarracenia. The main class here is sarracenia.Message. +a Sarracenia.Message is subclassed from a dict, so for most uses, it works like the +python built-in, but also we have a few major entry points some factoryies:

+

Building a message from a file

+

m = sarracenia.Message.fromFileData( path, options, lstat )

+

builds a notification message from a given existing file, consulting options, a parsed +in memory version of the configuration settings that are applicable

+

Options

+

see the sarracenia.config.Config class for functions to parse configuration files +and create corresponding python option dictionaries. One can supply small +dictionaries for example:

+
options['topicPrefix'] = [ 'v02', 'post' ]
+options['bindings'] = [ ('xpublic', [ 'v02', 'post'] , [ '#' ] )]
+options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_SomethingHelpfulToYou'
+
+
+

Above is an example of a minimal options dictionary taken from the tutorial +example called moth_api_consumer.py. often

+

If you don’t have a file

+

If you don’t have a local file, then build your notification message with:

+

m = sarracenia.Message.fromFileInfo( path, options, lstat )

+

where you can make up the lstat values to fill in some fields in the message. +You can make a fake lstat structure to provide these values using sarracenia.filemetadata +class which is either an alias for paramiko.SFTPAttributes +( https://docs.paramiko.org/en/latest/api/sftp.html#paramiko.sftp_attr.SFTPAttributes ) +if paramiko is installed, or a simple emulation if not.

+

from sarracenia.filemetadata import FmdStat

+

lstat = FmdStat() +lstat.st_mtime= utcinteger second count in UTC (numeric version of a Sarracenia timestamp.) +lstat.st_atime= +lstat.st_mode=0o644 +lstat.st_size= size_in_bytes

+

optional fields that may be of interest: +lstat.filename= “nameOfTheFile” +lstat.longname= ‘lrwxrwxrwx 1 peter peter 20 Oct 11 20:28 nameOfTheFile’

+

that you can then provide as an lstat argument to the above fromFileInfo() +call. However the notification message returned will lack an identity checksum field. +once you get the file, you can add the Identity field with:

+

m.computeIdentity(path, o):

+

In terms of consuming notification messages, the fields in the dictionary provide metadata +for the announced resource. The anounced data could be embedded in the notification message itself, +or available by a URL.

+

Messages are generally gathered from a source such as the Message Queueing Protocol wrapper +class: moth… sarracenia.moth.

+

data = m.getContent()

+

will return the content of the announced resource as raw data.

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+class sarracenia.TimeConversions[source]
+

Bases: object

+

Time conversion routines.

+
    +
  • os.stat, and time.now() return floating point

  • +
  • The floating point representation is a count of seconds since the beginning of the epoch.

  • +
  • beginning of epoch is platform dependent, and conversion to actual date is fraught (leap seconds, etc…)

  • +
  • Entire SR_* formats are text, no floats are sent over the protocol +(avoids byte order issues, null byte / encoding issues, and enhances readability.)

  • +
  • str format: YYYYMMDDHHMMSS.msec goal of this representation is that a naive +conversion to floats yields comparable numbers.

  • +
  • but the number that results is not useful for anything else, so need these +special routines to get a proper epochal time.

  • +
  • also OK for year 2032 or whatever (rollover of time_t on 32 bits.)

  • +
  • string representation is forced to UTC timezone to avoid having to communicate timezone.

  • +
+

timestr2flt() - accepts a string and returns a float.

+

caveat

+

FIXME: this encoding will break in the year 10000 (assumes four digit year) +and requires leading zeroes prior to 1000. One will have to add detection of +the decimal point, and change the offsets at that point.

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+sarracenia.durationToSeconds(str_value, default=None) float[source]
+

this function converts duration to seconds. +str_value should be a number followed by a unit [s,m,h,d,w] ex. 1w, 4d, 12h +return 0.0 for invalid string.

+
+ +
+
+sarracenia.durationToString(d) str[source]
+

given a numbner of seconds, return a short, human readable string.

+
+ +
+
+sarracenia.stat(path) SFTPAttributes[source]
+

os.stat call replacement which improves on it by returning +and SFTPAttributes structure, in place of the OS stat one, +featuring:

+
    +
  • mtime and ctime with subsecond accuracy

  • +
  • fields that can be overridden (not immutable.)

  • +
+
+ +
+
+sarracenia.timeflt2str(f=None)[source]
+

timeflt2str - accepts a float and returns a string.

+

flow is a floating point number such as returned by time.now() +(number of seconds since beginning of epoch.)

+

the str is YYYYMMDDTHHMMSS.sssss

+

20210921T011331.0123

+

translates to: Sept. 21st, 2021 at 01:13 and 31.0123 seconds. +always UTC timezone.

+
+ +
+
+

sarracenia.config

+

Second version configuration parser

+

FIXME: pas 2023/02/05… missing options from v2: max_queue_size, outlet, pipe

+
+
+class sarracenia.config.Config(parent=None)[source]
+

Bases: object

+

The option parser to produce a single configuration.

+

it can be instantiated with one of:

+
    +
  • one_config(component, config, action, isPost=False) – read the options for +a given component an configuration, (all in one call.)

  • +
+

On the other hand, a configu can be built up from the following constructors:

+
    +
  • default_config() – returns an empty configuration, given a config file tree.

  • +
  • no_file_config() – returns an empty config without any config file tree.

  • +
+

Then just add settings manually:

+
cfg = no_file_config()
+
+cfg.broker = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+cfg.topicPrefix = [ 'v02', 'post']
+cfg.component = 'subscribe'
+cfg.config = 'flow_demo'
+cfg.action = 'start'
+cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]
+cfg.queueName='q_anonymous.subscriber_test2'
+cfg.download=True
+cfg.batch=1
+cfg.messageCountMax=5
+
+# set the instance number for the flow class.
+cfg.no=0
+
+
+

# and at the end call finalize

+

cfg.finalize()

+
+
+__deepcopy__(memo) Configuration[source]
+

code for this from here: https://stackoverflow.com/questions/1500718/how-to-override-the-copy-deepcopy-operations-for-a-python-object +Needed for python < 3.7ish? (ubuntu 18) found this bug: https://bugs.python.org/issue10076 +deepcopy fails for objects with re’s in them? +ok on ubuntu 20.04

+
+ +
+
+__init__(parent=None) Config[source]
+

instantiate an empty Configuration

+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+_build_mask(option, arguments)[source]
+

return new entry to be appended to list of masks

+
+ +
+
+_parse_binding(subtopic_string)[source]
+
+
FIXME: see original parse, with substitions for url encoding.

also should sqwawk about error if no exchange or topicPrefix defined. +also None to reset to empty, not done.

+
+
+
+ +
+
+_parse_set_string(v: str, old_value: set) set[source]
+

given a set string, return a python set.

+
+ +
+
+_parse_setting(opt, value)[source]
+

v3 plugin accept options for specific modules.

+

parsed from: +set sarracenia.flowcb.log.filter.Log.level debug

+

example: +opt= sarracenia.flowcb.log.filter.Log.level value = debug

+

results in: +self.settings[ sarracenia.flowcb.log.filter.Log ][level] = debug

+

options should be fed to plugin class on instantiation. +stripped of class… +* options = { ‘level’ : ‘debug’ }

+
+ +
+
+_parse_v2plugin(entryPoint, value)[source]
+

config file parsing for a v2 plugin.

+
+ +
+
+_resolve_exchange()[source]
+

based on the given configuration, fill in with defaults or guesses. +sets self.exchange.

+
+ +
+
+_sundew_basename_parts(pattern, basename)[source]
+

modified from metpx SenderFTP

+
+ +
+
+_validate_urlstr(urlstr) tuple[source]
+

returns a tuple ( bool, expanded_url ) +the bool is whether the expansion worked, and the expanded_url is one with +the added necessary authentication details from sarracenia.Credentials.

+
+ +
+
+_varsub(word)[source]
+

substitute variable values from options

+
+ +
+
+class addBinding(option_strings, dest, nargs=None, const=None, default=None, type=None, choices=None, required=False, help=None, metavar=None)[source]
+

Bases: Action

+

called by argparse to deal with queue bindings.

+
+
+__call__(parser, namespace, values, option_string)[source]
+

Call self as a function.

+
+ +
+ +
+
+add_option(option, kind='list', default_value=None, all_values=None)[source]
+

options can be declared in any plugin. There are various kind of options, where the declared type modifies the parsing.

+
    +
  • ‘count’ integer count type.

  • +
  • ‘octal’ base-8 (octal) integer type.

  • +
  • +
    ‘duration’ a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)

    modified by a unit suffix ( m-minute, h-hour, w-week )

    +
    +
    +
  • +
  • ‘flag’ boolean (True/False) option.

  • +
  • ‘float’ a simple floating point number.

  • +
  • +
    ‘list’ a list of string values, each succeeding occurrence catenates to the total.

    all v2 plugin options are declared of type list.

    +
    +
    +
  • +
  • +
    ‘set’ a set of string values, each succeeding occurrence is unioned to the total.

    if all_values is provided, then constrain set to that.

    +
    +
    +
  • +
  • ‘size’ integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.

  • +
  • +
    ‘str’ an arbitrary string value, as will all of the above types, each

    succeeding occurrence overrides the previous one.

    +
    +
    +
  • +
+

If a value is set to None, that could mean that it has not been set.

+
+ +
+
+applyComponentDefaults(component)[source]
+

overlay defaults options for the given component to the given configuration.

+
+ +
+
+dictify()[source]
+

return a dict version of the cfg…

+
+ +
+
+dump()[source]
+

print out what the configuration looks like.

+
+ +
+
+finalize(component=None, config=None)[source]
+

Before final use, take the existing settings, and infer any missing needed defaults from what is provided. +Should be called prior to using a configuration.

+

There are default options that apply only if they are not overridden…

+
+ +
+
+mask_ppstr(mask)[source]
+

return a pretty print string version of the given mask, easier for humans to read.

+
+ +
+
+merge(oth)[source]
+

merge to lists of options.

+

merge two lists of options if one is cumulative then merge, +otherwise if not None, then take value from oth

+
+ +
+
+override(oth)[source]
+

override a value in a set of options.

+

why override() method and not just assign values to the dictionary? +in the configuration file, there are various ways to have variable substituion. +override invokes those, so that they are properly interpreted. Otherwise, +you just end up with a literal value.

+
+ +
+
+parse_args(isPost=False)[source]
+
+
user information:

accept a configuration, apply argParse library to augment the given configuration +with command line settings.

+

the post component has a different calling convention than others, so use that flag +if called from post.

+
+
development notes:

Use argparse.parser to modify defaults. +FIXME, many FIXME notes below. this is a currently unusable placeholder. +have not figured this out yet. many issues.

+

FIXME #1: +parseArgs often sets the value of the variable, regardless of it’s presence (normally a good thing.) +( if you have ‘store_true’ then default needed, for broker, just a string, it ignores if not present.) +This has the effect of overriding settings in the file parsed before the arguments. +Therefore: often supply defaults… but… sigh…

+

but there is another consideration stopping me from supplying defaults, wish I remembered what it was. +I think it is: +FIXME #2: +arguments are parsed twice: once to get basic stuff (loglevel, component, action) +and if the parsing fails there, the usage will print the wrong defaults…

+
+
+
+ +
+
+parse_file(cfg, component=None)[source]
+

add settings from a given config file to self

+
+ +
+
+sundew_dirPattern(pattern, urlstr, basename, destDir)[source]
+

does substitutions for patterns in directories.

+
+ +
+
+variableExpansion(cdir, message=None) str[source]
+
+

replace substitution patterns, variable substitutions as described in +https://metpx.github.io/sarracenia/Reference/sr3_options.7.html#variables

+

returns: the given string with the substiturions done.

+
+
examples: ${YYYYMMDD-70m} becomes 20221107 assuming that was the current date 70 minutes ago.

environment variables, and built-in settings are replaced also.

+
+
+
+

timeoffset -70m

+
+ +
+ +
+
+sarracenia.config.config_path(subdir, config, mandatory=True, ctype='conf')[source]
+

Given a subdir/config look for file in configish places.

+

return Tuple: Found (True/False), path_of_file_found|config_that_was_not_found

+
+ +
+
+sarracenia.config.get_log_filename(hostdir, component, configuration, no)[source]
+

return the name of a single logfile for a single instance.

+
+ +
+
+sarracenia.config.get_metrics_filename(hostdir, component, configuration, no)[source]
+

return the name of a single logfile for a single instance.

+
+ +
+
+sarracenia.config.get_pid_filename(hostdir, component, configuration, no)[source]
+

return the file name for the pid file for the specified instance.

+
+ +
+
+sarracenia.config.get_user_cache_dir(hostdir)[source]
+

hostdir = None if statehost is false,

+
+ +
+
+sarracenia.config.logger = <Logger sarracenia.config (WARNING)>
+

respect appdir stuff using an environment variable. +for not just hard coded as a class variable appdir_stuff

+
+
Type:
+

FIXME

+
+
+
+ +
+
+sarracenia.config.no_file_config()[source]
+

initialize a config that will not use Sarracenia configuration files at all. +meant for use by people writing independent programs to start up instances +with python API calls.

+
+ +
+
+class sarracenia.config.octal_number(value)[source]
+

Bases: int

+
+
+static __new__(cls, value)[source]
+
+ +
+
+__repr__() str[source]
+

Return repr(self).

+
+ +
+
+__str__() str[source]
+

Return str(self).

+
+ +
+ +
+
+sarracenia.config.one_config(component, config, action, isPost=False)[source]
+

single call return a fully parsed single configuration for a single component to run.

+

read in admin.conf and default.conf

+

apply component default overrides ( maps to: component/check ?) +read in component/config.conf +parse arguments from command line. +return config instance item.

+

appdir_stuff can be to override file locations for testing during development.

+
+ +
+
+sarracenia.config.str_options = ['action', 'admin', 'baseDir', 'broker', 'cluster', 'directory', 'exchange', 'exchange_suffix', 'feeder', 'filename', 'flatten', 'flowMain', 'header', 'hostname', 'identity', 'inlineEncoding', 'logLevel', 'pollUrl', 'post_baseUrl', 'post_baseDir', 'post_broker', 'post_exchange', 'post_exchangeSuffix', 'post_format', 'post_topic', 'queueName', 'sendTo', 'rename', 'report_exchange', 'source', 'strip', 'timezone', 'nodupe_ttl', 'nodupe_driver', 'nodupe_basis', 'tlsRigour', 'topic']
+

for backward compatibility,

+

convert some old plugins that are hard to get working with +v2wrapper, into v3 plugin.

+

the fdelay ones makes in depth use of sr_replay function, and +that has changed in v3 too much.

+

accelerators and rate limiting are now built-in, no plugin required.

+
+ +
+
+

sarracenia.credentials

+
+
+class sarracenia.credentials.Credential(urlstr=None)[source]
+

Bases: object

+

An object that holds information about a credential, read from a +credential file, which has one credential per line, format:

+
url option1=value1, option2=value2
+
+
+
+
Examples::

sftp://alice@herhost/ ssh_keyfile=/home/myself/mykeys/.ssh.id_dsa +ftp://georges:Gpass@hishost/ passive = True, binary = True

+
+
+

Format Documentation.

+
+
+url
+

object with URL, password, etc.

+
+
Type:
+

urllib.parse.ParseResult

+
+
+
+ +
+
+ssh_keyfile
+

path to SSH key file for SFTP

+
+
Type:
+

str

+
+
+
+ +
+
+passive
+

use passive FTP mode, defaults to True

+
+
Type:
+

bool

+
+
+
+ +
+
+binary
+

use binary FTP mode, defaults to True

+
+
Type:
+

bool

+
+
+
+ +
+
+tls
+

use FTPS with TLS, defaults to False

+
+
Type:
+

bool

+
+
+
+ +
+
+prot_p
+

use a secure data connection for TLS

+
+
Type:
+

bool

+
+
+
+ +
+
+bearer_token
+

bearer token for HTTP authentication

+
+
Type:
+

str

+
+
+
+ +
+
+login_method
+

force a specific login method for AMQP (PLAIN, +AMQPLAIN, EXTERNAL or GSSAPI)

+
+
Type:
+

str

+
+
+
+ +

Usage:

+
+

# build a credential from a url string:

+

from sarracenia.credentials import Credential

+

broker = Credential(‘amqps://anonymous:anonymous@hpfx.collab.science.gc.ca’)

+
+
+
+__init__(urlstr=None)[source]
+

Create a Credential object.

+
+
Parameters:
+

urlstr (str) – a URL in string form to be parsed.

+
+
+
+ +
+
+__str__()[source]
+

Returns attributes of the Credential object as a readable string.

+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+class sarracenia.credentials.CredentialDB(Unused_logger=None)[source]
+

Bases: object

+

Parses, stores and manages Credential objects.

+
+
+credentials
+

contains all sarracenia.credentials.Credential objects managed by the CredentialDB.

+
+
Type:
+

dict

+
+
+
+ +
+
Usage:

# build a credential via lookup in the normal files: +import CredentialDB from sarracenia.credentials

+

credentials = CredentialDB.read( “/the/path/to/the/credentials.conf” )

+

# if there are corresponding passwords or modulation of login information look it up.

+

broker = credentials.get( “amqps://hpfx.collab.science.gc.ca” ) +remote = credentials.get( “sftp://hoho@theserver” )

+
+
+
+
+__init__(Unused_logger=None)[source]
+

Create a CredentialDB.

+
+
Parameters:
+

Unused_logger – logger argument no longer used… left there for API compat with old calls.

+
+
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+_parse(line)[source]
+

Parse a line of a credentials file, add it to the CredentialDB.

+
+
Parameters:
+

line (str) – line to be parsed.

+
+
+
+ +
+
+_resolve(urlstr, url=None)[source]
+

Resolve credentials for AMQP vhost from ones passed as a string, and optionally a urllib.parse.ParseResult +object, into a sarracenia.credentials.Credential object.

+
+
Parameters:
+
    +
  • urlstr (str) – credentials in a URL string.

  • +
  • url (urllib.parse.ParseResult) – ParseResult object with creds.

  • +
+
+
Returns:
+

+
containing

result (bool): False if the creds were not in the CredentialDB. True if they were. +details (sarracenia.credentials.Credential): the updated Credential object, or None.

+
+
+

+
+
Return type:
+

tuple

+
+
+
+ +
+
+add(urlstr, details=None)[source]
+

Add a new credential to the DB.

+
+
Parameters:
+
    +
  • urlstr (str) – string-formatted URL to be parsed and added to DB.

  • +
  • details (sarracenia.credentials.Credential) – a Credential object can be passed in, otherwise one is +created by parsing urlstr.

  • +
+
+
+
+ +
+
+get(urlstr)[source]
+

Retrieve a Credential from the DB by urlstr. If the Credential is valid, but not already cached, it will be +added to the CredentialDB.

+
+
Parameters:
+

urlstr (str) – credentials as URL string to be parsed.

+
+
Returns:
+

+
containing
+
cache_result (bool): True if the credential was retrieved from the CredentialDB cache, False

if it was not in the cache. Note that False does not imply the Credential or urlstr is +invalid.

+
+
credential (sarracenia.credentials.Credential): the Credential

object matching the urlstr, None if urlstr is invalid.

+
+
+
+
+

+
+
Return type:
+

tuple

+
+
+
+ +
+
+has(urlstr)[source]
+

Return True if the Credential matching the urlstr is already in the CredentialDB.

+
+
Parameters:
+

urlstr (str) – credentials in a URL string.

+
+
+
+ +
+
+isTrue(S)[source]
+

Returns True if s is true, yes, on or 1.

+
+
Parameters:
+

S (str) – string to check if true.

+
+
+
+ +
+
+isValid(url, details=None)[source]
+

Validates a URL and Credential object. Checks for empty passwords, schemes, etc.

+
+
Parameters:
+
    +
  • url (urllib.parse.ParseResult) – ParseResult object for a URL.

  • +
  • details (sarracenia.credentials.Credential) – sarra Credential object containing additional details about +the URL.

  • +
+
+
Returns:
+

True if a URL is valid, False if not.

+
+
Return type:
+

bool

+
+
+
+ +
+
+read(path)[source]
+

Read in a file containing credentials (e.g. credentials.conf). All credentials are parsed and added to the +CredentialDB.

+
+
Parameters:
+

path (str) – path of file to be read.

+
+
+
+ +
+ +
+
+

sarracenia.diskqueue

+
+
+class sarracenia.diskqueue.DiskQueue(options, name)[source]
+

Bases: object

+

Process Persistent Queue…

+

Persist messages to a file so that processing can be attempted again later. +For safety reasons, want to be writing to a file ASAP. +For performance reasons, all those writes need to be Appends.

+

so continuous, but append-only io… with an occasional housekeeping cycle. +to resolve them

+

not clear if we need multi-task safety… just one task writes to each queue.

+

retry_ttl how long

+

self.retry_cache

+
    +
  • a dictionary indexed by some sort of key to prevent duplicate messages being stored in it.

  • +
+

retry_path = ~/.cache/sr3/<component>/<config>/diskqueue_<name>

+

with various suffixes:

+

.new – messages added to the retry list are appended to this file.

+

whenever a message is added to the retry_cache, it is appended to a +cumulative list of entries to add to the retry list.

+

every housekeeping interval, the two files are consolidated.

+

note that the ack_id of messages retreived from the retry list, is +removed. Files must be acked around the time they are placed on the +retry_list, as reception from the source should have already been acknowledged.

+
+
FIXME: would be fun to look at performance of this thing and compare it to

python persistent queue. the differences:

+

This class does no locking (presumed single threading.) +could add locks… and they would be coarser grained than stuff in persistentqueue +this should be faster than persistent queue, but who knows what magic they did. +This class doesn’t implement in-memory queue… it is entirely on disk… +saves memory, optimal for large queues. +probably good, since retries should be slow…

+

not sure what will run better.

+
+
+
+
+__init__(options, name)[source]
+
+ +
+
+__len__() int[source]
+

Returns the total number of messages in the DiskQueue.

+

Number of messages in the DiskQueue does not necessarily equal the number of messages available to get. +Messages in the .new file are counted, but can’t be retrieved until +on_housekeeping() has been run.

+
+
Returns:
+

number of messages in the DiskQueue.

+
+
Return type:
+

int

+
+
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+_count_msgs(file_path) int[source]
+

Count the number of messages (lines) in the queue file. This should be used only when opening an existing +file, because get() does not remove messages from the file.

+
+
Parameters:
+

file_path (str) – path to the file to be counted.

+
+
Returns:
+

count of messages in file, -1 if the file could not be read.

+
+
Return type:
+

int

+
+
+
+ +
+
+cleanup()[source]
+

remove statefiles.

+
+ +
+
+close()[source]
+

clean shutdown.

+
+ +
+
+get(maximum_messages_to_get=1)[source]
+

qty number of messages to retrieve from the queue.

+
+ +
+
+in_cache(message) bool[source]
+

return whether the entry is message is in the cache or not. +side effect: adds it.

+
+ +
+
+is_expired(message) bool[source]
+

return is the given message expired ?

+
+ +
+
+msg_get_from_file(fp, path)[source]
+

read a message from the state file.

+
+ +
+
+needs_requeuing(message) bool[source]
+

return +* True if message is not expired, and not already in queue. +* False otherwise.

+
+ +
+
+on_housekeeping()[source]
+
+
read rest of queue_file (from current point of unretried ones.)
    +
  • check if message is duplicate or expired.

  • +
  • write to .hk

  • +
+
+
read .new file,
    +
  • check if message is duplicate or expired.

  • +
  • writing to .hk (housekeeping)

  • +
+
+
+

remove .new +rename housekeeping to queue for next period.

+
+ +
+
+put(message_list)[source]
+

add messages to the end of the queue.

+
+ +
+ +
+
+

sarracenia.filemetadata

+
+
+class sarracenia.filemetadata.FileMetadata(path)[source]
+

Bases: object

+

This class implements storing metadata with a file.

+

on unlix/linux/mac systems, we use extended attributes, +where we apply a user.sr_ prefix to the attribute names to avoid clashes.

+

on Windows NT, create an “sr_.json” Alternate Data Stream to store them.

+

API:

+

All values are utf-8, hence readable by some subset of humans. +not bytes. no binary, go away…

+

x = sr_attr( path ) <- read metadata from file. +x.list() <- list all extant extended attributes.

+
    +
  • sample return value: [ ‘sum’, ‘mtime’ ]

  • +
+

x.get(‘sum’) <- look at one value.

+
    +
  • returns None if missing.

  • +
+

x.set(‘sum’, ‘hoho’) <- set one value.

+
    +
  • fails silently (fall-back gracefully.)

  • +
+

x.persist() <- write metadata back to file, if necessary.

+
+
+__init__(path)[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+get(name) str[source]
+

return the value of the named extended attribute.

+
+ +
+
+list()[source]
+

return the list of defined extended attributes. (keys to the dict.)

+
+ +
+
+persist()[source]
+

write the in-memory extended attributes to disk.

+
+ +
+
+set(name, value)[source]
+

set the name & value pair to the extended attributes for the file.

+
+ +
+ +
+
+

sarracenia.flow

+
+
+class sarracenia.flow.Flow(cfg=None)[source]
+

Bases: object

+

Implement the General Algorithm from the Concepts Guide.

+

All of the component types (e.g. poll, subscribe, sarra, winnow, shovel ) are implemented +as sub-classes of Flow. The constructor/factory accept a configuration +(sarracenia.config.Config class) with all the settings in it.

+

This class takes care of starting up, running with callbacks, and clean shutdown.

+

need to know whether to sleep between passes +o.sleep - an interval (floating point number of seconds) +o.housekeeping -

+

A flow processes worklists of messages

+

worklist given to callbacks…

+
    +
  • worklist.incoming –> new messages to continue processing

  • +
  • worklist.ok –> successfully processed

  • +
  • worklist.rejected –> messages to not be further processed.

  • +
  • worklist.failed –> messages for which processing failed.

  • +
  • worklist.dirrectories_ok –> directories created.

  • +
+

Initially all messages are placed in incoming. +if a callback decides:

+
    +
  • a message is not relevant, it is moved to rejected.

  • +
  • all processing has been done, it moves it to ok.

  • +
  • an operation failed and it should be retried later, move to retry

  • +
+

callbacks must not remove messages from all worklists, re-classify them. +it is necessary to put rejected messages in the appropriate worklist +so they can be acknowledged as received.

+

interesting data structure: +self.plugins – dict of modular functionality metadata.

+
    +
  • self.plugins[ “load” ] contains a list of (v3) flow_callbacks to load.

  • +
  • self.plugins[ entry_point ] - one for each invocation times of callbacks. examples: +“on_start”, “after_accept”, etc… contains routines to run at each entry_point

  • +
+
+
+__init__(cfg=None)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+_runCallbackMetrics()[source]
+

Collect metrics from plugins with a metricsReport entry point.

+

Expects the plugin to return a dictionary containing metrics, which is saved to self.metrics[plugin_name].

+
+ +
+
+_runHousekeeping(now) float[source]
+

Run housekeeping callbacks +Return the time when housekeeping should be run next

+
+ +
+
+do_download() None[source]
+
+
do download work for self.worklist.incoming, placing files:

successfully downloaded in worklist.ok +temporary failures in worklist.failed +permanent failures (or files not to be downloaded) in worklist.rejected

+
+
+
+ +
+
+do_send()[source]
+
+ +
+
+download(msg, options) bool[source]
+

download/transfer one file based on message, return True if successful, otherwise False.

+
+ +
+
+file_should_be_downloaded(msg) bool[source]
+

determine whether a comparison of local_file and message metadata indicates that it is new enough +that writing the file locally is warranted.

+

return True to say downloading is warranted.

+
+

False if the file in the message represents the same or an older version that what is corrently on disk.

+
+

origin: refactor & translation of v2: content_should_not_be downloaded

+
+
Assumptions:

new_path exists… there is a file to compare against.

+
+
+
+ +
+
+has_vip() list[source]
+

return list of vips which are active on the current machine, or an empty list.

+
+ +
+
+link1file(msg, symbolic=True) bool[source]
+

perform a link of a single file, based on a message, returning boolean success +if it’s Symbolic, then do that. else do a hard link.

+

imported from v2/subscribe/doit_download “link event, try to link the local product given by message”

+
+ +
+
+mkdir(msg) bool[source]
+

perform an mkdir.

+
+ +
+
+reject(m, code, reason) None[source]
+

reject a message.

+
+ +
+
+removeOneFile(path) bool[source]
+

process an unlink event, returning boolean success.

+
+ +
+
+renameOneItem(old, path) bool[source]
+

for messages with an rename file operation, it is to rename a file.

+
+ +
+
+run()[source]
+

This is the core routine of the algorithm, with most important data driven +loop in it. This implements the General Algorithm (as described in the Concepts Explanation Guide) +check if stop_requested once in a while, but never return otherwise.

+
+ +
+
+set_local_file_attributes(local_file, msg)[source]
+

after a file has been written, restore permissions and ownership if necessary.

+
+ +
+
+sundew_getDestInfos(msg, currentFileOption, filename)[source]
+

modified from sundew client

+

WHATFN – First part (‘:’) of filename +HEADFN – Use first 2 fields of filename +NONE – Use the entire filename +TIME or TIME: – TIME stamp appended +DESTFN=fname – Change the filename to fname

+

ex: mask[2] = ‘NONE:TIME’

+
+ +
+
+updateFieldsAccepted(msg, urlstr, pattern, maskDir, maskFileOption, mirror, path_strip_count, pstrip, flatten) None[source]
+

Set new message fields according to values when the message is accepted.

+
    +
  • urlstr: the urlstr being matched (baseUrl+relPath+sundew_extension)

  • +
  • pattern: the regex that was matched.

  • +
  • maskDir: the current directory to base the relPath from.

  • +
  • maskFileOption: filename option value (sundew compatibility options.)

  • +
  • strip: number of path entries to strip from the left side of the path.

  • +
  • pstrip: pattern strip regexp to apply instead of a count.

  • +
  • flatten: a character to replace path separators with toe change a multi-directory +deep file name into a single long file name

  • +
+
+ +
+
+write_inline_file(msg) bool[source]
+

write local file based on a message with inlined content.

+
+ +
+ +
+
+

sarracenia.flow.poll

+
+
+class sarracenia.flow.poll.Poll(options)[source]
+

Bases: Flow

+

repeatedly query a remote (non-sarracenia) server to list the files there. +post messages (to post_broker) for every new file discovered there.

+

the sarracenia.flowcb.poll class is used to implement the remote querying, +and is highly customizable to that effect.

+

if the vip option is set, +* subscribe to the same settings that are being posted to. +* consume all the messages posted, keeping new file duplicate cache updated.

+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.post

+
+
+class sarracenia.flow.post.Post(options)[source]
+

Bases: Flow

+

post messages about local files.

+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.report

+
+
+class sarracenia.flow.report.Report(options)[source]
+

Bases: Flow

+

forward report messages.

+

Not really implemented at the moment. It is just a shovel synonym for now. +more logic should be added.

+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.sarra

+
+
+class sarracenia.flow.sarra.Sarra(options)[source]
+

Bases: Flow

+
    +
  • download files from a remote server to the local one

  • +
  • modify the messages so they refer to the downloaded files.

  • +
  • re-post them to another exchange for the next other subscribers.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.sender

+
+
+class sarracenia.flow.sender.Sender(options)[source]
+

Bases: Flow

+
    +
  • subscribe to a stream of messages about local files.

  • +
  • send the files to a remote server.

  • +
  • modify the messages to refer to the remote file copies.

  • +
  • post the messages for subscribers of the remote server.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.shovel

+
+
+class sarracenia.flow.shovel.Shovel(options)[source]
+

Bases: Flow

+
    +
  • subscribe to some messages.

  • +
  • post them somewhere else.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.subscribe

+
+
+class sarracenia.flow.subscribe.Subscribe(options)[source]
+

Bases: Flow

+
    +
  • subscribe to messages about files.

  • +
  • download the corresponding files.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.watch

+
+
+class sarracenia.flow.watch.Watch(options)[source]
+

Bases: Flow

+
    +
  • create messages for files that appear in a directory.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.flow.winnow

+
+
+class sarracenia.flow.winnow.Winnow(options)[source]
+

Bases: Flow

+
    +
  • subscribe to a stream of messages.

  • +
  • suppress duplicates,

  • +
  • post the thinned out stream somewhere else.

  • +
+
+
+__init__(options)[source]
+

The cfg is should be an sarra/config object.

+
+ +
+ +
+
+

sarracenia.instance

+
+
+class sarracenia.instance.RedirectedTimedRotatingFileHandler(filename, when='h', interval=1, backupCount=0, encoding=None, delay=False, utc=False, atTime=None, errors=None)[source]
+

Bases: TimedRotatingFileHandler

+
+
+doRollover()[source]
+

do a rollover; in this case, a date/time stamp is appended to the filename +when the rollover happens. However, you want the file to be named for the +start of the interval, not the current time. If there is a backup count, +then we have to get a list of matching filenames, sort them and remove +the one with the oldest suffix.

+
+ +
+ +
+
+class sarracenia.instance.instance[source]
+

Bases: object

+

Process management for a single flow instance. +start and stop instances.

+

this is the main entry point launched from the sr3 cli, with arguments for it to turn into a specific configuration.

+
+
+__init__()[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+start()[source]
+

Main element to run a single flow instance. It parses the command line arguments twice. +the first pass, is to initialize the log file and debug level, and select the configuration file to parse. +Once the log file is set, and output & error re-direction is in place, the second pass begins:

+

The configuration files are parsed, and then the options are parsed a second time to act +as overrides to the configuration file content.

+

As all process management is handled by sr.py, the action here is not parsed, but always either +start (daemon) or foreground (interactive)

+
+ +
+ +
+
+

sarracenia.identity

+
+
+class sarracenia.identity.Identity[source]
+

Bases: object

+

A class for algorithms to get a fingerprint for a file being announced. +Appropriate fingerprinting algorithms vary according to file type.

+

required methods in subclasses:

+
+
def registered_as(self):

return a one letter string identifying the algorithm (mostly for v2.) +in v3, the registration comes from the identity sub-class name in lower case.

+
+
def set_path(self,path):

start a checksum for the given path… initialize.

+
+
def update(self,chunk):

update the checksum based on the given bytes from the file (sequential access assumed.)

+
+
+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+update_file(path)[source]
+

read the entire file, check sum it. +this is kind of last resort as it cost an extra file read. +It is better to call update( as the file is being read for other reasons.

+
+ +
+
+property value
+

return the current value of the checksum calculation.

+
+ +
+ +
+
+

sarracenia.identity.arbitrary

+
+
+class sarracenia.identity.arbitrary.Arbitrary[source]
+

Bases: Identity

+

For applications where there is no known way of determining equivalence, allow them to supply +an arbitrary tag, that can be used to compare products for duplicate suppression purposes.

+

use setter to set the value… some sort of external checksum algorithm that cannot be reproduced.

+
+
+__init__()[source]
+
+ +
+
+property value
+

return the current value of the checksum calculation.

+
+ +
+ +
+
+

sarracenia.identity.sha512

+
+
+class sarracenia.identity.sha512.Sha512[source]
+

Bases: Identity

+

The SHA512 algorithm to checksum the entire file, which is called ‘s’.

+
+ +
+
+

sarracenia.identity.md5

+
+
+class sarracenia.identity.md5.Md5[source]
+

Bases: Identity

+

use the (obsolete) Message Digest 5 (MD5) algorithm, applied on the content +of a file, to generate an identity signature.

+
+
+static registered_as()[source]
+

v2name.

+
+ +
+ +
+
+

sarracenia.identity.random

+
+
+class sarracenia.identity.random.Random[source]
+

Bases: Identity

+

Trivial minimalist checksumming algorithm, returns random number for any file.

+
+
+property value
+

return the current value of the checksum calculation.

+
+ +
+ +
+
+

sarracenia.moth

+
+
+class sarracenia.moth.Moth(props=None, is_subscriber=True)[source]
+

Bases: object

+
+

Moth … Messages Organized by Topic Headers +(en français: Messages organisés par thème hierarchique. )

+

A multi-protocol library for use by hierarchical message passing implementations, +(messages which have a ‘topic’ header that is used for routing by brokers.)

+
    +
  • regardless of protocol, the message format returned should be the same.

  • +
  • the message is turned into a sarracenia.Message object, which acts like a python +dictionary, corresponding to key-value pairs in the message body, and properties.

  • +
  • topic is a special key that may end up in the message body, or some sort of property +or metadata.

  • +
  • the protocol should support acknowledgement under user control. Such control indicated +by the presence of an entry_point called “ack”. The entry point accepts “ack_id” as +a message identifier to be passed to the broker. Whatever protocol symbol is used +by the protocol, it is passed through this message property. Examples: +in rabbitmq/amqp ack takes a “delivery_tag” as an argument, in MQTT, it takes a “message-id” +so when receiving an AMQP message, the m[‘ack_id’] is assigned the delivery_tag from the message.

  • +
  • There is a special dict item: “_DeleteOnPost”, +to identify keys which are added only for local use. +they will be removed from the message when publishing. +examples: topic (sent outside body), message-id (used for acknowledgements.) +new_basedir, ack_id, new_… (settings…)

  • +
+

Intent is to be specialized for topic based data distribution (MQTT style.) +API to allow pass-through of protocol specific properties, but apply templates for genericity.

+

Target protocols (and corresponding libraries.): AMQP, MQTT, ?

+

Things to specify:

+
    +
  • broker

  • +
  • topicPrefix

  • +
  • subTopic

  • +
  • queueName (for amqp, used as client-id for mqtt)

  • +
+

this library knows nothing about Sarracenia, the only code used from sarracenia is to interpret +duration properties, from the root sarracenia/__init__.py, the broker argument from sarracenia.credentials

+

usage:

+
import sarracenia.moth
+import sarracenia.credentials
+
+
+props = sarracenia.moth.default_options
+props['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+props['expire'] = 300
+props['batch'] = 1
+is_subscriber=True
+
+c= Moth( props, is_subscriber  )
+
+messages = c.newMessages()
+
+# if there are new messages from a publisher, return them, otherwise return
+# an empty list []].
+
+p=Moth( { 'batch':1 }, False )
+
+p.putNewMessage()
+
+p.close()
+# tear down connection.
+
+
+

Initialize a broker connection. Connections are unidirectional. +either for subscribing (with subFactory) or publishing (with pubFactory.)

+

The factories return objects subclassed to match the protocol required +by the broker argument.

+

arguments to the factories are:

+
    +
  • broker … the url of the broker to connect to.

  • +
  • props is a dictionary or properties/parameters.

  • +
  • supplied as overrides to the default properties listed above.

  • +
+

Some may vary among protocols:

+
Protocol     library implementing    URL to select
+--------     --------------------    -------------
+
+AMQPv0.9 --> amqplib from Celery --> amqp, amqps
+
+AMQPv0.9 --> pika                --> pika, pikas
+
+MQTTv3   --> paho                --> mqtt, mqtts
+
+AMQPv1.0 --> qpid-proton         --> amq1, amq1s
+
+
+
+

messaging_strategy

+

how to manage the connection. Covers whether to treat the connection +as new or assume it is set up. Also, If something goes wrong. +What should be done.

+
    +
  • reset: on startup… erase any state, and re-initialize.

  • +
  • stubborn: If set to True, loop forever if something bad happens. +Never give up. This sort of setting is desired in operations, especially unattended. +if set to False, may give up more easily.

  • +
  • failure_duration is to advise library how to structure connection service level.

    +
      +
    • 5m - make a connection that will recover from transient errors of a few minutes, +but not tax the broker too much for prolonged outages.

    • +
    • 5d - duration outage to striving to survive connection for five days.

    • +
    +
  • +
+

Changing recovery_strategy setting, might result in having to destroy and re-create +consumer queues (AMQP.)

+

Options

+

both

+
    +
  • ‘topicPrefix’ : [ ‘v03’ ]

  • +
  • ‘messageDebugDump’: False, –> enable printing of raw messages.

  • +
  • ‘inline’: False, - Are we inlining content within messages?

  • +
  • ‘inlineEncoding’: ‘guess’, - what encoding should we use for inlined content?

  • +
  • ‘inlineByteMax’: 4096, - Maximum size of messages to inline.

  • +
+

for get

+
    +
  • ‘batch’ : 100 # how many messages to get at once

  • +
  • ‘broker’ : an sr_broker ?

  • +
  • ‘queueName’ : Mandatory, name of a queue. (only in AMQP… hmm…)

  • +
  • ‘bindings’ : [ list of bindings ]

  • +
  • ‘loop’

  • +
+

optional:

+
    +
  • ‘message_ttl’

  • +
+

for put:

+
    +
  • ‘exchange’ (only in AMQP… hmm…)

  • +
+
+
+__init__(props=None, is_subscriber=True) None[source]
+

If is_subscriber=True, then this is a consuming instance. +expect calls to get* routines.

+

if is_subscriber=False, then expect/permit only calls to put*

+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+ack(message: Message) None[source]
+

tell broker that a given message has been received.

+

ack uses the ‘ack_id’ property to send an acknowledgement back to the broker.

+
+ +
+
+cleanup() None[source]
+

get rid of server-side resources associated with a client. (queues/id’s, etc…)

+
+ +
+
+property default_options: dict
+

get default properties to override, used by client for validation.

+
+ +
+
+static findAllSubclasses(cls) set[source]
+

Recursively finds all subclasses of a class. __subclasses__() only gives direct subclasses.

+
+ +
+
+getNewMessage() Message[source]
+

If there is one new message available, return it. Otherwise return None. Do not block.

+
+
side effects:

metrics. +self.metrics[‘RxByteCount’] should be incremented by size of payload. +self.metrics[‘RxGoodCount’] should be incremented by 1 if a good message is received. +self.metrics[‘RxBadCount’] should be incremented by 1 if an invalid message is received (&discarded.)

+
+
+
+ +
+
+metricsDisconnect() None[source]
+

tear down an existing connection.

+
+ +
+
+newMessages() list[source]
+

If there are new messages available from the broker, return them, otherwise return None.

+

On Success, this routine returns immediately (non-blocking) with either None, or a list of messages.

+

On failure, this routine blocks, and loops reconnecting to broker, until interaction with broker is successful.

+
+ +
+
+putNewMessage(message: Message, content_type: str = 'application/json', exchange: str | None = None) bool[source]
+

publish a message as set up to the given topic.

+

return True is succeeded, False otherwise.

+
+
side effect

self.metrics[‘TxByteCount’] should be incremented by size of payload. +self.metrics[‘TxGoodCount’] should be incremented by 1 if a good message is received. +self.metrics[‘TxBadCount’] should be incremented by 1 if an invalid message is received (&discarded.)

+
+
+
+ +
+ +
+
+

sarracenia.moth.amqp

+
+
+class sarracenia.moth.amqp.AMQP(props, is_subscriber)[source]
+

Bases: Moth

+

implementation of the Moth API for the amqp library, which is built to talk to rabbitmq brokers in 0.8 and 0.9 +AMQP dialects.

+

to allow acknowledgements we map: AMQP’ ‘delivery_tag’ to the ‘ack_id’

+

additional AMQP specific options:

+

exchangeDeclare - declare exchanges before use. +queueBind - bind queue to exchange before use. +queueDeclare - declare queue before use.

+
+
+__connect(broker) bool
+

connect to broker. +returns True if connected, false otherwise. +* side effect: self.channel set to a new channel.

+

Expect caller to handle errors.

+
+ +
+
+__init__(props, is_subscriber) None[source]
+

connect to broker, depending on message_strategy stubborness, remain connected.

+
+ +
+
+ack(m: Message) None[source]
+

do what you need to acknowledge that processing of a message is done.

+
+ +
+
+getNewMessage() Message[source]
+

If there is one new message available, return it. Otherwise return None. Do not block.

+
+
side effects:

metrics. +self.metrics[‘RxByteCount’] should be incremented by size of payload. +self.metrics[‘RxGoodCount’] should be incremented by 1 if a good message is received. +self.metrics[‘RxBadCount’] should be incremented by 1 if an invalid message is received (&discarded.)

+
+
+
+ +
+
+getSetup() None[source]
+

Setup so we can get messages.

+
+
if message_strategy is stubborn, will loop here forever.

connect, declare queue, apply bindings.

+
+
+
+ +
+
+newMessages() list[source]
+

If there are new messages available from the broker, return them, otherwise return None.

+

On Success, this routine returns immediately (non-blocking) with either None, or a list of messages.

+

On failure, this routine blocks, and loops reconnecting to broker, until interaction with broker is successful.

+
+ +
+
+putNewMessage(message: Message, content_type: str = 'application/json', exchange: str | None = None) bool[source]
+

put a new message out, to the configured exchange by default.

+
+ +
+ +
+
+sarracenia.moth.amqp.logger = <Logger sarracenia.moth.amqp (WARNING)>
+

amqp_ss_maxlen

+

the maximum length of a “short string”, as per AMQP protocol, in bytes.

+
+ +
+
+

sarracenia.moth.pika

+
+
+class sarracenia.moth.pika.PIKA(broker)[source]
+

Bases: Moth

+

moth subclass based on the pika AMQP/rabbitmq client library.

+

stub: not implemented.

+
+
+__init__(broker)[source]
+

If is_subscriber=True, then this is a consuming instance. +expect calls to get* routines.

+

if is_subscriber=False, then expect/permit only calls to put*

+
+ +
+ +
+
+

sarracenia.moth.mqtt

+
+
+

sarracenia.moth.amq1

+
+
+class sarracenia.moth.amq1.AMQ1(broker, props, is_subscriber)[source]
+

Bases: Moth

+
+
+__init__(broker, props, is_subscriber)[source]
+

AMQP 1.0 library to be built with libqpid-proton (the only free amqp 1.0 library around.)

+

stub, not implemented

+
+ +
+ +
+
+

sarracenia.rabbitmq_admin

+

rabbitmq administration bindings, to allow sr to invoke broker management functions.

+
+
+sarracenia.rabbitmq_admin.add_user(url, role, user, passwd, simulate)[source]
+

add the given user with the given credentials.

+
+ +
+
+sarracenia.rabbitmq_admin.broker_get_exchanges(url, ssl_key_file=None, ssl_cert_file=None)[source]
+

get the list of existing exchanges using a url query.

+
+ +
+
+sarracenia.rabbitmq_admin.del_user(url, user, simulate)[source]
+

delete user from the given broker.

+
+ +
+
+sarracenia.rabbitmq_admin.exec_rabbitmqadmin(url, options, simulate=False)[source]
+

invoke rabbitmqadmin using a sub-process, with the given options.

+
+ +
+
+sarracenia.rabbitmq_admin.get_exchanges(url)[source]
+

get the list of existing exchanges.

+
+ +
+
+sarracenia.rabbitmq_admin.get_queues(url)[source]
+

get the list of existing queues.

+
+ +
+
+sarracenia.rabbitmq_admin.get_users(url)[source]
+

get the list of existing users.

+
+ +
+
+sarracenia.rabbitmq_admin.run_rabbitmqadmin(url, options, simulate=False)[source]
+

spawn a subprocess to run rabbitmqadmin with the given options. +capture result.

+
+ +
+
+sarracenia.rabbitmq_admin.user_access(url, user)[source]
+

Given an administrative URL, return a list of exchanges and queues the user can access.

+

lox = list of exchanges, just a list of names. +loq = array of queues, where the value of each is the number of messages ready.

+

return value:

+
{ 'exchanges': { 'configure': lox, 'write': lox, 'read': lox },
+  'queues' : { 'configure': loq, 'write': loq, 'read': loq },
+  'bindings' : { <queue> : { 'exchange': <exchange> , 'key' : <routing_key> } }
+}
+
+
+
+ +
+
+

sarracenia.transfer

+
+
+exception sarracenia.transfer.TimeoutException[source]
+

Bases: Exception

+

timeout exception

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+class sarracenia.transfer.Transfer(proto, options)[source]
+

Bases: object

+

This is a sort of abstract base class for implementing transfer protocols. +Implemented subclasses include support for: local files, https, sftp, and ftp.

+

This class has routines that do i/o given descriptors opened by the sub-classes, +so that each one does not need to re-implement copying, for example.

+

Each subclass needs to implement the following routines:

+

if downloading:

+
get    ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 )
+getAccellerated( msg, remote_file, local_file, length )
+ls     ()
+cd     (dir)
+delete (path)
+
+
+

if sending:

+
put    ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 )
+putAccelerated ( msg, remote_file, local_file, length=0 )
+cd     (dir)
+mkdir  (dir)
+umask  ()
+chmod  (perm)
+rename (old,new)
+
+
+

Note that the ls() call returns are polymorphic. One of:

+
    +
  • a dictionary where the key is the name of the file in the directory, +and the value is an SFTPAttributes structure for if (from paramiko.) +(sftp.py as an example)

  • +
  • a dictionary where the key is the name of the file, and the value is a string +that looks like the output of a linux ls command. +(ftp.py as an example.)

  • +
  • a sequence of bytes… will be parsed as an html page. +(https.py as an example)

  • +
+

The first format is the vastly preferred one. The others are fallbacks when the first +is not available. +The flowcb/poll/__init__.py lsdir() routing will turn ls tries to transform any of +these return values into the first form (a dictionary of SFTPAttributes) +Each SFTPAttributes structure needs st_mode set, and folders need stat.S_IFDIR set.

+

if the lsdir() routine gets a sequence of bytes, the on_html_page() and on_html_parser_init(, +or perhaps handle_starttag(..) and handle_data() routines) will be used to turn them into +the first form.

+

web services with different such formats can be accommodated by subclassing and overriding +the handle_* entry points.

+

uses options (on Sarracenia.config data structure passed to constructor/factory.) +* credentials - used to authentication information. +* sendTo - server to connect to. +* batch - how many files to transfer before a connection is torn down and re-established. +* permDefault - what permissions to set on files transferred. +* permDirDefault - what permission to set on directories created. +* timeout - how long to wait for operations to complete. +* byteRateMax - maximum transfer rate (throttle to avoid exceeding) +* bufsize - size of buffers for file transfers.

+
+
+__init__(proto, options)[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+on_data(chunk) bytes[source]
+

transform data as it is being read. +Given a buffer, return the transformed buffer. +Checksum calculation is based on pre transformation… likely need +a post transformation value as well.

+
+ +
+ +
+
+sarracenia.transfer.alarm_set(time)[source]
+

FIXME: replace with set itimer for > 1 second resolution… currently rouding to nearest second.

+
+ +
+
+

sarracenia.transfer.file

+
+
+class sarracenia.transfer.file.File(proto, options)[source]
+

Bases: Transfer

+

Transfer sub-class for local file i/o.

+
+
+__init__(proto, options)[source]
+
+ +
+
+cd(path)[source]
+

proto classes are used for remote sessions, so this +cd is for REMOTE directory… when file remote as a protocol it is for the source. +should not change the “local” working directory when downloading.

+
+ +
+ +
+
+

sarracenia.transfer.https

+
+
+class sarracenia.transfer.https.Https(proto, options)[source]
+

Bases: Transfer

+

HyperText Transfer Protocol (HTTP) ( https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol ) +sarracenia transfer protocol subclass supports/uses additional custom options:

+
    +
  • accelWgetCommand (default: ‘/usr/bin/wget %s -o - -O %d’ )

  • +
+
+
built with:

urllib.request ( https://docs.python.org/3/library/urllib.request.html )

+
+
+
+
+__init__(proto, options)[source]
+
+ +
+ +
+
+

sarracenia.transfer.ftp

+
+
+class sarracenia.transfer.ftp.Ftp(proto, options)[source]
+

Bases: Transfer

+

File Transfer Protocol (FTP) ( https://datatracker.ietf.org/doc/html/rfc959 ) +sarracenia transfer protocol subclass supports/uses additional custom options:

+
    +
  • accelFtpputCommand (default: ‘/usr/bin/ncftpput %s %d’ )

  • +
  • accelFtpgetCommand (default: ‘/usr/bin/ncftpget %s %d’ )

  • +
+

built using: ftplib ( https://docs.python.org/3/library/ftplib.html )

+
+
+__init__(proto, options)[source]
+
+ +
+ +
+
+

sarracenia.transfer.sftp

+
+
+class sarracenia.transfer.sftp.Sftp(proto, options)[source]
+

Bases: Transfer

+

SecSH File Transfer Protocol (SFTP) ( https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt ) +Sarracenia transfer protocol subclass supports/uses additional custom options:

+
    +
  • accelScpCommand (default: ‘/usr/bin/scp %s %d’ )

  • +
+

The module uses the paramiko library for python SecSH support ( https://www.paramiko.org/ )

+
+
+__init__(proto, options)[source]
+
+ +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/flowcb.html b/Reference/flowcb.html new file mode 100644 index 000000000..b155610c2 --- /dev/null +++ b/Reference/flowcb.html @@ -0,0 +1,2436 @@ + + + + + + + FlowCallback Reference — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

FlowCallback Reference

+

Documentation of the Flow callback plugins included +with sr3.

+ +
+

sarracenia.flowcb

+
+
+class sarracenia.flowcb.FlowCB(options, class_logger=None)[source]
+

Bases: object

+

Flow Callback is the main class for implementing plugin customization to flows.

+

sample activation in a configuration file:

+

flowCallback sarracenia.flowcb.name.Name

+

will instantiate an object of that type whose appropriately name methods +will be called at the right time.

+

__init__ accepts options as an argument.

+

options is a sarracenia.config.Config object, used to override default behaviour

+

a setting is declared in a configuration file like so:

+
set sarracenia.flowcb.filter.log.Log.level debug
+
+
+

(the prefix for the setting matches the type hierarchy in flowCallback) +the plugin should get the setting:

+
options.level = 'debug'
+
+
+

worklist given to on_plugins…

+
    +
  • worklist.incoming –> new messages to continue processing

  • +
  • worklist.ok –> successfully processed

  • +
  • worklist.rejected –> messages to not be further processed.

  • +
  • worklist.failed –> messages for which processing failed. Failed messages will be retried.

  • +
  • worklist.directories_ok –> list of directories created during processing.

  • +
+

Initially, all messages are placed in incoming. +if a plugin entry_point decides:

+
    +
  • a message is not relevant, it is moved to the rejected worklist.

  • +
  • all processing has been done, it moves it to the ok worklist

  • +
  • an operation failed and it should be retried later, append it to the failed +worklist

  • +
+

Do not remove any message from all lists, only move messages between them. +it is necessary to put rejected messages in the appropriate worklist +so they can be acknowledged as received. Messages can only removed after ack.

+

def __init__(self,options) -> None:

+
Task: initialization of the flowCallback at instantiation time.
+
+usually contains:
+
+self.o = options
+
+
+

def ack(self,messagelist) -> None:

+
Task: acknowledge messages from a gather source.
+
+
+

def gather(self, messageCountMax) -> (gather_more, messages)

+
Task: gather messages from a source... return a tuple:
+
+      * gather_more ... bool whether to continue gathering
+      * messages ... list of messages
+
+      or just return a list of messages.
+
+      In a poll, gather is always called, regardless of vip posession.
+
+      In all other components, gather is only called when in posession
+      of the vip.
+
+return (True, list)
+ OR
+return list
+
+
+

def after_accept(self,worklist) -> None:

+
Task: just after messages go through accept/reject masks,
+      operate on worklist.incoming to help decide which messages to process further.
+      and move messages to worklist.rejected to prevent further processing.
+      do not delete any messages, only move between worklists.
+
+
+

def after_work(self,worklist) -> None:

+
Task: operate on worklist.ok (files which have arrived.)
+
+All messages on the worklist.ok list have been acknowledged, so to suppress posting
+of them, or futher processing, the messages must be removed from worklist.ok.
+
+worklist.failed processing should occur in here as it will be zeroed out after this step.
+The flowcb/retry.py plugin, for example, processes failed messages.
+
+
+

def destfn(self,msg) -> str:

+
Task: look at the fields in the message, and perhaps settings and
+      return a new file name for the target of the send or download.
+
+kind of a last resort function, exists mostly for sundew compatibility.
+can be used for selective renaming using accept clauses.
+
+
+

def download(self,msg) -> bool:

+
Task: looking at msg['new_dir'], msg['new_file'], msg['new_inflight_file']
+      and the self.o options perform a download of a single file.
+      return True on a successful transfer, False otherwise.
+
+      if self.o.dry_run is set, simulate the output of a download without
+      performing it.
+
+This replaces built-in download functionality, providing an override.
+for individual file transfers. ideally you set checksums as you download.
+
+
+

def metricsReport(self) -> dict:

+
+

Return a dictionary of metrics. Example: number of messages remaining in retry queues.

+
+
+
def on_cleanup(self) -> None::

allow plugins to perform additional work after broker resources are eliminated. +local state files are still present when this runs.

+
+
def on_declare(self) -> None::

local state files are still already present when this runs. +allow plugins to perform additional work besides broker resource setup.

+
+
+

def on_housekeeping(self) -> None:

+
do periodic processing.
+
+
+

def on_start(self) -> None:

+
After the connection is established with the broker and things are instantiated, but
+before any message transfer occurs.
+
+
+

def on_stop(self) -> None:

+
what it says on the tin... clean up processing when stopping.
+
+
+

def poll(self) -> list:

+
Task: gather messages from a destination... return a list of messages.
+      works like a gather, but...
+
+      When specified, poll replaces the built-in poll of the poll component.
+      it runs only when the machine running the poll has the vip.
+      in components other than poll, poll is never called.
+return []
+
+
+

def post(self,worklist) -> None:

+
Task: operate on worklist.ok, and worklist.failed. modifies them appropriately.
+      message acknowledgement has already occurred before they are called.
+
+to indicate failure to process a message, append to worklist.failed.
+worklist.failed processing should occur in here as it will be zeroed out after this step.
+
+
+

def send(self,msg) -> bool:

+
Task: looking at msg['new_dir'], msg['new_file'], and the self.o options perform a transfer
+      of a single file.
+      return True on a successful transfer, False otherwise.
+
+      if self.o.dry_run is set, simulate the output of a send without
+      performing it.
+
+This replaces built-in send functionality for individual files.
+
+
+
+
def please_stop(self):

Pre-warn a flowcb that a stop has been requested, allowing processing to wrap up +before the full stop happens.

+
+
+
+
+__init__(options, class_logger=None)[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+please_stop()[source]
+

flow callbacks should not time.sleep for long periods, but only nap and check +between naps if a stop has been requested.

+
+ +
+ +
+
+sarracenia.flowcb.load_library(factory_path, options)[source]
+

Loading the entry points for a python module. It searches +the normal python module path using the importlib module.

+

the factory_path is a combined file specification with a dot separator +with a special last entry being the name of the class within the file.

+

factory_path a.b.c.C

+

means import the module named a.b.c and instantiate an object of type +C. In that class-C object, look for the known callback entry points.

+

or C might be guessed by the last class in the path not following +python convention by not starting with a capital letter, in which case, +it will just guess.

+

re +note that the ~/.config/sr3/plugins will also be in the python library +path, so modules placed there will be found, in addition to those in the +package itself in the sarracenia/flowcb directory

+
+
callback foo -> foo.Foo

sarracenia.flowcb.foo.Foo

+
+
callback foo.bar -> foo.bar.Bar

sarracenia.flowcb.foo.bar.Bar +foo.bar +sarracenia.flowcb.foo.bar

+
+
+
+ +
+
+

sarracenia.flowcb.accept

+

sarracenia.flowcb.accept modules are ones where the main focus is on the after_accept entry point. +This entry point is called after reject & accept rules have been called, and typically after +duplicate suppression has also been applied.

+

They can be used to further refine which files should be downloaded, by moving messages from the +worklist.incoming to worklist.rejected.

+
+
+

sarracenia.flowcb.accept.delete

+
+
Plugin delete.py:

the default after_accept handler for log. +prints a simple notice. # FIXME is this doc accurate? seems it is also modifying a message.

+
+
Usage:

flowcb sarracenia.flowcb.accept.delete.Delete

+
+
+
+
+class sarracenia.flowcb.accept.delete.Delete(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.download

+

Place holder customized flow callback classes whose focus is the download() entry point.

+

download(self,msg):

+
+
+
Task: looking at msg[‘new_dir’], msg[‘new_path’], msg[‘new_inflight_path’]

and the self.o options perform a download of a single file. +return True on a successful transfer, False otherwise.

+
+
+

if self.o.dry_run is set, simulate the output of a download without +performing it.

+

downlaod is called by the sarra and subscribe components, and can be used +to override the built-in methods for downloading a file.

+

It does the download based on the fields in the message provided:

+

retreival path: msg[‘baseUrl’] + ‘/’ + msg[‘relPath’]

+

taking care of the inflight setting, the inflight/temporary file: +is defined for use by the download routing: msg[‘new_inflight_path’]

+

Final local location to store is defined by:

+
+

msg[‘new_path’] == msg[‘new_dir’] + ‘/’ + msg[‘new_file’]

+
+

This replaces built-in download functionality, providing an override. +for individual file transfers. ideally you set checksums as you download.

+

looking at self.o.identity_method to establish download checksum algorithm. +might have to allow for cod… say it is checksum_method:

+
checksum = sarracenia.identity.Identity.factory(self.o.checksum_method)
+while downloading:
+    checksum.update(chunk)
+
+
+

where chunk is the bytes read. It saves a file read to calculate the checksum +during the download.

+

if the checksum does not match what is in the received message, then +it is imperative, to avoid looping, to apply the actual checksum of the +data to the message:

+
+

msg[‘identity’] = { ‘method’: checksum_method, ‘value’: checksum.get_sumstr() }

+
+

return Boolean success indicator. if False, download will be attempted again and/or +appended to retry queue.

+
+
+
+

sarracenia.flowcb.accept.downloadbaseurl

+
+
Plugin downloadbaseurl.py:

Downloads files sourced from the baseUrl of the poster, and saves them in the +directory specified in the config. Created to use with the poll_nexrad.py +plugin to download files uploaded in the NEXRAD US Weather Radar public dataset. +Compatible with Python 3.5+.

+
+
+

Example

+
+
A sample do_download option for subscribe.

Downloads the file located at message[‘baseUrl’] and saves it

+
+
Usage:

flowcb sarracenia.flowcb.accept.downloadbaseurl.DownloadBaseUrl

+
+
+
+
+class sarracenia.flowcb.accept.downloadbaseurl.DownloadBaseUrl(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.hourtree

+
+
Plugin hourtree.py:

When receiving a file, insert an hourly directory into the local delivery path hierarchy.

+
+
+

Example

+

input A/B/c.gif –> output A/B/<hour>/c.gif

+
+
Usage:

flowcb sarracenia.flowcb.accept.hourtree.HourTree

+
+
+
+
+class sarracenia.flowcb.accept.hourtree.HourTree(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.httptohttps

+
+
Plugin httptohttps.py:

This plugin simply turns messages with baseUrl http://… into https://

+

The process would need to be restarted. From now on, all http messages that would be +consumed, would be turned into an https message. The remaining of the process will +treat the message as if posted that way. That plugin is an easy way to turn transaction +between dd.weather.gc.ca and the user into secured https transfers.

+
+
Usage:

flowcb sarracenia.flowcb.accept.httptohttps.HttpToHttps

+
+
+
+
+class sarracenia.flowcb.accept.httptohttps.HttpToHttps(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.longflow

+
+
Plugin longflow.py:

This plugin is strictly for self-test purposes. +Creates a header ‘toolong’ that is longer than 255 characters, and so gets truncated. +Each header in a message that is too long should generate a warning message in the sarra log. +flow_test checks for the ‘truncated’ error message. +Put some utf characters in there to make it interesting… (truncation complex.)

+
+
Usage:

flowcb sarracenia.flowcb.accept.longflow.LongFlow

+
+
+
+
+class sarracenia.flowcb.accept.longflow.LongFlow(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.posthourtree

+
+
Plugin posthourtree.py:

When posting a file, insert an hourly directory into the delivery path hierarchy.

+
+
+

Example

+

input A/B/c.gif –> output A/B/<hour>/c.gif

+
+
Usage:

callback accept.posthourtree

+
+
+
+
+class sarracenia.flowcb.accept.posthourtree.Posthourtree(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.postoverride

+
+
Plugin postoverride.py:

Override message header for products that are posted. This can be useful or necessary +when re-distributing beyond the original intended destinations.

+
+
+

Example

+

for example company A delivers to their own DMZ server. ACME is a client of them, +and so subscribes to the ADMZ server, but the to_cluster=ADMZ, when ACME downloads, they +need to override the destination to specify the distribution within ACME. +* postOverride to_clusters ACME +* postOverrideDel from_cluster

+
+
Usage:

flowcb sarracenia.flowcb.accept.postoverride.PostOverride +postOverride x y +postOverrideDel z

+
+
+
+
+class sarracenia.flowcb.accept.postoverride.PostOverride(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.printlag

+
+
Plugin printlag.py:

This is an after_accept plugin. +print a message indicating how old messages received are. +this should be used as an on_part script. For each part received it will print a line +in the local log that looks like this:

+

2015-12-23 22:54:30,328 [INFO] posted: 20151224035429.115 (lag: 1.21364 seconds ago) to deliver: /home/peter/test/dd/bulletins/alphanumeric/20151224/SA/EGGY/03/SAUK32_EGGY_240350__EGAA_64042,

+

the number printed after “lag:” the time between the moment the message was originally posted on the server, +and the time the script was called, which is very near the end of writing the file to local disk.

+

This can be used to gauge whether the number of instances or internet link are sufficient +to transfer the data selected. if the lag keeps increasing, then something must be done.

+
+
Usage:

flowcb sarracenia.flowcb.accept.printlag.PrintLag

+
+
+
+
+class sarracenia.flowcb.accept.printlag.PrintLag(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.rename4jicc

+
+
+class sarracenia.flowcb.accept.rename4jicc.Rename4Jicc(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.renamedmf

+
+
Plugin renamedmf.py:

This is an example of the usage of after_accept script +to rename the product when it is downloaded +It adds a ‘:datetime’ to the name of the product +unlikely to be useful to others, except as example.

+
+
+

Example

+

takes px name : CACN00_CWAO_081900__WZS_14623:pxatx:CWAO:CA:5:Direct:20081008190602 +add datetimestamp : CACN00_CWAO_081900__WZS_14623:pxatx:CWAO:CA:5:Direct:20081008190602:20081008190612

+
+
Usage:

flowcb sarracenia.flowcb.accept.renamedmf.RenameDMF

+
+
+
+
+class sarracenia.flowcb.accept.renamedmf.RenameDMF(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.renamewhatfn

+
+
Plugin renamewhatfn.py:

This plugin is no longer needed. Sundew compoatibility was added to Sarracenia, +so now can get the same effect by using the filename option which works like it +does in Sundew:

+

filename WHATFN

+

what it was used for: +This renamer strips everything from the first colon in the file name to the end. +This does the same thing as a ‘WHATFN’ config on a sundew sender.

+
+
+

Example

+

takes px name : /apps/dms/dms-metadata-updater/data/international_surface/import/mdicp4d:pull-international-metadata:CMC:DICTIONARY:4:ASCII:20160223124648 +rename for : /apps/dms/dms-metadata-updater/data/international_surface/import/mdicp4d

+
+
Usage:

flowcb sarracenia.flowcb.accept.renamewhatfn.RenameWhatFn

+
+
+
+
+class sarracenia.flowcb.accept.renamewhatfn.RenameWhatFn(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.save

+
+
Plugin save.py:

Converts a consuming component into one that writes the queue into a file. +If there is some sort of problem with a component, then add callback save and restart.

+

The messages will accumulate in a save file in ~/.cache/<component>/<config>/ ??<instance>.save +When the situation is returned to normal (you want the component to process the data as normal): +* remove the callback save +* note the queue this configuration is using to read (should be in the log on startup.) +* run an sr_shovel with -restore_to_queue and the queue name.

+
+
Options:

save.py takes ‘msgSaveFile’ as an argument. +“_9999.sav” will be added to msgSaveFile, where the 9999 represents the instance number. +As instances run in parallel, rather than sychronizing access, just writes to one file per instance.

+
+
Usage:

callback accept.save +msgSaveFile x

+
+
+
+
+class sarracenia.flowcb.accept.save.Save(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.speedo

+
+
Plugin speedo.py:

Gives a speedometer reading on the messages going through an exchange. +as this is an after_accept +Accumulate the number of messages and the bytes they represent over a period of time.

+
+
Options:

msgSpeedoInterval -> how often the speedometer is updated. (default: 5) +msg_speedo_maxlag -> if the message flow indicates that messages are ‘late’, emit warnings. (default 60)

+
+
Usage:

callback accept.speedo +msgSpeedoInterval x +msg_speedo_maxlag y

+
+
+
+
+class sarracenia.flowcb.accept.speedo.Speedo(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+

set defaults for options. can be overridden in config file.

+
+ +
+ +
+
+

sarracenia.flowcb.accept.sundewpxroute

+
+
Plugin sundewpxroute.py:

Implement message filtering based on a routing table from MetPX-Sundew. +Make it easier to feed clients exactly the same products with sarracenia, +that they are used to with sundew.

+
+
Usage:

the pxrouting option must be set in the configuration before the plugin +is configured, like so: +* pxRouting /local/home/peter/src/pdspx/routing/etc/pxRouting.conf +* pxClient navcan-amis +flowcb sarracenia.flowcb.accept.sundewpxroute.SundewPxRoute

+
+
+
+
+class sarracenia.flowcb.accept.sundewpxroute.SundewPxRoute(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+

For World Meteorological Organization message oriented routing. +Read the configured metpx-sundew routing table, and figure out which +Abbreviated Header Lines (AHL’s) are configured to be sent to ‘target’ +being careful to account for membership in clientAliases.

+

init sets ‘ahls_to_route’ according to the contents of pxrouting

+
+ +
+ +
+
+

sarracenia.flowcb.accept.testretry

+
+
Plugin testretry.py:

This changes the message randomly so that it will cause +a download or send an error. +When a message is being retried, it is randomly fixed

+
+
Usage:

flowcb sarracenia.flowcb.accept.testretry.TestRetry

+
+
+
+
+class sarracenia.flowcb.accept.testretry.TestRetry(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.toclusters

+
+
Plugin toclusters.py:

Implements inter-pump routing by filtering destinations. +This is placed on a sarra process copying data between pumps. +Whenever it gets a message, it looks at the destination and processing +only continues if it is beleived that that message is a valid destination for the local pump.

+
+
Options:
+
The local pump will select messages destined for the DD or DDI clusters, and reject those for DDSR, which isn’t in the list.
    +
  • msgToClusters DDI

  • +
  • msgToClusters DD

  • +
+
+
+
+
Usage:

flowcb sarracenia.flowcb.accept.toclusters.ToClusters +msgToClusters x +msgToClusters y +…

+
+
+
+
+class sarracenia.flowcb.accept.toclusters.ToClusters(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.tohttp

+
+
Plugin tohttp.py:

ToHttp is the converse of ToFile. +After processing on a filter, a file URL needs to be turned back into a web url. +Uses savedUrl created by ToFile, to convert file url back to original.

+
+
Usage:

flowcb sarracenia.flowcb.accept.tohttp.ToHttp

+
+
+
+
+class sarracenia.flowcb.accept.tohttp.ToHttp(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.tolocal

+
+
Plugin tolocal.py:

This is a helper script to work with filters. +What a data pump advertises, it will usually use Web URL, but if one is +on a server where the files are available, it is more efficient to access +them as local files, so filters operate on file urls.

+
+
+

Example

+

baseDir /var/www/html +url is http://localhost/<date>/<src>/input/file.txt

+

flowcb sarracenia.flowcb.accept.tolocal.ToLocal # converts web URL to file URL

+
+

http://localhost/ –> file://var/www/html/ +url is now file://var/www/html/<date>/<src>/input/file.txt +m.savedurl = http://localhost/

+
+

flowcb sarracenia.flowcb.accept.<some converter that works on local files.>

+
+

A new file is created in another directory. +url is now file://var/www/<date>/<src>/output/file.txt

+
+

flowcb sarracenia.flowcb.accept.tohttp.ToHttp # turns the file URL back into a web one.

+
+

file://var/www/html/ –> http:///localhost/ +url is now: http://localhost/<date>/<src>/output/file.txt

+
+

The regular expression used to find the web url matches either http or https +and just captures upto the first ‘/’.

+

if you need to capture a different kind of url, such as …

+

https://hostname/~user/ ….

+

The easiest way is to set toLocalUrl as follows:

+

baseDir /home/user/www +toLocalUrl (https://hostname/~user/)

+

the parentheses around the URL set the value of to be put in m.savedurl that +will be restored when the companion plugin msg_2http is called.

+
+
Usage:

flowcb sarracenia.flowcb.accept.tolocal.ToLocal

+
+
+
+
+class sarracenia.flowcb.accept.tolocal.ToLocal(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.tolocalfile

+
+
Plugin tolocalfile.py:

This is a helper script to work with converters (filters) and senders.

+

What a data pump advertises, it will usually use Web URL, but if one is +on a server where the files are available, it is more efficient to access +them as local files, and so this plugin turn the message’s notice Web URL +into a File URL (file:/d1/d2/…/fn)

+
+
Normal Usage:

A Web URL in an amqp message is hold in the following values: +message[‘baseUrl’] (ex.: http://localhost) and +message[‘relPath’] (ex.: /<data>/<src>/d3/…/fn)

+

We will save these values before their modification : +message[‘saved_baseUrl’] = message[‘baseUrl’] +message[‘saved_relPath’] = message[‘relPath’]

+

We will then turn them into an absolute File Url: (Note if a baseDir was set it prefix the relPath) +message[‘baseUrl’] = ‘file:’ +message[‘relPath’] = [baseDir] + message[‘relPath’]

+
+
+

Example

+

baseDir /var/www/html

+

message pubtime=20171003131233.494 baseUrl=http://localhost relPath=/20171003/CMOE/productx.gif

+

flowcb sarracenia.flowcb.accept.tolocalfile.ToLocalFile

+

will receive this:: +* message[‘baseUrl’] is ‘http://localhost’ +* message[‘relPath’] is ‘/20171003/CMOE/GIF/productx.gif’

+
    +
  • will copy/save these values

  • +
  • message[‘saved_baseUrl’] = message[‘baseUrl’]

  • +
  • message[‘saved_relPath’] = message[‘relPath’]

  • +
  • turn the original values into a File URL

  • +
  • message[‘baseUrl’] = ‘file:’

  • +
  • if parent[‘baseDir’] : +* message[‘relPath’] = parent[‘baseDir’] + ‘/’ + message[‘relPath’] +* message[‘relPath’] = message[‘relPath’].replace(‘//’,’/’)

  • +
+

A sequence of after_accept plugins can perform various changes to the messages and/or +to the product… so here lets pretend we have an after_accept plugin that converts +gif to png and prepares the proper message for it

+

flowcb sarracenia.flowcb.accept.giftopng.GifToPng +After the tolocalfile this script could perform something like:

+
# build the absolute path of the png product
+new_path = message['relPath'].replace('GIF','PNG')
+new_path[-4:] = '.png'
+
+# proceed to the conversion gif2png
+ok = self.gif2png(gifpath=message.relPath,pngpath=new_path)
+
+
+

change the message to announce the new png product:

+
if ok :
+    message['baseUrl'] = message['saved_baseUrl']
+message['relPath'] = new_path
+if self.o.baseDir :
+    message['relPath'] = new_path.replace(self.o.baseDir,'',1)
+else :
+    logger.error(...
+# we are ok... proceed with this png file
+
+
+
+
Usage:

flowcb sarracenia.flowcb.accept.tolocalfile.ToLocalFile

+
+
+
+
+class sarracenia.flowcb.accept.tolocalfile.ToLocalFile(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.accept.wmotypesuffix

+
+
Plugin wmotypesuffix.py:

Given the WMO-386 TT designator of a WMO file, file type suffix to the file name. +Web browsers and modern operating systems may do the right thing if files have a recognizable suffix.

+

http://www.wmo.int/pages/prog/www/ois/Operational_Information/Publications/WMO_386/AHLsymbols/TableA.html

+

Status: proof of concept demonstrator… missing many TT’s. please add! +Tested with UNIDATA feeds, discrepancies: +TableA says L is Aviation XML, but UNIDATA Feed, it is all GRIB. +XW - should be CAP, but is GRIB. +IX used by Americans for HDF, unsure if that is kosher/halal/blessed, but it is in the UNIDATA feed.

+

IU/IS/IB are BUFR +other type designators welcome… for example, GRIB isn’t identified yet. +default to .txt.

+
+
Usage:

flowcb sarracenia.flowcb.accept.wmotypesuffix.WmoTypeSuffix

+
+
+
+
+class sarracenia.flowcb.accept.wmotypesuffix.WmoTypeSuffix(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.clamav

+

A sample on_part plugin to perform virus scanning, using the ClamAV engine.

+

requires a clamd binding package to be installed. On debian derived systems:

+
sudo apt-get install python3-pyclamd
+
+
+

on others:

+
pip3 install pyclamd
+
+
+

author: Peter Silva

+
+
+class sarracenia.flowcb.clamav.Clamav(options)[source]
+

Bases: FlowCB

+

Invoke ClamAV anti-virus scanning on files as they pass through a data pump. +when it is invoked depends on the component it is used from.

+

from a sender, post, or poll, the scan should stop processing prior +to the transfer.

+

for other components, subsscribers that download, it needs to take place +after downloading.

+
+
+__init__(options) None[source]
+
+ +
+ +
+
+

sarracenia.flowcb.destfn.sample

+

a destfn plugin script is used by senders or subscribers to do complex file naming. +this is an API demonstrator that prefixes the name delivered with ‘renamed_’:

+
filename DESTFNSCRIPT=sarracenia.flowcb.destfn.sample.Sample
+
+
+

An alternative method of invocation is to apply it selectively:

+
accept k* DESTFNSCRIPT=sarracenia.flowcb.destfn.sample.Sample
+
+
+

As with other flowcb plugins, the import will be done using normal +python import mechanism equivalent to:

+
import sarracenia.flowcb.destfn.sample
+
+
+

and then in that class file, there is a Sample class, the sample class +contains the destfn method, or entry_point.

+

The destfn routine consults the fields in the given message, and based on them, +return a new file name for the file to have after transfer (download or send.)

+

the routines have access to the settings via options provided to init, +accessed, by convention, as self.o.

+

The routine can also modify fields create new ones in the message.

+

the destfn routine returns the new name of the file.

+
+
+class sarracenia.flowcb.destfn.sample.Sample(options, class_logger=None)[source]
+

Bases: FlowCB

+
+ +
+
+

sarracenia.flowcb.filter

+

a module that is focused on transforming data should be called a filter. +Filter plugins intended for after_accept(self, worklist) entry_point.

+

At that point: +Messages have been gathered, then passed through the accept and reject pattern matches. +One of the first callbacks is the nodupe, so that duplicate suppression may cause additional +rejections.

+
+
+

sarracenia.flowcb.filter.deleteflowfiles

+
+
+class sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles(options, class_logger=None)[source]
+

Bases: FlowCB

+

This is a custom callback class for the sr_insects flow tests. +delete files for messages in two directories.

+
+ +
+
+

sarracenia.flowcb.filter.fdelay

+

This plugin delays processing of messages by message_delay seconds

+

sarracenia.flowcb.msg.fdelay 30 +import sarracenia.flowcb.filter.fdelay.Fdelay

+

or more simply:

+

fdelay 30 +callback filter.fdelay

+

every message will be at least 30 seconds old before it is forwarded by this plugin. +in the meantime, the message is placed on the retry queue by marking it as failed.

+
+
+class sarracenia.flowcb.filter.fdelay.Fdelay(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.filter.pclean_f90

+

msg_pclean_f90 module: file propagation test for Sarracenia components (in flow test) +https://github.com/MetPX/sr_insects/

+
+
+class sarracenia.flowcb.filter.pclean_f90.PClean_F90(options)[source]
+

Bases: PClean

+
+

functionality within the flow_tests of the sr_insects project. +This plugin class receive a msg from xflow_public and check propagation of the underlying file

+
    +
  • it checks if the propagation was ok

  • +
  • it randomly set a new test file with a different type in the watch dir (f31 amqp)

  • +
  • it posts the product to be treated by f92

  • +
  • when the msg for the extension file comes back, recheck the propagation

  • +
+
+

When a product is not fully propagated, the error is reported and the test is considered as a +failure. It also checks if the file differs from original

+
+ +
+
+

sarracenia.flowcb.filter.pclean_f92

+

msg_pclean_f90 module: second file propagation test for Sarracenia components (in flow test)

+
+
+class sarracenia.flowcb.filter.pclean_f92.PClean_F92(options)[source]
+

Bases: PClean

+

This plugin that manage the removal of every file

+
    +
  • it fails if one removal failed

  • +
+
+ +
+
+

sarracenia.flowcb.filter.wmo2msc

+

wmo2msc.py is an on_message plugin script to convert WMO bulletins on local disk +to MSC internal format in an alternate tree. It is analogous to Sundew’s ‘bulletin-file’. +Meant to be called as an sr_shovel plugin.

+

It prints an output line:

+

wmo2msc: <input_file> -> <output_file> (<detected format>)

+

usage:

+

Use the directory setting to know the root of tree where files are placed. +FIXME: well, likely what you really what is something like:

+
<date>/<source>/dir1/dir2/dir3
+
+<date>/<source>/dir1/dir2/newdir4/...
+
+-- so Directory doesn't cut it.
+
+
+

In a sr_shovel configuration:

+
directory /....
+callback filter.wmo2msc
+
+
+

Parameters:

+
    +
  • filter_wmo2msc_replace_dir old,new

  • +
  • filter_wmo2msc_uniquify hash|time|anything else +- whether to add a string in addition to the AHL to make the filename unique. +- hash - means apply a hash, so that the additional string is content based. +- if time, add a suffix _YYYYMMDDHHMMSS_99999 which ensures file name uniqueness. +- otherwise, no suffix will be added. +- default: hash

  • +
  • filter_wmo2msc_convert on|off +if on, then traditional conversion to MSC-BULLETINS is done as per TANDEM/APPS & MetPX Sundew +this involves n as termination character, and other charater substitutions.

  • +
  • filter_wmo2msc_tree on|off +if tree is off, files are just placed in destination directory. +if tree is on, then the file is placed in a subdirectory tree, based on +the WMO 386 AHL:

    +
    TTAAii CCCC YYGGgg  ( example: SACN37 CWAO 300104 )
    +
    +TT = SA - surface observation.
    +AA = CN - Canada ( but the AA depends on TT value, in many cases not a national code. )
    +ii = 37 - a number.. there are various conventions, they are picked to avoid duplication.
    +
    +
    +

    The first line of the file is expected to contain an AHL. and when we build a tree +from it, we build it as follows:

    +
    TT/CCCC/GG/TTAAii_CCCC_YYGGgg_<uniquify>
    +
    +
    +

    assuming tree=on, uniquify=hash:

    +
    +

    SA/CWAO/01/SACN37_CWAO_300104_1c699da91817cc4a84ab19ee4abe4e22

    +
    +
  • +
+
+
NOTE: Look at the end of the file for SUPPLEMENTARY INFORMATION

including hints about debugging.

+
+
+
+
+class sarracenia.flowcb.filter.wmo2msc.Wmo2msc(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+
+doSpecificProcessing()[source]
+

Modify bulletins received from Washington via the WMO socket protocol. +started as a direct copy from sundew of routine with same name in bulletinManagerWmo.py

+
    +
  • encode/decode, and binary stuff came because of python3

  • +
+
+ +
+
+replaceChar(oldchar, newchar)[source]
+

replace all occurrences of oldchar by newchar in the the message byte stream. +started as a direct copy from sundew of routine with same name in bulletin.py +- the storage model is a bit different, we store the entire message as one bytearray. +- sundew stored it as a series of lines, so replaceChar implementation changed.

+
+ +
+ +
+
+

sarracenia.flowcb.gather

+
+
+sarracenia.flowcb.gather.logger = <Logger sarracenia.flowcb.gather (WARNING)>
+

This file was originally a place holder… thought this was not needed, then there was a stack trace. +and setup.py gets leaves the directory out if not present.

+

then started finding this convenient for common routines.

+
+ +
+
+

sarracenia.flowcb.gather.file

+
+
+class sarracenia.flowcb.gather.file.File(options)[source]
+

Bases: FlowCB

+

read the file system, create messages for the files you find.

+

this is taken from v2’s sr_post.py

+

FIXME FIXME FIXME +FIXME: the sr_post version would post files on the fly as it was traversing trees. +so it was incremental and had little overhead. This one is does the whole recursion +in one gather.

+

It will fail horribly for large trees. Need to re-formulate to replace recursion with interation. +perhaps a good time to use python iterators.

+

also should likely switch from listdir to scandir

+
+
+__init__(options)[source]
+
+ +
+
+gather(messageCountMax)[source]
+

from sr_post.py/run

+

FIXME: really bad performance with large trees: It scans an entire tree +before emitting any messages. Need to re-factor with iterator style so produce +result in batch sized chunks incrementally.

+
+ +
+
+post1file(path, lstat, is_directory=False) list[source]
+

create the notification message for a single file, based on the lstat metadata.

+

when lstat is present it is used to decide whether the file is an ordinary file, a link +or a directory, and the appropriate message is built and returned.

+

if the lstat metadata is None, then that signifies a “remove” message to be created. +In the remove case, without the lstat, one needs the is_directory flag to decide whether +it is an ordinary file remove, or a directory remove. is_directory is not used other +than for the remove case.

+

The return value is a list that usually contains a single message. It is a list to allow +for if options are combined such that a symbolic link and the realpath it posts to may +involve multiple messages for a single file. Similarly in the multi-block transfer case.

+
+ +
+
+process_event(event, src, dst)[source]
+

return a tuple: pop? + list of messages.

+
+ +
+
+walk(src)[source]
+

walk directory tree returning 1 message for each file in it.

+
+ +
+
+walk_priming(p)[source]
+

Find all the subdirectories of the given path, start watches on them. +deal with symbolically linked directories correctly

+
+ +
+ +
+
+class sarracenia.flowcb.gather.file.SimpleEventHandler(parent)[source]
+

Bases: PatternMatchingEventHandler

+
+
+__init__(parent)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.gather.message

+
+
+class sarracenia.flowcb.gather.message.Message(options)[source]
+

Bases: FlowCB

+

gather messages from a sarracenia.moth message queuing protocol source.

+
+
+__init__(options) None[source]
+
+ +
+
+gather(messageCountMax) list[source]
+
+
Returns:
+

a list of messages obtained from this source.

+
+
Return type:
+

True … you can gather from other sources. and

+
+
+
+ +
+ +
+
+

sarracenia.flowcb.housekeeping

+

plugins intended for on_message entry_point.

+

(when messages are received.)

+
+
+

sarracenia.flowcb.housekeeping.resources

+

Default on_housekeeping handler that: +- Logs Memory and CPU usage. +- Restarts components to deal with memory leaks.

+

If MemoryMax is not in the config, it is automatically calculated with the following procedure:

+
    +
  1. The plugin processes the first MemoryBaseLineFile items to reach a steady state. +- Subscribers process messages-in +- Posting programs process messages-posted

  2. +
  3. Set MemoryMax threshold to MemoryMultiplier * (memory usage at time steady state)

  4. +
+

If memory use ever exceeds the MemoryMax threshold, then the plugin triggers a restart, reducing memory consumption.

+

Parameters:

+
+
MemoryMaxsize (default: none)

Hard coded maximum for tolerable memory consumption. +Must be suffixed with k/m/g for Kilo/Mega/Giga byte values. +If not set then the following options will have an effect:

+
+
MemoryBaseLineFileint, optional (default: 100)

How many files to process before measuring to establish the baseline memory usage. +(how many files are expected to process before a steady state is reached)

+
+
MemoryMultiplierint, optional (default: 3)

How many times past the steady state memory footprint you want to allow the component to grow before restarting. +It could be normal for memory usage to grow, especially if plugins store data in memory.

+
+
+
+
returns:
+

Nothing, restarts components if memory usage is outside of configured thresholds.

+
+
+
+
+class sarracenia.flowcb.housekeeping.resources.Resources(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+
+restart()[source]
+

Do an in-place restart of the current process (keeps pid). +Gets a new memory stack/heap, keeps all file descriptors but replaces the buffers.

+
+ +
+
+threshold
+

Per-process maximum memory footprint that is considered too large, forcing a process restart.

+
+ +
+ +
+
+

sarracenia.flowcb.log

+
+
+class sarracenia.flowcb.log.Log(options)[source]
+

Bases: FlowCB

+

The logging flow callback class. +logs message at the indicated time. Controlled by:

+

logEvents - which entry points to emit log messages at.

+

logMessageDump - print literal messages when printing log messages.

+

every housekeeping interval, prints:

+
    +
  • how many messages were received, rejected, %accepted.

  • +
  • number of files transferred, their size, and rate in files/s and bytes/s

  • +
  • lag: some information about how old the messages are when processed

  • +
+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.mdelaylatest

+
+
+class sarracenia.flowcb.mdelaylatest.MDelayLatest(options)[source]
+

Bases: FlowCB

+

This plugin delays processing of messages by message_delay seconds +If multiple versions of a file are published within the interval, only the latest one +will be published.

+

mdelay 30 +flowcb sarracenia.flowcb.mdelaylatest.MDelayLatest

+

every message will be at least 30 seconds old before it is forwarded by this plugin. +In the meantime, the message is placed on the retry queue by marking it as failed.

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.nodupe

+
+
+class sarracenia.flowcb.nodupe.NoDupe(options, class_logger=None)[source]
+

Bases: FlowCB

+

duplicate suppression family of modules.

+

invoked with:

+

callback sarracenia.flowcb.nodupe.disk

+

or: +callback sarracenia.flowcb.nodupe.redis

+

with default being loaded depdending on the presence of a

+

nodupe_driver “redis”

+

setting (defaults to disk.)

+
+ +
+
+

sarracenia.flowcb.nodupe.data

+
+
+class sarracenia.flowcb.nodupe.data.Data(options, class_logger=None)[source]
+

Bases: FlowCB

+

duplicate suppression based on data alone. Overrides the path used for lookups +in the cache so that all files have the same name, and so if the checksum +is the same, regardless of file name, it is considered a duplicate.

+
+ +
+
+

sarracenia.flowcb.nodupe.name

+
+
+class sarracenia.flowcb.nodupe.name.Name(options, class_logger=None)[source]
+

Bases: FlowCB

+

Override the the comparison so that files with the same name, +regardless of what directory they are in, are considered the same. +This is useful when receiving data from two different sources (two different trees) +and winnowing between them.

+
+ +
+
+

sarracenia.flowcb.retry

+
+
+class sarracenia.flowcb.retry.Retry(options)[source]
+

Bases: FlowCB

+

overall goal:

+
    +
  • When file transfers fail, write the messages to a queue to be retried later. +There is also a second retry queue for failed posts.

  • +
+

how it works:

+
    +
  • the after_accept checks how many incoming messages we received. +If there is a full batch to process, don’t try to retry any.

  • +
  • if there is room, then fill in the batch with some retry requests.

  • +
  • when after_work is called, the worklist.failed list of messages +is the files where the transfer failed. write those messages to +a retry queue.

  • +
  • the DiskQueue or RedisQueue classes are used to store the retries, and it handles +expiry on each housekeeping event.

  • +
+
+
+__init__(options) None[source]
+
+ +
+
+after_accept(worklist) None[source]
+

If there are only a few new messages, get some from the download retry queue and put them into +worklist.incoming.

+

Do this in the after_accept() entry point if retry_refilter is False.

+
+ +
+
+after_post(worklist) None[source]
+

Messages in worklist.failed should be put in the post retry queue.

+
+ +
+
+after_work(worklist) None[source]
+

Messages in worklist.failed should be put in the download retry queue. If there are only a few new +messages, get some from the post retry queue and put them into worklist.ok.

+
+ +
+
+gather(qty) None[source]
+

If there are only a few new messages, get some from the download retry queue and put them into +worklist.incoming.

+

Do this in the gather() entry point if retry_refilter is True.

+
+ +
+
+metricsReport() dict[source]
+

Returns the number of messages in the download_retry and post_retry queues.

+
+
Returns:
+

containing metrics: {'msgs_in_download_retry': (int), 'msgs_in_post_retry': (int)}

+
+
Return type:
+

dict

+
+
+
+ +
+ +
+
+

sarracenia.flowcb.pclean

+

msg_pclean module: base module for propagation tests and cleanup for Sarracenia components (in flow test) +Used by https://github.com/MetPX/sr_insects test suite for CI/CD.

+
+
+class sarracenia.flowcb.pclean.PClean(options)[source]
+

Bases: FlowCB

+

Base plugin class that is used in shovel pclean_f9x:

+
+
    +
  • it checks if the propagation was ok.

  • +
  • it randomly set a test in the watch f40.conf for propagation

  • +
  • it posts the product again (more test in shovel clean_f91) which is propagated too

  • +
  • it remove the original product

  • +
+
+

It also uses a file delay to tolerate a maximum lag for the test

+

The posted message contains a tag in the header for the test performed which is the extension used for the test

+
+
+__init__(options)[source]
+
+ +
+
+build_path_dict(fxx_dirs, relpath, ext='')[source]
+

This build paths necessary to pclean tests

+

It is a subset of all flow test path based on fxx download directory provided.

+
+
Parameters:
+
    +
  • root – usually the sarra dev doc root directory

  • +
  • fxx_dirs – a list of the flow test directory needed

  • +
  • relpath – the relative path of the file (starting with the date) without the forward slash

  • +
  • ext – the extension from the extension test (optional)

  • +
+
+
Returns:
+

a dictionnary of all paths built

+
+
+
+ +
+
+get_extension(relpath)[source]
+

Check whether the extension is in the header

+
+
Parameters:
+

msg – the msg used for the test

+
+
Returns:
+

the value corresponding to the extension key in the msg header

+
+
+
+ +
+ +
+
+

sarracenia.flowcb.sample

+
+
+class sarracenia.flowcb.sample.Sample(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.scheduled

+
+
+class sarracenia.flowcb.scheduled.Scheduled(options, logger=<Logger sarracenia.flowcb.scheduled (WARNING)>)[source]
+

Bases: FlowCB

+

Scheduled flow callback plugin arranges to post url’s +at scheduled times.

+

usage:

+

In the configuration file, need:

+
callback scheduled
+
+
+

and the schedule can be a specified as:

+
    +
  • scheduled_interval 1m (once a minute) a duration

  • +
  • scheduled_hour 4,9 at 4Z and 9Z every day.

  • +
  • scheduled_minute 33,45 within scheduled hours which minutes.

  • +
+

Scheduled_interval takes precedence over the others, making it +easier to specify an interval for testing/debugging purposes.

+

use in code (for subclassing):

+

from sarracenia.scheduled import Scheduled

+
+
class hoho(Scheduled):

replace the gather() routine… +keep the top lines “until time to run” +replace whatever is below. +will only run when it should.

+
+
+
+
+__init__(options, logger=<Logger sarracenia.flowcb.scheduled (WARNING)>)[source]
+
+ +
+
+update_appointments(when)[source]
+

# make a flat list from values where comma separated on a single or multiple lines.

+

set self.appointments to a list of when something needs to be run during the current day.

+
+ +
+
+wait_seconds(sleepfor)[source]
+

sleep for the given number of seconds, like time.sleep() but broken into +shorter naps to be able to honour stop_requested, or when housekeeping is needed.

+
+ +
+ +
+
+

sarracenia.flowcb.script

+
+
+

sarracenia.flowcb.shiftdir2baseurl

+
+
+class sarracenia.flowcb.shiftdir2baseurl.ShiftDir2baseUrl(options)[source]
+

Bases: FlowCB

+

modify message to shift directories from relPath to baseUrl:

+
+
given the setting shiftDir2baseUrl == 2 and given message with

baseDir=https://a relPath=b/c/d/e subtopic=b/c/d –> baseDir=https://a/b/c relPath=d/e subtopic=d

+
+
+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.poll

+
+
+class sarracenia.flowcb.poll.Poll(options, class_logger=None)[source]
+

Bases: FlowCB

+

The Poll flow callback class implements the main logic for polling remote resources. +the poll routine returns a list of messages for new files to be filtered.

+

when instantiated with options, the options honoured include:

+
    +
  • pollUrl - the URL of the server to be polled.

  • +
  • post_baseURL - parameter for messages to be returned. Also used to look +up credentials to help subscribers with retrieval.

  • +
  • masks - These are the directories at the pollUrl to poll. +derived from the accept/reject clauses, but filtering should happen later. +entire directories are listed at this point.

  • +
  • timezone - interpret listings from an FTP server as being in the given timezone +(as per pytz

  • +
  • chmod - used to identify the minimum permissions to accept for a file to +be included in a polling result.

  • +
  • identity_method - parameter for how to build identity checksum for messages. +as these are usually remote files, the default is typically “cod” (calculate on download)

  • +
  • rename - parameter used to to put in messages built to specify the rename field contents.

  • +
  • options are passed to sarracenia.Transfer classes for their use as well.

  • +
+

Poll uses sarracenia.transfer (ftp, sftp, https, etc… )classes to +requests lists of files using those protocols using built-in logic.

+

Internally, Poll normalizes the listings received by placing them into paramiko.SFTPAttributes +metadata records (similar to stat records) and builds a Sarracenia.Message from them. +The poll routine does one pass of this, returning a list of Sarracenia.Messages.

+

To customize:

+
    +
  • one can add new sarracenia.transfer protocols, each implementing the ls entry point +to be compatible with this polling routine, ideally the entry point would return a +list of paramiko.SFTPAttributes for each file in a directory listing. +This can be used to implement polling of structured remote resources such as S3 or webdav.

  • +
  • one can deal with different formats of HTTP pages by overriding the handle_data entry point, +as done in nasa_mls_nrt.py plugin

  • +
  • for traditional file servers, the listing format should be decypherable with the built-in processing.

  • +
  • sftp file servers provide paramiko.SFTPAttributes naturally which are timezone agnostic.

  • +
  • for some FTP servers, one may need to specify the timezone option to override the UTC default.

  • +
  • If there are problems with date or line formats, one can sub-class poll, and override only the on_line +routine to deal with that.

  • +
+
+
+__init__(options, class_logger=None)[source]
+
+ +
+
+handle_data(data)[source]
+

routine called from html.parser to deal with a single line. +if the line is about a file, then create a new entry for it +with a metadata available from SFTPAttributes.

+

example lines:

+
+
+
from hpfx.collab.science.gc.ca:

20230113T00Z_MSC_REPS_HGT_ISBL-0850_RLatLon0.09x0.09_PT000H.grib2 2023-01-13 03:49 5.2M

+
+
from https://data.cosmic.ucar.edu/suominet/nrt/ncConus/y2023/

CsuPWVh_2023.011.22.00.0060_nc 11-Jan-2023 23:58 47K

+
+
+
+

this can be overridden by subclassing to deal with new web sites.

+

Other web servers put their file indices in a tabular format, where there is a number +of cells per row: +<tr><td></td><td href=filename>filename</td><td>yyyy-mm-dd hh:mm</td><td>size</td> +This handle_data supports both formats… +the tabular format is provided by a vanilla apache2 on a debian derived system.

+
+ +
+
+on_html_page(data) dict[source]
+

called once per directory or page of HTML, invokes html.parser, returns +a dictionary of file entries.

+
+ +
+
+on_line(line) SFTPAttributes[source]
+

default line processing, converts a file listing into an SFTPAttributes. +does nothing if input is already an SFTPAttributes item, returning it unchanged. +verifies that file is accessible (based on self.o.permDefault pattern to establish minimum permissions.)

+
+ +
+ +
+
+

sarracenia.flowcb.poll.airnow

+

Posts updated files of airnowtech. +Compatible with Python 3.5+.

+
+
usage:

in an sr3 poll configuration file:

+

pollUrl http://files.airnowtech.org/?prefix=airnow/today/

+

callback airnow

+
+
STATUS: unknown… need some authentication, or perhaps the method has changed.

does not seem to work out of the box.

+
+
+
+
+class sarracenia.flowcb.poll.airnow.Airnow(options, class_logger=None)[source]
+

Bases: FlowCB

+
+ +
+
+

sarracenia.flowcb.poll.mail

+

Posts any new emails from an email server, connected to using +the specified protocol, either pop3 or imap. The imaplib/poplib +implementations in Python use the most secure SSL settings by +default: PROTOCOL_TLS, OP_NO_SSLv2, and OP_NO_SSLv3. +Compatible with Python 2.7+.

+

A sample do_poll option for sr_poll. +connects to an email server with the provided +credentials and posts all new messages by their msg ID.

+
+
usage:

in an sr_poll configuration file:

+

pollUrl [imap|imaps|pop|pops]://[user[:password]@]host[:port]/

+

IMAP over SSL uses 993, POP3 over SSL uses 995 +IMAP unsecured uses 143, POP3 unsecured uses 110

+

Full credentials must be in credentials.conf. +If port is not specified it’ll default to the ones above based on protocol/ssl setting.

+
+
+

This posts what messages are available. A separate component is needed to +download the message, which would need:

+
+

callback download.mail_ingest

+
+

to process these posts.

+
+
+class sarracenia.flowcb.poll.mail.Mail(options)[source]
+

Bases: Poll

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.poll.nasa_mls_nrt

+
+
+class sarracenia.flowcb.poll.nasa_mls_nrt.Nasa_mls_nrt(options, class_logger=None)[source]
+

Bases: Poll

+
+
+handle_data(data)[source]
+

decode some HTML into an SFTPAttributes record for a file.

+
+ +
+ +
+
+

sarracenia.flowcb.poll.nexrad

+
+
+

sarracenia.flowcb.poll.noaa_hydrometric

+

Posts updated files of NOAA water level/temperature hydrometric data. Station site IDs provided +in the poll_noaa_stn_file. Compatible with Python 3.5+.

+

usage: +sample url: https://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=9450460&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv

+

in an sr_poll configuration file:

+
pollUrl http://tidesandcurrents.noaa.gov/api
+retrievePathPattern /datagetter?range=1&station={0:}&product={1:}&units=metric&time_zone=gmt&application=web_services&format=csv
+
+poll_noaa_stn_file [path/to/stn/file]
+callback noaa_hydrometric
+
+
+

sample station file:

+
7|70678|9751639|Charlotte Amalie|US|VI|-4.0
+7|70614|9440083|Vancouver|US|WA|-8.0
+
+
+

The poll: +If poll_noaa_stn_file isn’t set, it’ll grab an up-to-date version of all station site code data from the +NOAA website. The station list file is in the following format: +SourceID | SiteID | SiteCode | SiteName | CountryID | StateID | UTCOffset +Each station on its own line. +Posts the file on the exchange if the request returns a valid URL.

+

in v2, one needed a matching downloader plugin, but in sr3 we can leverage the retrievePath feature +so that normalk downloader works, so only the poll one needed.

+
+
+class sarracenia.flowcb.poll.noaa_hydrometric.Noaa_hydrometric(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.poll.usgs

+

Posts updated files of USGS hydrometric data. Station site IDs provided +in the poll_usgs_stn_file. Compatible with Python 3.5+.

+
+
Status: UNTESTED… don’t have a working basis (even on v2) to compare against.

was updated for v3. but don’t have credentials to confirm it now works.

+
+
usage:

in an sr3 poll configuration file: +pollUrl http://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site={0:}&period=PT3H&parameterCd=00060,00065,00011

+

batch [station_chunk] +poll_usgs_station station-declaration +callback gather/usgs

+

If multiple usgs stations need to be fetched in one call, station_chunk should specify how big the station +blocks should be. If not set it’ll individually download station data. +If poll_usgs_stn_file isn’t set, it’ll default to pulling the siteIDs from: +https://water.usgs.gov/osw/hcdn-2009/HCDN-2009_Station_Info.xlsx +directory. each station declaration is in the following format: +SourceID | SiteID | SiteCode | SiteName | CountryID | StateID | UTCOffset +Each station on its own line.

+

to reformat into a parseable configuration option, declare poll_usgs_station as a list option, +so each line looks like:

+
poll_usgs_station 7|70026|9014087|Dry Dock, MI|US|MI|-5.0
+
+
+

then this file can be an include directive, and include supports both local and remote url’s +so the db can be admined in some other http accessible git repository.

+

More info on the http rest parameters at: https://waterservices.usgs.gov/rest/IV-Service.html +For writing fault-resistant code that polls from usgs: https://waterservices.usgs.gov/docs/portable_code.html +Sign up for updates involving if/how the format changes: http://waterdata.usgs.gov/nwis/subscribe?form=email +Parameter codes to tailor the data you want: +https://help.waterdata.usgs.gov/code/parameter_cd_query?fmt=rdb&inline=true&group_cd=% +Currently the parametercd is set to: +00060 Physical Discharge, cubic feet per second +00065 Physical Gage height, feet +00011 Physical Temperature, water, degrees Fahrenheit

+
+
+
+
+class sarracenia.flowcb.poll.usgs.Usgs(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.post

+

Place holder… thought this was not needed, then there was a stack trace.

+
+
+

sarracenia.flowcb.send.email

+

sarracenia.flowcb.send.email.Email is an sr3 sender plugin. Once a file is +posted, the plugin matches the topic(what the filename begins with) to the +file name and sends the appropriate emails.

+
+
Usage:
    +
  1. Need the following variables in an sr_sender config defined: file_email_to, file_email_relay +Optionally, you can also provide a sender name/email as file_email_form:

    +
    +

    file_email_to AACN27 muhammad.taseer@canada.ca, test@test.com +file_email_relay email.relay.server.ca +file_email_from santa@canada.ca

    +
    +
  2. +
  3. In the config file, include the following line:

    +
    +

    callback send.email

    +
    +
  4. +
  5. sr_sender foreground emails.conf

  6. +
+
+
+

Original Author: Wahaj Taseer - June, 2019

+
+
+class sarracenia.flowcb.send.email.Email(options)[source]
+

Bases: FlowCB

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.post.message

+
+
+class sarracenia.flowcb.post.message.Message(options)[source]
+

Bases: FlowCB

+

post messages to sarracenia.moth message queuing protocol destination.

+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.send

+

plugins intended for on_message entry_point.

+

(when messages are received.)

+
+
+

sarracenia.flowcb.wistree

+
+
+

sarracenia.flowcb.work

+

plugins that use primarily the after_work entry point, normally +executed after the file transfer (either send or get) has completed.

+

usually such plugins will contain a loop:

+
+
for msg in worklist.ok

do_something.

+
+
+

to operate on all the files transferrred or processed successfully.

+
+
+

sarracenia.flowcb.work.age

+

print the age of files written (compare current time to mtime of message.) +usage:

+

flowcb work.age

+
+
+class sarracenia.flowcb.work.age.Age(options, class_logger=None)[source]
+

Bases: FlowCB

+
+ +
+
+

sarracenia.flowcb.work.rxpipe

+
+
+class sarracenia.flowcb.work.rxpipe.Rxpipe(options)[source]
+

Bases: FlowCB

+

After each file is transferred, write it’s name to a named_pipe.

+
+
parameter:

rxpipe_name – the path for the named pipe to write the file names to.

+
+
+
+
+__init__(options)[source]
+
+ +
+ +
+
+

sarracenia.flowcb.v2wrapper

+
+
+class sarracenia.flowcb.v2wrapper.V2Wrapper(o)[source]
+

Bases: FlowCB

+
+
+__init__(o)[source]
+

A wrapper class to run v02 plugins. +us run_entry(entry_point,module)

+

entry_point is a string like ‘on_message’, and module being the one to add.

+
+
weird v2 stuff:

when calling init, self is a config/subscriber… +when calling on_message, self is a message… +that is kind of blown away for each message… +parent is the config/subscriber in both cases. +so v2 state variables are always stored in parent.

+
+
+
+ +
+
+on_time(time)[source]
+

run plugins for a given entry point.

+
+ +
+
+run_entry(ep, m)[source]
+

run plugins for a given entry point.

+
+ +
+ +
+
+sarracenia.flowcb.v2wrapper.sumstrFromMessage(msg) str[source]
+

accepts a v3 message as argument msg. returns the corresponding sum string for a v2 ‘sum’ header.

+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/index.html b/Reference/index.html new file mode 100644 index 000000000..9228f1a9e --- /dev/null +++ b/Reference/index.html @@ -0,0 +1,275 @@ + + + + + + + Reference — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Reference

+
+

Contents:

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr3.1.html b/Reference/sr3.1.html new file mode 100644 index 000000000..b4344242c --- /dev/null +++ b/Reference/sr3.1.html @@ -0,0 +1,376 @@ + + + + + + + SR3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3

+
+

sr3 Sarracenia CLI

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr3 options action [ component/config … ]

+
+
+

DESCRIPTION

+

Sr3 is a tool to manage a fleet of daemons, whose output is mostly +in log files. Sr3 allows one to start, stop, and inquire the status of configured +Sarracenia flows. It is the primary command line entry point to +Sarracenia 3 ( https://metpx.github.io/sarracenia/ ) +When sr3 is started up, it reads the entire configuration tree, and the status of all flows +can be queried with, for example sr3 status. When component/config is given it is to +have sr3 operate on a subset of all the configurations present.

+ +

The command line has three major elements: +* options +* action +* component/config

+

A flow is a group of processes running using a common component/config.

+
+
+

OPTIONS

+

Most options are stored in configuration files referred to by the component/config indicated +by the listing file, but on occasion, one may wish to use command line to override +values in the configuration file. Options are defined in Sr3 Options (7) +Refer to that manual page for a full discussion. There is one exception:

+
-h (or --help)
+
+
+

The help option is only available on the command line. It is used to get a prompt +describing the subset of options available to override config file values.

+
+
+

ACTIONS

+

The type of action to take. One of:

+
+
    +
  • add: copy to the list of available configurations.

  • +
  • cleanup: deletes the component’s resources on the server.

  • +
  • convert: copy configurations from v2 to sr3 location, updating on the way.

  • +
  • declare: creates the component’s resources on the server.

  • +
  • disable: mark a configuration as ineligible to run.

  • +
  • edit: modify an existing configuration.

  • +
  • enable: mark a configuration as eligible to run.

  • +
  • features: what parts of sr3 are working?

  • +
  • foreground: run a single instance in the foreground logging to stderr

  • +
  • list: list all the configurations available.

  • +
  • list plugins: list all the plugins available.

  • +
  • list examples: list all the plugins available.

  • +
  • remove: remove a configuration.

  • +
  • restart: stop and then start the configuration.

  • +
  • run: run as a master process (like start, but don’t return.)

  • +
  • sanity: looks for instances which have crashed or gotten stuck and restarts them.

  • +
  • show view an interpreted version of a configuration file.

  • +
  • start: start the configuration running

  • +
  • status: check if the configuration is running.

  • +
  • stop: stop the configuration from running

  • +
+
+
+
+

COMPONENTS

+

The Flow Algorithm is what is +run by all sr3 processes. The flow algorithm’s behaviour is customized by options, +some of which control optional modules (flowcallbacks) Each component has a +different set of default option settings to cover a common use case.

+
    +
  • cpump - copy notification messages from one pump another second one (a C implementation of shovel.)

  • +
  • flow - flux générique, pas de valeurs par défaut, bonne base pour la construction de flux personalisés.

  • +
  • poll - poll a non-sarracenia web or file server to create notification messages for processing.

  • +
  • post|sr3_post|sr_cpost|watch - create notification messages for files for processing.

  • +
  • sarra - download file from a remote server to the local one, and re-post them for others.

  • +
  • sender - send files from a local server to a remote one.

  • +
  • shovel - copy notification messages, only, not files.

  • +
  • watch - create notification messages for each new file that arrives in a directory, or at a set path.

  • +
  • winnow - copy notification messages, suppressing duplicates.

  • +
+
+
+

CONFIGURATIONS

+

When a component/configuration pair is specified on the command line, +It is actually building the effective configuration from:

+
+
    +
  1. default.conf

  2. +
  3. admin.conf

  4. +
  5. <component>.conf (subscribe.conf, audit.conf, etc…)

  6. +
  7. <component>/<config>.conf

  8. +
+
+

Settings in an individual .conf file are read in after the default.conf +file, and so can override defaults. Options specified on +the command line override configuration files.

+

While one can manage configuration files using the add, remove, +list, edit, disable, and enable actions, one can also do all +of the same activities manually by manipulating files in the settings +directory. The configuration files for an sr3 configuration +called myflow would be here:

+
+
    +
  • linux: ~/.config/sarra/subscribe/myflow.conf (as per: XDG Open Directory Specication )

  • +
  • Windows: %AppDir%/science.gc.ca/sarra/myflow.conf , this might be: +C:UserspeterAppDataLocalscience.gc.casarramyflow.conf

  • +
  • MAC: FIXME.

  • +
+
+

The top of the tree has ~/.config/sarra/default.conf which contains settings that +are read as defaults for any component on start up. In the same +directory, ~/.config/sarra/credentials.conf contains credentials (passwords) to +be used by sarracenia ( CREDENTIALS for details. )

+

One can also set the XDG_CONFIG_HOME environment variable to override default placement, or +individual configuration files can be placed in any directory and invoked with the +complete path. When components are invoked, the provided file is interpreted as a +file path (with a .conf suffix assumed.) If it is not found as a file path, then the +component will look in the component’s config directory ( config_dir / component ) +for a matching .conf file.

+

If it is still not found, it will look for it in the site config dir +(linux: /usr/share/default/sarra/component).

+

Finally, if the user has set option remote_config to True and if he has +configured web sites where configurations can be found (option remote_config_url), +The program will try to download the named file from each site until it finds one. +If successful, the file is downloaded to config_dir/Downloads and interpreted +by the program from there. There is a similar process for all plugins that can +be interpreted and executed within sarracenia components. Components will first +look in the plugins directory in the users config tree, then in the site +directory, then in the sarracenia package itself, and finally it will look remotely.

+
+

Remote Configurations

+

One can specify URI’s as configuration files, rather than local files. Example:

+
+
    +
  • –config http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

On startup, sr3 checks if the local file cap.conf exists in the +local configuration directory. If it does, then the file will be read to find +a line like so:

+
+
    +
  • –remote_config_url http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

In which case, it will check the remote URL and compare the modification time +of the remote file against the local one. The remote file is not newer, or cannot +be reached, then the component will continue with the local file.

+

If either the remote file is newer, or there is no local file, it will be downloaded, +and the remote_config_url line will be prepended to it, so that it will continue +to self-update in future.

+
+
+

Logs

+

for the logs, look in ~/.cache/sr3/logs (on linux. Other platforms, will vary.) +To find them on any platform:

+
fractal% sr3 list
+User Configurations: (from: /home/peter/.config/sr3 )
+admin.conf                       credentials.conf                 default.conf
+logs are in: /home/peter/.cache/sr3/log
+
+
+

Last line indicates the directory.

+
+
+
+

EXAMPLES

+

Here is a short complete example configuration file:

+
broker amqps://dd.weather.gc.ca/
+
+subtopic model_gem_global.25km.grib2.#
+accept .*
+
+
+

This above file will connect to the dd.weather.gc.ca broker, connecting as +anonymous with password anonymous (defaults) to obtain notification messages about +files in the http://dd.weather.gc.ca/model_gem_global/25km/grib2 directory. +All files which arrive in that directory or below it will be downloaded +into the current directory (or just printed to standard output if -n option +was specified.)

+

A variety of example configuration files are available here:

+
+
+
+
+

SEE ALSO

+

User Commands:

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

sr3_cpump(1) - C implementation of the shovel component. (copy notification messages)

+

Formats:

+

sr3_credentials(7) - Convert logfile lines to .save Format for reload/resend.

+

sr3_options(7) - Convert logfile lines to .save Format for reload/resend.

+

sr3_post(7) - The format of notification messages.

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr3_cpump.1.html b/Reference/sr3_cpump.1.html new file mode 100644 index 000000000..fb59bc173 --- /dev/null +++ b/Reference/sr3_cpump.1.html @@ -0,0 +1,258 @@ + + + + + + + SR_CPUMP — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR_CPUMP

+
+

sr_shovel in C

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr_cpump foreground|start|stop|restart|reload|status configfile +sr_cpump cleanup|declare configfile

+
+
+

DESCRIPTION

+

sr_cpump is an alternate implementation of the shovel component of sr3(1) +with some limitations.

+
+
    +
  • doesn’t download data, only circulates posts.

  • +
  • runs as only a single instance (no multiple instances).

  • +
  • does not support any plugins.

  • +
  • does not support vip for high availability.

  • +
  • different regular expression library: POSIX vs. python.

  • +
  • does not support regex for the strip command (no non-greedy regex).

  • +
+
+

It can therefore act as a drop-in replacement for:

+
+

sr3 shovel - process shovel messages.

+

sr3 winnow - process winnow messages.

+
+

The C implementation may be easier to make available in specialized environments, +such as High Performance Computing, as it has far fewer dependencies than the python version. +It also uses far less memory for a given role. Normally the python version +is recommended, but there are some cases where use of the C implementation is sensible.

+

sr_cpump connects to a broker (often the same as the posting broker) +and subscribes to the notifications of interest. On reception of a post, +it looks up its sum in its cache. If it is found, the file has already come through, +so the notification is ignored. If not, then the file is new, and the sum is added +to the cache and the notification is posted.

+

sr_cpump can be used, like sr3 winnow(1), to trim messages +from sr3_post(1), sr3 poll(1) +or sr3 watch (1) etc… It is used when there are multiple +sources of the same data, so that clients only download the source data once, from +the first source that posted it.

+

The sr3_cpump command takes two arguments: an action start|stop|restart|reload|status… (self described) +followed by a configuration file.

+

The foreground action is used when debugging a configuration, when the user wants to +run the program and its configfile interactively… The foreground instance +is not concerned by other actions. The user would stop using the foreground instance +by simply <ctrl-c> on linux or use other means to send SIGINT or SIGTERM to the process.

+

The actions cleanup, declare, can be used to manage resources on +the rabbitmq server. The resources are either queues or exchanges. declare creates +the resources. creates and additionally does the bindings of queues.

+

The actions add, remove, edit, list, enable, disable act +on configurations.

+
+
+

CONFIGURATION

+

In general, the options for this component are described by +the sr3_options(7) page which should be read first. +It fully explains the option configuration language, and how to find +the option settings.

+

NOTE: The regular expression library used in the C implementation is the POSIX +one, and the grammar is slightly different from the python implementation. Some +adjustments may be needed.

+
+
+

ENVIRONMENT VARIABLES

+

If the SR_CONFIG_EXAMPLES variable is set, then the add directive can be used +to copy examples into the user’s directory for use and/or customization.

+

An entry in the ~/.config/sarra/default.conf (created via sr_subscribe edit default.conf ) +could be used to set the variable:

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

The value should be available from the output of a list command from the python +implementation.

+
+
+

SEE ALSO

+

User Commands:

+

sr3(1) - Sarracenia main command line interface.

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

Formats:

+

sr3_credentials(7) - Convert logfile lines to .save Format for reload/resend.

+

sr3_options(7) - Convert logfile lines to .save Format for reload/resend.

+

sr_post(7) - The format of notification message messages.

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr3_credentials.7.html b/Reference/sr3_credentials.7.html new file mode 100644 index 000000000..06fe8fd6f --- /dev/null +++ b/Reference/sr3_credentials.7.html @@ -0,0 +1,243 @@ + + + + + + + SR3 CREDENTIALS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3 CREDENTIALS

+
+

SR3 Credential File Format

+
+
manual section:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

CONFIGURATION

+

One normally does not specify passwords in configuration files. Rather they are placed +in the credentials file:

+
edit ~/.config/sr3/credentials.conf
+
+
+

For every url specified that requires a password, one places +a matching entry in credentials.conf. +The broker option sets all the credential information to connect to the RabbitMQ server

+
    +
  • broker amqp{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

  • +
+
(default: amqps://anonymous:anonymous@dd.weather.gc.ca/ )
+
+
+

For all sarracenia programs, the confidential parts of credentials are stored +only in ~/.config/sarra/credentials.conf. This includes the destination and the broker +passwords and settings needed by components. The format is one entry per line. Examples:

+
    +
  • amqp://user1:password1@host/

  • +
  • amqps://user2:password2@host:5671/dev

  • +
  • amqps://usern:passwd@host/ login_method=PLAIN

  • +
  • sftp://user5:password5@host

  • +
  • sftp://user6:password6@host:22 ssh_keyfile=/users/local/.ssh/id_dsa

  • +
  • ftp://user7:password7@host passive,binary

  • +
  • ftp://user8:password8@host:2121 active,ascii

  • +
  • ftps://user7:De%3Aize@host passive,binary,tls

  • +
  • ftps://user8:%2fdot8@host:2121 active,ascii,tls,prot_p

  • +
  • https://ladsweb.modaps.eosdis.nasa.gov/ bearer_token=89APCBF0-FEBE-11EA-A705-B0QR41911BF4

  • +
+

In other configuration files or on the command line, the url simply lacks the +password or key specification. The url given in the other files is looked +up in credentials.conf.

+
+

Credential Details

+

You may need to specify additional options for specific credential entries. These details can be added after the end of the URL, with multiple details separated by commas (see examples above).

+

Supported details:

+
    +
  • ssh_keyfile=<path> - (SFTP) Path to SSH keyfile

  • +
  • passive - (FTP) Use passive mode

  • +
  • active - (FTP) Use active mode

  • +
  • binary - (FTP) Use binary mode

  • +
  • ascii - (FTP) Use ASCII mode

  • +
  • ssl - (FTP) Use SSL/standard FTP

  • +
  • tls - (FTP) Use FTPS with TLS

  • +
  • prot_p - (FTPS) Use a secure data connection for TLS connections (otherwise, clear text is used)

  • +
  • bearer_token=<token> (or bt=<token>) - (HTTP) Bearer token for authentication

  • +
  • login_method=<PLAIN|AMQPLAIN|EXTERNAL|GSSAPI> - (AMQP) By default, the login method will be automatically determined. This can be overriden by explicity specifying a login method, which may be required if a broker supports multiple methods and an incorrect one is automatically selected.

  • +
+
+
Note::

SFTP credentials are optional, in that sarracenia will look in the .ssh directory +and use the normal SSH credentials found there.

+

These strings are URL encoded, so if an account has a password with a special +character, its URL encoded equivalent can be supplied. In the last example above, +%2f means that the actual password isi: /dot8 +The next to last password is: De:olonize. ( %3a being the url encoded value for a colon character. )

+
+
+
+
+
+

SEE ALSO

+

sr3(1) - Sarracenia main command line interface.

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

sr3_cpump(1) - C implementation of the shovel component. (copy messages)

+

Formats:

+

sr3_options(7) - the configuration options

+

sr3_post(7) - the format of notification messages.

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr3_options.7.html b/Reference/sr3_options.7.html new file mode 100644 index 000000000..5febe2682 --- /dev/null +++ b/Reference/sr3_options.7.html @@ -0,0 +1,1870 @@ + + + + + + + SR3 OPTIONS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3 OPTIONS

+
+

SR3 Configuration File Format

+
+
manual section:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+
name value
+name value for use
+name value_${substitution}
+.
+.
+.
+
+
+
+
+

DESCRIPTION

+

Options are placed in configuration files, one per line, in the form:

+
option <value>
+
+
+

For example:

+
debug true
+debug
+
+
+

sets the debug option to enable more verbose logging. If no value is specified, +the value true is implicit, so the above are equivalent. A second example:

+
broker amqps://anonymous@dd.weather.gc.ca
+
+
+

In the above example, broker is the option keyword, and the rest of the line is the +value assigned to the setting. Configuration files are a sequence of settings, one per line. +Note:

+
    +
  • the files are read from top to bottom, most importantly for directory, strip, mirror, +and flatten options apply to accept clauses that occur after them in the file.

  • +
  • The forward slash (/) as the path separator in Sarracenia configuration files on all +operating systems. Use of the backslash character as a path separator (as used in the +cmd shell on Windows) may not work properly. When files are read on Windows, the path +separator is immediately converted to the forward slash, so all pattern matching, +in accept, reject, strip etc… directives should use forward slashes when a path +separator is needed.

  • +
  • # is the prefix for lines of non-functional descriptions of configurations, or comments. +Same as shell and/or python scripts.

  • +
  • All options are case sensitive. Debug is not the same as debug nor DEBUG. +Those are three different options (two of which do not exist and will have no effect, +but will generate an ´unknown option’ warning).

  • +
+

The file has an inherent order, in that it is read from top to bottom, so options +set on one line often affect later lines:

+
mirror off
+directory /data/just_flat_files_here_please
+accept .*flatones.*
+
+mirror on
+directory /data/fully_mirrored
+accept .*
+
+
+

In the above snippet the mirror setting is off, then the directoy value is set, +so files whose name includes flatones will all be place in the /data/just_flat_files_here_please +directory. For files which don’t have that name, they will not be picked up +by the first accept, and so the mirror on, and the new directory setting will tak over, +and those other files will land in /data/fully_mirrored. A second example:

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+
+

Note

+

FIXME: does this match only files ending in ‘gif’ or should we add a $ to it? +will it match something like .gif2 ? is there an assumed .* at the end?

+
+

In sequence #1, all files ending in ‘gif’ are rejected. In sequence #2, the +accept .* (which accepts everything) is encountered before the reject statement, +so the reject has no effect. Some options have global scope, rather than being +interpreted in order. For thoses cases, the last declaration overrides the +ones higher in the file..

+
+
+

Variables

+

One can include substitutions in option values. They are represented by ${name}. +The name can be an ordinary environment variable, or a chosen from a number of +built-in ones. For example:

+
varTimeOffset -5m
+directory /mylocaldirectory/${%Y%m%d}/mydailies
+accept    .*observations.*
+
+rename hoho.${%o-1h%Y%m%d_%H%M%S.%f}.csv
+
+
+

In the last example above, the varTimeOffset will modify the evaluation of YYYYMMDD to be 5m in the past. +In the rename option, the time to be substituted is one hour in the past. +One can also specify variable substitutions to be performed on arguments to the directory +option, with the use of ${..} notation:

+
    +
  • %… - a datetime.strftime()

    +
      +
    • compatible date/time formatting string augmented by an offset duration suffix (o- for in the past, o+ for in the future)

    • +
    • example (complex date): ${%Y/%m/%d_%Hh%M:%S.%f} –> 2022/12/04_17h36:34.014412

    • +
    • example (add offset): ${%o-1h%Y/%m/%d_%Hh%M:%S.%f} –> 2022/12/04_16h36:34.014412

    • +
    +
  • +
  • time offset begin a strtime pattern with %o for an offset +-1(s/m/h/d/w) units.

  • +
  • SOURCE - the amqp user that injected data (taken from the notification message.)

  • +
  • BD - the base directory

  • +
  • BUP - the path component of the baseUrl (or: baseUrlPath)

  • +
  • BUPL - the last element of the baseUrl path. (or: baseUrlPathLast)

  • +
  • PBD - the post base dir

  • +
  • var - any environment variable.

  • +
  • BROKER_USER - the user name for authenticating to the broker (e.g. anonymous)

  • +
  • POST_BROKER_USER - the user name for authenticating to the post_broker (e.g. anonymous)

  • +
  • PROGRAM - the name of the component (subscribe, shovel, etc…)

  • +
  • CONFIG - the name of the configuration file being run.

  • +
  • HOSTNAME - the hostname running the client.

  • +
  • RANDID - a random id that will be consistent within a single invocation.

  • +
+

The %Y%m%d and %h time stamps refer to the time at which the data is processed by +the component, it is not decoded or derived from the content of the files delivered. +All date/times in Sarracenia are in UTC. use the varTimeOffset setting to adjust +from the current time.

+

Refer to sourceFromExchange for a common example of usage. Note that any sarracenia +built-in value takes precedence over a variable of the same name in the environment. +Note that flatten settings can be changed between directory options.

+

Note:

+
the ${% date substitutions are present, the interpretation of % patterns in filenames
+by strftime, may mean it is necessary to escape precent characters them via doubling: %%
+
+
+
+

Sundew Compatible Substituions

+

In MetPX Sundew, there is a much more strict +file naming standard, specialised for use with World Meteorological +Organization (WMO) data. Note that the file naming convention predates, and +bears no relation to the WMO file naming convention currently approved, but is strictly an internal +format. The files are separated into six fields by colon characters. The first field, DESTFN, +gives the WMO (386 style) Abbreviated Header Line (AHL) with underscores replacing blanks:

+
TTAAii CCCC YYGGGg BBB ...
+
+
+

(see WMO manuals for details) followed by numbers to render the product unique (as in practice, +though not in theory, there are a large number of products which have the same identifiers). +The meanings of the fifth field is a priority, and the last field is a date/time stamp. +The other fields vary in meaning depending on context. A sample file name:

+
SACN43_CWAO_012000_AAA_41613:ncp1:CWAO:SA:3.A.I.E:3:20050201200339
+
+
+

If a file is sent to sarracenia and it is named according to the Sundew conventions, then the +following substitution fields are available:

+
${T1}    replace by bulletin's T1
+${T2}    replace by bulletin's T2
+${A1}    replace by bulletin's A1
+${A2}    replace by bulletin's A2
+${ii}    replace by bulletin's ii
+${CCCC}  replace by bulletin's CCCC
+${YY}    replace by bulletin's YY   (obs. day)
+${GG}    replace by bulletin's GG   (obs. hour)
+${Gg}    replace by bulletin's Gg   (obs. minute)
+${BBB}   replace by bulletin's bbb
+${RYYYY} replace by reception year
+${RMM}   replace by reception month
+${RDD}   replace by reception day
+${RHH}   replace by reception hour
+${RMN}   replace by reception minutes
+${RSS}   replace by reception second
+YYYYMMDD - the current daily timestamp. (v2 compat, prefer strftime %Y%m%d )
+HH       - the current hourly timestamp. (v2 compat, prefer strftime %h )
+JJJ      - the current hourly timestamp. (v2 compat, prefer strftime %j )
+
+
+

The ‘R’ fields come from the sixth field, and the others come from the first one. +When data is injected into sarracenia from Sundew, the sundew_extension notification message header +will provide the source for these substitions even if the fields have been removed +from the delivered file names.

+

Note:

+
The version 2 compatible date strings (e.g. YYYYMMDD) originate with obsolete
+WMO practices, and support will be removed at a future date. Please use strftime
+style patterns in new configurations.
+
+
+
+
SR_DEV_APPNAME
+

The SR_DEV_APPNAME environment variable can be set so that the application configuration and state directories +are created under a different name. This is used in development to be able to have many configurations +active at once. It enables more testing than always working with the developer´s real configuration.

+

Example: export SR_DEV_APPNAME=sr-hoho… when you start up a component on a linux system, it will +look in ~/.config/sr-hoho/ for configuration files, and write state files in the ~/.cache/sr-hoho +directory.

+
+
+
+
+

OPTION TYPES

+

sr3 options come in several types:

+
+
count, size

integer count type. same as size described below.

+
+
duration

a floating point number indicating a quantity of seconds (0.001 is 1 milisecond) +modified by a unit suffix ( m-minute, h-hour, w-week )

+
+
flag

an option that has only True or False values (aka: a boolean value)

+
+
float

a floating point number.

+
+
list

a list of string values, each succeeding occurrence catenates to the total. +all v2 plugin options are declared of type list.

+
+
set

a set of string values, each succeeding occurrence is unioned to the total.

+
+
size

integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers. +alone base 10: 1k=1000, with a ‘b’ suffix, base 2: 1kb=1024

+
+
str

a string value

+
+
+
+
+

OPTIONS

+

The actual options are listed below. Note that they are case sensitive, and +only a subset are available on the command line. Those that are available +on the command line have the same effect as when specified in configuration +files.

+

The options available in configuration files:

+
+

accelThreshold <size> default: 0 (disabled.)

+

The accelThreshold indicates the minimum size of file being transferred for +which a binary downloader will be launched.

+
+
+

accelXxxCommand

+

Can specify alternate binaries for downloaders to tune for specific cases.

+ + + + + + + + + + + + + + + + + + + + + +

Option

Defaul value

accelWgetCommand

/usr/bin/wget %s -O %d

accelScpCommand

/usr/bin/scp %s %d

accelCpCommand

/usr/bin/cp %s %d

accelFtpgetCommand

/usr/bin/ncftpget %s %d

accelFtpputCommand

/usr/bin/ncftpput %s %d

+

use the %s to stand-in for the name of the source file, and %d for the +file being written. An example setting to override with:

+
accelCpCommand dd if=%s of=%d bs=4096k
+
+
+
+
+

accept, reject and acceptUnmatched

+
    +
  • accept <regexp pattern> (optional) [<keywords>]

  • +
  • reject <regexp pattern> (optional)

  • +
  • acceptUnmatched <boolean> (default: True)

  • +
+

The accept and reject options process regular expressions (regexp). +The regexp is applied to the the notification message’s URL for a match.

+

If the notification message’s URL of a file matches a reject pattern, the notification message +is acknowledged as consumed to the broker and skipped.

+

One that matches an accept pattern is processed by the component.

+

In many configurations, accept and reject options are mixed +with the directory option. They then relate accepted notification messages +to the directory value they are specified under.

+

After all accept / reject options are processed, normally +the notification message is accepted for further processing. To override that +default, set acceptUnmatched to False. The accept/reject +settings are interpreted in order. Each option is processed orderly +from top to bottom. For example:

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+

In sequence #1, all files ending in ‘gif’ are rejected. In sequence #2, the accept .* (which +accepts everything) is encountered before the reject statement, so the reject has no effect.

+

It is best practice to use server side filtering to reduce the number of notification messages sent +to the component to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all. More details on how +to apply the directives follow:

+

The accept and reject options use regular expressions (regexp) to match URL. +These options are processed sequentially. +The URL of a file that matches a reject pattern is not published. +Files matching an accept pattern are published. +Again a rename can be added to the accept option… matching products +for that accept option would get renamed as described… unless the accept matches +one file, the rename option should describe a directory into which the files +will be placed (prepending instead of replacing the file name).

+

The permDefault option allows users to specify a linux-style numeric octal +permission mask:

+
permDefault 040
+
+
+

means that a file will not be posted unless the group has read permission +(on an ls output that looks like: —r—–, like a chmod 040 <file> command). +The permDefault options specifies a mask, that is the permissions must be +at least what is specified.

+

The regexp pattern can be used to set directory parts if part of the notification message is put +to parenthesis. sender can use these parts to build the directory name. The +rst enclosed parenthesis strings will replace keyword ${0} in the directory name… +the second ${1} etc.

+

Example of use:

+
filename NONE
+
+directory /this/first/target/directory
+
+accept .*file.*type1.*
+
+directory /this/target/directory
+
+accept .*file.*type2.*
+
+accept .*file.*type3.*  DESTFN=file_of_type3
+
+directory /this/${0}/pattern/${1}/directory
+
+accept .*(2016....).*(RAW.*GRIB).*
+
+
+

A selected notification message by the first accept would be delivered unchanged to the first directory.

+

A selected notification message by the second accept would be delivered unchanged to the second directory.

+

A selected notification message by the third accept would be renamed “file_of_type3” in the second directory.

+

A selected notification message by the forth accept would be delivered unchanged to a directory.

+

It’s named /this/20160123/pattern/RAW_MERGER_GRIB/directory if the notification message would have a notice like:

+

20150813161959.854 http://this.pump.com/ relative/path/to/20160123_product_RAW_MERGER_GRIB_from_CMC

+
+
+

acceptSizeWrong: <boolean> (default: False)

+

When a file is downloaded and its size does not match the one advertised, it is +normally rejected, as a failure. This option accepts the file even with the wrong +size. helpful when file is changing frequently, and there is some queueing, so +the file is changed by the time it is retrieved.

+
+
+

attempts <count> (default: 3)

+

The attempts option indicates how many times to +attempt downloading the data before giving up. The default of 3 should be appropriate +in most cases. When the retry option is false, the file is then dropped immediately.

+

When The retry option is set (default), a failure to download after prescribed number +of attempts (or send, in a sender) will cause the notification message to be added to a queue file +for later retry. When there are no notification messages ready to consume from the AMQP queue, +the retry queue will be queried.

+
+
+

baseDir <path> (default: /)

+

baseDir supplies the directory path that, when combined with the relative +one in the selected notification gives the absolute path of the file to be sent. +The default is None which means that the path in the notification is the absolute one.

+

Sometimes senders subscribe to local xpublic, which are http url’s, but sender +needs a localfile, so the local path is built by concatenating:

+
baseDir + relative path in the baseUrl + relPath
+
+
+

When used for reception, it specifies the root of the tree that upstream files are assumed +to be from, to be replaced on download by either post_baseDir or the directory setting +in effect.

+
+
+

baseUrl_relPath <flag> (default: off)

+

Normally, the relative path (baseUrl_relPath is False, appended to the base directory) for +files which are downloaded will be set according to the relPath header included +in the notification message. If baseUrl_relPath is set, however, the notification message’s relPath will +be prepended with the sub-directories from the notification message’s baseUrl field.

+
+
+

batch <count> (default: 100)

+

The batch option is used to indicate how many files should be transferred +over a connection, before it is torn down, and re-established. On very low +volume transfers, where timeouts can occur between transfers, this should be +lowered to 1. For most usual situations the default is fine. For higher volume +cases, one could raise it to reduce transfer overhead. It is only used for file +transfer protocols, not HTTP ones at the moment.

+
+
+

blocksize <size> default: 0 (auto)

+

NOTE: EXPERIMENTAL sr3, expected to return in future version** +This blocksize option controls the partitioning strategy used to post files. +The value should be one of:

+
0 - autocompute an appropriate partitioning strategy (default)
+1 - always send entire files in a single part.
+<blocksize> - used a fixed partition size (example size: 1M )
+
+
+

Files can be announced as multiple parts. Each part has a separate checksum. +The parts and their checksums are stored in the cache. Partitions can traverse +the network separately, and in parallel. When files change, transfers are +optimized by only sending parts which have changed.

+

The outlet option allows the final output to be other than a post. +See sr3_cpump(1) for details.

+
+
+

broker

+

broker [amqp|mqtt]{s}://<user>:<password>@<brokerhost>[:port]/<vhost>

+

A URI is used to configure a connection to a notification message pump, either +an MQTT or an AMQP broker. Some Sarracenia components set a reasonable default for +that option. provide the normal user,host,port of connections. In most configuration files, +the password is missing. The password is normally only included in the +credentials.conf file.

+

Sarracenia work has not used vhosts, so vhost should almost always be /.

+

for more info on the AMQP URI format: ( https://www.rabbitmq.com/uri-spec.html )

+

either in the default.conf or each specific configuration file. +The broker option tell each component which broker to contact.

+

broker [amqp|mqtt]{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

+
+
::

(default: None and it is mandatory to set it )

+
+
+

Once connected to an AMQP broker, the user needs to bind a queue +to exchanges and topics to determine the notification messages of interest.

+
+
+

bufsize <size> (default: 1MB)

+

Files will be copied in bufsize-byte blocks. for use by transfer protocols.

+
+
+

byteRateMax <size> (default: 0)

+

byteRateMax is greater than 0, the process attempts to respect this delivery +speed in kilobytes per second… ftp,ftps,or sftp)

+

FIXME: byteRateMax… only implemented by sender? or subscriber as well, data only, or notification messages also?

+
+
+

callback <classSpec>

+

callback appends a flowcallback class to the list of those to be called during processing.

+

Most customizable processing or “plugin” logic, is implemented using the flow callback class. +At different points in notification message processing, flow callback classes define +entry_points that match that point in processing. for for every such point in the processing, +there is a list of flow callback routines to call.

+

FlowCallback Reference

+

the classSpec is similar to an import statement from python. It uses the python search +path, and also includes ~/.config/sr3/plugins. There is some shorthand to make usage +shorter for common cases. for example:

+
+

callback log

+
+

Sarracenia will first attempt, to prepend log with sarracenia.flowcb.log and then +instantiate the callback instance as an item of class sarracenia.flowcb.Log. If it does not +find such a class, then it will attempt to find a class name log, and instantiate an +object log.Log.

+

More detail here FlowCallback load_library

+
+
+

callback_prepend <classSpec>

+

identical to callback, but meant to specify functions to be executed early, that is prepended +to the list of plugins to run.

+
+
+

dangerWillRobinson (default: omitted)

+

This option is only recognized as a command line option. It is specified when an operation is expected +to have irreversibly destructive or perhaps unexpected effects. for example:

+
sr3 stop
+
+
+

will stop running components, but not those that are being run in the foreground. Stopping those +may be surprising to the analysts that will be looking at them, so that is not done by default:

+
sr3 --dangerWillRobinson stop
+
+
+

stops stops all components, including the foreground ones. Another example would be the cleanup +action. This option deletes queues and exchanges related to a configuration, which can be +destructive to flows. By default, cleanup only operates on a single configuration at a time. +One can specify this option to wreak greater havoc.

+
+
+

declare

+
+
env NAME=Value

One can also reference environment variables in configuration files, +using the ${ENV} syntax. If Sarracenia routines needs to make use +of an environment variable, then they can be set in configuration files:

+
declare env HTTP_PROXY=localhost
+
+
+
+
exchange exchange_name

using the admin url, declare the exchange with exchange_name

+
+
subscriber

A subscriber is user that can only subscribe to data and return report notification messages. Subscribers are +not permitted to inject data. Each subscriber has an xs_<user> named exchange on the pump, +where if a user is named Acme, the corresponding exchange will be xs_Acme. This exchange +is where an subscribe process will send its report notification messages.

+

By convention/default, the anonymous user is created on all pumps to permit subscription without +a specific account.

+
+
source

A user permitted to subscribe or originate data. A source does not necessarily represent +one person or type of data, but rather an organization responsible for the data produced. +So if an organization gathers and makes available ten kinds of data with a single contact +email or phone number for questions about the data and its availability, then all of +those collection activities might use a single ‘source’ account.

+

Each source gets a xs_<user> exchange for injection of data notification messages, and, similar to a subscriber +to send report notification messages about processing and receipt of data. Source may also have an xl_<user> +exchange where, as per report routing configurations, report notification messages of consumers will be sent.

+
+
feeder

A user permitted to write to any exchange. Sort of an administrative flow user, meant to pump +notification messages when no ordinary source or subscriber is appropriate to do so. Is to be used in +preference to administrator accounts to run flows.

+
+
+

User credentials are placed in the credentials.conf +file, and sr3 --users declare will update +the broker to accept what is specified in that file, as long as the admin password is +already correct.

+
    +
  • By default, all users are declared. However, flows can be specified on the command line to constrain +the declared users to only those in the given flow. For example:

    +
      +
    • sr3 --users declare will declare all users

    • +
    • sr3 --users declare subscribe/dd_amis will only declare users specified in subscribe/dd_amis

    • +
    +
  • +
+
+
+

debug

+

Setting option debug is identical to use logLevel debug

+
+
+

delete <boolean> (default: off)

+

When the delete option is set, after a download has completed successfully, the subscriber +will delete the file at the upstream source. Default is false.

+
+
+

discard <boolean> (default: off)

+

The discard option,if set to true, deletes the file once downloaded. This option can be +useful when debugging or testing a configuration.

+
+
+

directory <path> (default: .)

+

The directory option defines where to put the files on your server. +Combined with accept / reject options, the user can select the +files of interest and their directories of residence (see the mirror +option for more directory settings).

+

The accept and reject options use regular expressions (regexp) to match URL. +These options are processed sequentially. +The URL of a file that matches a reject pattern is never downloaded. +One that matches an accept pattern is downloaded into the directory +declared by the closest directory option above the matching accept option. +acceptUnmatched is used to decide what to do when no reject or accept clauses matched.

+
ex.   directory /mylocaldirectory/myradars
+      accept    .*RADAR.*
+
+      directory /mylocaldirectory/mygribs
+      reject    .*Reg.*
+      accept    .*GRIB.*
+
+
+
+
+

destfn_script <script> (default:None)

+

This Sundew compatibility option defines a script to be run when everything is ready +for the delivery of the product. The script receives the sender class +instance. The script takes the parent as an argument, and for example, any +modification to parent.msg.new_file will change the name of the file written locally.

+
+
+

download <flag> (default: True)

+

used to disable downloading in subscribe and/or sarra component. +set False by default in shovel or winnow components.

+
+
+

dry_run <flag> (default: False)

+

Run in simulation mode with respect to file transfers. Still connects to a broker and downloads and processes +messages, but transfers are disabled, for use when testing a sender, or a downloader, say to run in parallel +with an existing one, and compare the logs to see if the sender is configured to send the same files as +the old one (implemented with some other system.)

+
+
+

durable <flag> (default: True)

+

The AMQP durable option, on queue declarations. If set to True, +the broker will preserve the queue across broker reboots. +The queue will be written to and recovered from disk if the broker is restarted.

+

Note: only persistent messages will remain in a durable queue after a broker restart. +Persistent messages can be published by enabling the persistent option (it is enabled by default).

+
+
+

fileEvents <event,event,…>

+

A comma separated list of file event types to monitor. +Available file events: create, delete, link, modify, mkdir, rmdir +to only add events to the current list start the event list with a plus sign (+). +To remove them, prefix with a minus sign (-).

+

The create, modify, and delete events reflect what is expected: a file being created, modified, or deleted. +If link is set, symbolic links will be posted as links so that consumers can choose +how to process them. If it is not set, then no symbolic link events will ever be posted.

+
+

Note

+

move or rename events result in a special double post pattern, with one post as the old name +and a field newname set, and a second post with the new name, and a field oldname set. +This allows subscribers to perform an actual rename, and avoid triggering a download when possible.

+

FIXME: rename algorithm improved in v3 to avoid use of double post… just

+
+
+
+

exchange <name> (default: xpublic) and exchangeSuffix

+

The convention on data pumps is to use the xpublic exchange. Users can establish +private data flow for their own processing. Users can declare their own exchanges +that always begin with xs_<username>, so to save having to specify that each +time, one can just set exchangeSuffix kk which will result in the exchange +being set to xs_<username>_kk (overriding the xpublic default). +These settings must appear in the configuration file before the corresponding +topicPrefix and subtopic settings.

+
+
+

exchangeDeclare <flag>

+

On startup, by default, Sarracenia redeclares resources and bindings to ensure they +are uptodate. If the exchange already exists, this flag can be set to False, +so no attempt to exchange the queue is made, or it´s bindings. +These options are useful on brokers that do not permit users to declare their exchanges.

+
+
+

expire <duration> (default: 5m == five minutes. RECOMMEND OVERRIDING)

+

The expire option is expressed as a duration… it sets how long should live +a queue without connections.

+

A raw integer is expressed in seconds, if the suffix m,h,d,w +are used, then the interval is in minutes, hours, days, or weeks. After the queue expires, +the contents are dropped, and so gaps in the download data flow can arise. A value of +1d (day) or 1w (week) can be appropriate to avoid data loss. It depends on how long +the subscriber is expected to shutdown, and not suffer data loss.

+

if no units are given, then a decimal number of seconds can be provided, such as +to indicate 0.02 to specify a duration of 20 milliseconds.

+

The expire setting must be overridden for operational use. +The default is set low because it defines how long resources on the broker will be assigned, +and in early use (when default was 1 week) brokers would often get overloaded with very +long queues for left-over experiments.

+
+
+

filename <keyword> (default:None)

+

From metpx-sundew, the support of this option give all sorts of possibilities +for setting the remote filename. Some keywords are based on the fact that +metpx-sundew filenames are five (to six) fields strings separated by for colons.

+

The default value on Sundew is NONESENDER, but in the interest of discouraging use +of colon separation in files, the default in Sarracenia is WHATFN

+

The possible keywords are :

+
+
None
    +
  • the filename is not modified at all. (different from NONE!) +turn off any Sundew compatibility filename processing.

  • +
+
+
WHATFN
    +
  • the first part of the Sundew filename (string before first :)

  • +
+
+
HEADFN
    +
  • HEADER part of the sundew filename

  • +
+
+
SENDER
    +
  • the Sundew filename may end with a string SENDER=<string> in this case the <string> will be the remote filename

  • +
+
+
NONE
    +
  • deliver with the complete Sundew filename (without :SENDER=…)

  • +
+
+
NONESENDER
    +
  • deliver with the complete Sundew filename (with :SENDER=…)

  • +
+
+
TIME
    +
  • time stamp appended to filename. Example of use: WHATFN:TIME

  • +
+
+
DESTFN=str
    +
  • direct filename declaration str

  • +
+
+
SATNET=1,2,3,A
    +
  • cmc internal satnet application parameters

  • +
+
+
DESTFNSCRIPT=script.py
    +
  • invoke a script (same as destfn_script) to generate the name of the file to write

  • +
+
+
+
+
+

flatten <string> (default: ‘/’)

+

The flatten option is use to set a separator character. The default value ( ‘/’ ) +nullifies the effect of this option. This character replaces the ‘/’ in the url +directory and create a “flatten” filename from its dd.weather.gc.ca path. +For example retrieving the following url, with options:

+
http://dd.weather.gc.ca/model_gem_global/25km/grib2/lat_lon/12/015/CMC_glb_TMP_TGL_2_latlon.24x.24_2013121612_P015.grib2
+
+  flatten   -
+  directory /mylocaldirectory
+  accept    .*model_gem_global.*
+
+
+

would result in the creation of the filepath:

+
/mylocaldirectory/model_gem_global-25km-grib2-lat_lon-12-015-CMC_glb_TMP_TGL_2_latlon.24x.24_2013121612_P015.grib2
+
+
+
+
+

flowMain (default: None)

+

By default, a flow will run the sarracenia.flow.Flow class, which implements the Flow algorithm generically. +The generic version does no data transfer, only creating and manipulating messages. That is appropriate for +shovel, winnow, post & watch components, but components that transfer or transform data need +to define additional behaviour by sub-classing Flow. Examples: sarracenia.flow.sender, sarracenia.flow.poll, sarracenia.flow.subscribe.

+

The flowMain option allows a flow configuration to run a subclass of flow, instead of the default parent +class. Example:

+
flowMain subscribe
+
+
+

In a generic flow configuration file will configure the flow to act as a subscriber component. +One can create custom components by subclassing Flow and using the flowMain directive to have +it invoked.

+
+ +
+

force_polling <flag> (default: False)

+

By default, watch selects an (OS dependent) optimal method to watch a +directory.

+

For large trees, the optimal method can be manyfold (10x or even +100x) faster to recognize when a file has been modified. In some cases, +however, platform optimal methods do not work (such as with some network +shares, or distributed file systems), so one must use a slower but more +reliable and portable polling method. The force_polling keyword causes +watch to select the polling method in spite of the availability of a +normally better one.

+

For a detailed discussion, see: Detecting File Changes

+

NOTE:

+
When directories are consumed by processes using the subscriber *delete* option, they stay empty, and
+every file should be reported on every pass.  When subscribers do not use *delete*, watch needs to
+know which files are new.  It does so by noting the time of the beginning of the last polling pass.
+File are posted if their modification time is newer than that.  This will result in many multiple notification messages
+by watch, which can be minimized with the use of cache.   One could even depend on the cache
+entirely and turn on the *delete* option, which will have watch attempt to post the entire tree
+every time (ignoring mtime).
+
+**KNOWN LIMITATION**: When *force_polling* is set, the *sleep* setting should be
+at least 5 seconds. It is not currently clear why.
+
+
+
+
+

header <name>=<value>

+

Add a <name> header with the given value to a notification message. Used to pass strings as metadata in the +notification messages to improve decision making for consumers. Should be used sparingly. There are limits +on how many headers can be used, and minimizing the size of messages has important performance +impacts.

+
+
+

housekeeping <interval> (default: 300 seconds)

+

The housekeeping option sets how often to execute periodic processing as determined by +the list of on_housekeeping plugins. By default, it prints a log message every houskeeping interval.

+
+
+

include config

+

include another configuration within this configuration.

+
+
+

inflight <string> (default: .tmp or NONE if post_broker set)

+

The inflight option sets how to ignore files when they are being transferred +or (in mid-flight betweeen two systems). Incorrect setting of this option causes +unreliable transfers, and care must be taken. See Delivery Completion +for more details.

+

The value can be a file name suffix, which is appended to create a temporary name during +the transfer. If inflight is set to ., then it is a prefix, to conform with +the standard for “hidden” files on unix/linux. +If inflight ends in / (example: tmp/ ), then it is a prefix, and specifies a +sub-directory of the destination into which the file should be written while in flight.

+

Whether a prefix or suffix is specified, when the transfer is +complete, the file is renamed to its permanent name to allow further processing.

+

When detecting a file with sr3_post, sr3_cpost, or watch, or poll, the inflight option +can also be specified as a time interval, for example, 10 for 10 seconds. +When set to a time interval, file posting process ensures that it waits until +the file has not been modified in that interval. So a file will +not be processed until it has stayed the same for at least 10 seconds. +This is the same as setting the fileAgeMin setting.

+

Lastly, inflight can be set to NONE, which case the file is written directly +with the final name, where the recipient will wait to receive a post notifying it +of the file’s arrival. This is the fastest, lowest overhead option when it is available. +It is also the default when a post_broker is given, indicating that some +other process is to be notified after delivery.

+

NOTE:

+
When writing a file, if you see the error message::
+
+    inflight setting: 300, not for downloads
+
+It is because the time interval setting is only for reading files. The writer
+cannot control how long a subsequent reader will wait to look at a file being
+downloaded, so specifying a minimum modification time is inappropriate.
+in looking at local files before generating a post, it is not used as say, a means
+of delaying sending files.
+
+
+
+
+

inline <flag> (default: False)

+

When posting messages, The inline option is used to have the file content +included in the post. This can be efficient when sending small files over high +latency links, a number of round trips can be saved by avoiding the retrieval +of the data using the URL. One should only inline relatively small files, +so when inline is active, only files smaller than inlineByteMax bytes +(default: 1024) will actually have their content included in the post messages. +If inlineOnly is set, and a file is larger than inlineByteMax, the file +will not be posted.

+
+
+

inlineByteMax <size>

+

The maximum size of messages to inline.

+

when inlining file content, what sort of encoding should be done? Three choices:

+
+
    +
  • text: the file content is assumed to be utf-8 text and encoded as such.

  • +
  • binary: the file content is unconditionally converted to base64 binary encoding.

  • +
  • guess: try making text, if that fails fall back to binary.

  • +
+
+
+
+

inlineOnly

+

discard messages if the data is not inline.

+
+
+

inplace <flag> (default: On)

+

Large files may be sent as a series of parts, rather than all at once. +When downloading, if inplace is true, these parts will be appended to the file +in an orderly fashion. Each part, after it is inserted in the file, is announced to subscribers. +This can be set to false for some deployments of sarracenia where one pump will +only ever see a few parts, and not the entirety, of multi-part files.

+

The inplace option defaults to True. +Depending of inplace and if the message was a part, the path can +change again (adding a part suffix if necessary).

+
+
+

Instances

+

Sometimes one instance of a component and configuration is not enough to process & send all available notifications.

+

instances <integer> (default:1)

+

The instance option allows launching several instances of a component and configuration. +When running sender for example, a number of runtime files are created. +In the ~/.cache/sarra/sender/configName directory:

+
A .sender_configname.state         is created, containing the number instances.
+A .sender_configname_$instance.pid is created, containing the PID  of $instance process.
+
+
+

In directory ~/.cache/sarra/log:

+
A .sender_configname_$instance.log  is created as a log of $instance process.
+
+
+
+

Note

+

While the brokers keep the queues available for some time, queues take resources on +brokers, and are cleaned up from time to time. A queue which is not accessed +and has too many (implementation defined) files queued will be destroyed. +Processes which die should be restarted within a reasonable period of time to avoid +loss of notifications. A queue which is not accessed for a long (implementation dependent) +period will be destroyed.

+
+
+
+

identity <string>

+

All file notification messages include a checksum. It is placed in the amqp message header will have as an +entry sum with default value ‘d,md5_checksum_on_data’. +The sum option tell the program how to calculate the checksum. +In v3, they are called Identity methods:

+
cod,x      - Calculate On Download applying x
+sha512     - do SHA512 on file content  (default)
+md5        - do md5sum on file content
+md5name    - do md5sum checksum on filename
+random     - invent a random value for each post.
+arbitrary  - apply the literal fixed value.
+
+
+

v2 options are a comma separated string. Valid checksum flags are :

+
    +
  • 0 : no checksum… value in post is a random integer (only for testing/debugging.)

  • +
  • d : do md5sum on file content

  • +
  • n : do md5sum checksum on filename

  • +
  • p : do SHA512 checksum on filename and partstr [1]

  • +
  • s : do SHA512 on file content (default)

  • +
  • z,a : calculate checksum value using algorithm a and assign after download.

  • +
+ +
+
+

logEvents ( default: after_accept,after_work,on_housekeeping )

+

The set of points during notification message processing to emit standard log messages. +other values: on_start, on_stop, post, gather, … etc… It is comma separated, and +if the list starts with a plus sign (+) then the selected events are appended to current value. +A minus signe (-) can be used to remove events from the set.

+
+
+

logLevel ( default: info )

+

The level of logging as expressed by python’s logging. Possible values are : critical, error, info, warning, debug.

+
+
+

logMetrics ( default: False )

+

Write metrics to a daily metrics file for statistics gathering. can be used to generate statistics. +File is in the same directory as the logs, and has a date suffix.

+
+
+

logReject ( default: False )

+

Normally, messages rejection is done silently. When logReject is True, a log message will be generated for +each message rejected, and indicating the basis for the rejection.

+
+
+

logStdout ( default: False )

+

The logStdout disables log management. Best used on the command line, as there is +some risk of creating stub files before the configurations are completely parsed:

+
sr3 --logStdout start
+
+
+

All launched processes inherit their file descriptors from the parent. so all output is like an interactive session.

+

This is in contrast to the normal case, where each instance takes care of its logs, rotating and purging periodically. +In some cases, one wants to have other software take care of logs, such as in docker, where it is preferable for all +logging to be to standard output.

+

It has not been measured, but there is a reasonable likelihood that use of logStdout with large configurations (dozens +of configured instances/processes) will cause either corruption of logs, or limit the speed of execution of all processes +writing to stdout.

+
+
+

logRotateCount <max_logs> ( default: 5 )

+

Maximum number of logs (both messages and metrics) archived.

+
+
+

logRotateInterval <interval>[<time_unit>] ( default: 1d )

+

The duration of the interval with an optional time unit (ie 5m, 2h, 3d) for rotation of logs and metrics.

+
+
+

messageCountMax <count> (default: 0)

+

If messageCountMax is greater than zero, the flow will exit after processing the given +number of messages. This is normally used only for debugging.

+
+
+

messageRateMax <float> (default: 0)

+

if messageRateMax is greater than zero, the flow attempts to respect this delivery +speed in terms of messages per second. Note that the throttle is on messages obtained or generated +per second, prior to accept/reject filtering. the flow will sleep to limit the processing rate.

+
+
+

messageRateMin <float> (default: 0)

+

if messageRateMin is greater than zero, and the flow detected is lower than this rate, +a warning message will be produced:

+
+
+

message_ttl <duration> (default: None)

+

The message_ttl option set the time a message can live in the queue. +Past that time, the message is taken out of the queue by the broker.

+
+
+

mirror <flag> (default: off)

+

The mirror option can be used to mirror the dd.weather.gc.ca tree of the files. +If set to True the directory given by the directory option +will be the basename of a tree. Accepted files under that directory will be +placed under the subdirectory tree leaf where it resides under dd.weather.gc.ca. +For example retrieving the following url, with options:

+
http://dd.weather.gc.ca/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif
+
+  mirror    True
+  directory /mylocaldirectory
+  accept    .*RADAR.*
+
+
+

would result in the creation of the directories and the file +/mylocaldirectory/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif +mirror settings can be changed between directory options.

+
+
+

no <count>

+

(normally not used by humans)

+

Present on instances started by the sr3 management interface. +The no option is only used on the command line, and not intended for users. +It is an option for use by sr3 when spawning instances to inform each process +which instance it is. e.g instance 3 will be spawned with –no 3

+
+
+

nodupe_basis <data|name|path> (default: path)

+

A keyword option to identify which files are compared for +duplicate suppression purposes. Normally, the duplicate suppression uses the entire path +to identify files which have not changed. This allows for files with identical +content to be posted in different directories and not be suppressed. In some +cases, suppression of identical files should be done regardless of where in +the tree the file resides. Set ‘name’ for files of identical name, but in +different directories to be considered duplicates. Set to ‘data’ for any file, +regardless of name, to be considered a duplicate if the checksum matches.

+

This is implemented as an alias for:

+
+

callback_prepend nodupe.name

+
+

or:

+
+

callback_prepend nodupe.data

+
+

More information: Duplicate Suppresion

+
+
+

fileAgeMax

+

If files are older than this setting (default: 7h in poll, 0 in other components), +then ignore them, they are too old to post. 0 deactivates the setting.

+
+
In a Poll:
+
+
+
+
+

fileAgeMin

+

If files are newer than this setting (default: 0), then ignore them, they are too +new to post. 0 deactivates the setting.

+
+
+

nodupe_ttl <off|on|999[smhdw]>

+

When nodupe_ttl (also suppress_duplicates*, and **cache ) is set to a non-zero time +interval, each new message is compared against ones received within that interval, to see if +it is a duplicate. Duplicates are not processed further. What is a duplicate? A file with +the same name (including parts header) and checksum. Every hearbeat interval, a cleanup +process looks for files in the cache that have not been referenced in cache seconds, +and deletes them, in order to keep the cache size limited. Different settings are +appropriate for different use cases.

+

A raw integer interval is in seconds, if the suffix m,h,d, or w are used, then the interval +is in minutes, hours, days, or weeks. After the interval expires the contents are +dropped, so duplicates separated by a large enough interval will get through. +A value of 1d (day) or 1w (week) can be appropriate. Setting the option without specifying +a time will result in 300 seconds (or 5 minutes) being the expiry interval.

+

Default value in a Poll is 8 hours, should be longer than nodupe_fileAgeMax to prevent +re-ingesting files that have aged out of the duplicate suppression cache.

+

Use of the cache is incompatible with the default *parts 0* strategy, one must specify an +alternate strategy. One must use either a fixed blocksize, or always never partition files. +One must avoid the dynamic algorithm that will change the partition size used as a file grows.

+

Note that the duplicate suppresion store is local to each instance. When N +instances share a queue, the first time a posting is received, it could be +picked by one instance, and if a duplicate one is received it would likely +be picked up by another instance. For effective duplicate suppression with instances, +one must deploy two layers of subscribers. Use +a first layer of subscribers (shovels) with duplicate suppression turned +off and output with post_exchangeSplit, which route notification messages by checksum to +a second layer of subscibers (winnow) whose duplicate suppression caches are active.

+
+
+

outlet post|json|url (default: post)

+

The outlet option is used to allow writing of notification messages to file instead of +posting to a broker. The valid argument values are:

+

post:

+
+

post messages to an post_exchange

+

post_broker amqp{s}://<user>:<pw>@<brokerhost>[:port]/<vhost> +post_exchange <name> (MANDATORY) +post_topicPrefix <string> (default: “v03”) +on_post <script> (default: None)

+

The post_broker defaults to the input broker if not provided. +Just set it to another broker if you want to send the notifications +elsewhere.

+

The post_exchange must be set by the user. This is the exchange under +which the notifications will be posted.

+
+

json:

+
+

write each message (json/v03 encoded) to standard output.

+
+

url:

+
+

just output the retrieval URL to standard output.

+
+

FIXME: The outlet option came from the C implementation ( sr3_cpump ) and it has not +been used much in the python implementation.

+
+
+

overwrite <flag> (default: off)

+

The overwrite option,if set to false, avoid unnecessary downloads under these conditions :

+

1- the file to be downloaded is already on the user’s file system at the right place and

+

2- the checksum of the amqp message matched the one of the file.

+

The default is False.

+
+
+

path <path>

+

post evaluates the filesystem path from the path option +and possibly the post_baseDir if the option is used.

+

If a path defines a file then this file is watched.

+

If a path defines a directory then all files in that directory are watched…

+

This is also used to say which directories to look at for a poll

+

If this path defines a directory, all files in that directory are +watched and should watch find one (or more) directory(ies), it +watches it(them) recursively until all the tree is scanned.

+

The AMQP notification messages are made of the tree fields, the notification message time, +the url option value and the resolved paths to which were withdrawn +the post_baseDir present and needed.

+
+
+

permDefault, permDirDefault, permLog, permCopy

+

Permission bits on the destination files written are controlled by the permCopy directives. +permCopy will apply the mode permissions posted by the source of the file. +If no source mode is available, the permDefault will be applied to files, and the +permLog will be applied to directories. If no default is specified, +then the operating system defaults (on linux, controlled by umask settings) +will determine file permissions. (Note that the chmod option is interpreted as a synonym +for permDefault, and chmod_dir is a synonym for permDirDefault).

+

When set in a posting component, permCopy has the effect of including or excluding +the mode header from the messages.

+

when set in a polling component, permDefault has the of setting minimum permissions for +a file to be accepted. +(on an ls output that looks like: —r—–, like a chmod 040 <file> command). +The permDefault options specifies a mask, that is the permissions must be +at least what is specified.

+
+
+

persistent <flag> (default: True)

+

The persistent option sets the delivery_mode for an AMQP message. When True, +persistent messages will be published (delivery_mode=2), when False, transient +(non-durable, delivery_mode=1) messages will be published.

+

Persistent messages are written to disk by the broker, and will survive a broker restart. +Any transient messages in a queue will be lost when a broker is restarted. Note: persistent +messages will only survive a broker restart when they reside in a durable queue. Non-durable +queues, including all messages inside them, will be lost when a broker is restarted.

+
+
+

pollUrl <url>

+

Specification of a remote server resources to query with a poll +See the POLLING +in the Command Line Guide.

+
+
+

post_baseDir <path>

+

The post_baseDir option supplies the directory path that, when combined (or found) +in the given path, gives the local absolute path to the data file to be posted. +The post_baseDir part of the path will be removed from the posted notification message. +For sftp urls it can be appropriate to specify a path relative to a user account. +Example of that usage would be: –post_baseDir ~user –url sftp:user@host +For file: url’s, baseDir is usually not appropriate. To post an absolute path, +omit the –post_baseDir setting, and just specify the complete path as an argument.

+
+
+

post_baseUrl <url>

+

The post_baseUrl option sets how to get the file… it defines the protocol, +host, port, and optionally, the user. It is best practice to not include +passwords in urls.

+
+
+

post_broker <url>

+

the broker url to post messages to see broker for details

+
+
+

post_exchange <name> (default: xpublic)

+

The post_exchange option set under which exchange the new notification +will be posted. when publishing to a pump as an administrator, a common +choice for post_exchange is ‘xpublic’.

+

When publishing a product, a user can trigger a script, using +flow callback entry_points such as after_accept, and after_work +to modify messages generated about files prior to posting.

+
+
+

post_exchangeSplit <count> (default: 0)

+

The post_exchangeSplit option appends a two digit suffix resulting from +hashing the last character of the checksum to the post_exchange name, +in order to divide the output amongst a number of exchanges. This is currently used +in high traffic pumps to allow multiple instances of winnow, which cannot be +instanced in the normal way. Example:

+
post_exchangeSplit 5
+post_exchange xwinnow
+
+
+

will result in posting messages to five exchanges named: xwinnow00, xwinnow01, +xwinnow02, xwinnow03 and xwinnow04, where each exchange will receive only one fifth +of the total flow.

+
+
+

post_format <name> (default: v03)

+

Sets the message format for posted messages. the currently included values are:

+
    +
  • v02 … used by all existing data pumps for most cases.

  • +
  • v03 … default in sr3 JSON format easier to work with.

  • +
  • wis … a experimental geoJSON format in flux for the World Meteorological Organization

  • +
+

When provided, this value overrides whatever can be deduced from the post_topicPrefix.

+
+
+

post_on_start

+

When starting watch, one can either have the program post all the files in the directories watched +or not. (not implemented in sr3_cpost)

+
+
+

post_topic <string>

+

Explicitly set a posting topic string, overriding the usual +group of settings. For sarracenia data pumps, this should never be needed, +as the use of post_exchange, post_topicPrefix, and relpath normally builds the right +value for topics for both posting and binding.

+
+
+

post_topicPrefix (default: topicPrefix)

+

Prepended to the sub-topic to form a complete topic hierarchy. +This option applies to publishing. Denotes the version of messages published +in the sub-topics. (v03 refers to sr3_post.7.html) defaults to whatever +was received.

+
+
+

prefetch <N> (default: 1)

+

The prefetch option sets the number of messages to fetch at one time. +When multiple instances are running and prefetch is 4, each instance will obtain up to four +messages at a time. To minimize the number of messages lost if an instance dies and have +optimal load sharing, the prefetch should be set as low as possible. However, over long +haul links, it is necessary to raise this number, to hide round-trip latency, so a setting +of 10 or more may be needed.

+
+
+

queueName|queue|queue_name|qn

+
    +
  • queueName <name>

  • +
+

By default, components create a queue name that should be unique. The +default queue_name components create follows the following convention:

+
+

q_<brokerUser>.<programName>.<configName>.<random>.<random>

+
+

Where:

+
    +
  • brokerUser is the username used to connect to the broker (often: anonymous )

  • +
  • programName is the component using the queue (e.g. subscribe ),

  • +
  • configName is the configuration file used to tune component behaviour.

  • +
  • random is just a series of characters chosen to avoid clashes from multiple +people using the same configurations

  • +
+

Users can override the default provided that it starts with q_<brokerUser>.

+

When multiple instances are used, they will all use the same queue, for trivial +multi-tasking. If multiple computers have a shared home file system, then the +queue_name is written to:

+
+

~/.cache/sarra/<programName>/<configName>/<programName>_<configName>_<brokerUser>.qname

+
+

Instances started on any node with access to the same shared file will use the +same queue. Some may want use the queue_name option as a more explicit method +of sharing work across multiple nodes.

+
+
+

queueBind

+

On startup, by default, Sarracenia redeclares resources and bindings to ensure they +are uptodate. If the queue already exists, These flags can be +set to False, so no attempt to declare the queue is made, or it´s bindings. +These options are useful on brokers that do not permit users to declare their queues.

+
+
+

queueDeclare

+

FIXME: same as above.. is this normal?

+

On startup, by default, Sarracenia redeclares resources and bindings to ensure they +are uptodate. If the queue already exists, These flags can be +set to False, so no attempt to declare the queue is made, or it´s bindings. +These options are useful on brokers that do not permit users to declare their queues.

+
+
+

randomize <flag>

+

Active if -r|–randomize appears in the command line… or randomize is set +to True in the configuration file used. If there are several notification messages because the +file is posted by block (the blocksize option was set), the block notification messages +are randomized meaning that they will not be posted

+
+
+

realpathAdjust <count> (Experimental) (default: 0)

+

The realpathAdjust option adjusts how much paths are resolved with the C standard realpath +library routine. The count indicates how many path elements should be ignored, counting +from the beginning of the path with positive numbers, or the end with negative ones. +An adjustment of zero means to apply realpath to the entire path.

+

Implemented in C, but not python currently.

+
+
+

realpathFilter <flag> (Experimental)

+

the realpathFilter option resolves paths using the C standard realpath library routine, +but only for the purpose of applying accept reject filters. This is used only during +posting.

+

This option is being used to study some use cases, and may disappear in future.

+

Implemented in C, but not python currently.

+
+
+

realpathPost <flag> (Experimental)

+

The realpathPost option resolves paths given to their canonical ones, eliminating +any indirection via symlinks. The behaviour improves the ability of watch to +monitor trees, but the trees may have completely different paths than the arguments +given. This option also enforces traversing of symbolic links.

+

This option is being used to investigate some use cases, and may disappear in future.

+
+
+

sendTo <url>

+

Specification of a remote resource to deliver to in a sender.

+
+
+

rename <path>

+

With the rename option, the user can suggest a destination path for its files. If the given +path ends with ‘/’ it suggests a directory path… If it doesn’t, the option specifies a file renaming. +Often used with variable substitutions, to provide dynamic, patterned names.

+
+
+

report and report_exchange

+

NOTE: NOT IMPLEMENTEDin sr3, expected to return in future version +For each download, by default, an amqp report message is sent back to the broker. +This is done with option :

+
    +
  • report <flag> (default: True)

  • +
  • report_exchange <report_exchangename> (default: xreport|xs_*username* )

  • +
+

When a report is generated, it is sent to the configured report_exchange. Administrative +components post directly to xreport, whereas user components post to their own +exchanges (xs_*username*). The report daemons then copy the messages to xreport after validation.

+

These reports are used for delivery tuning and for data sources to generate statistical information. +Set this option to False, to prevent generation of reports.

+
+
+

reset <flag> (default: False)

+

When reset is set, and a component is (re)started, its queue is +deleted (if it already exists) and recreated according to the component’s +queue options. This is when a broker option is modified, as the broker will +refuse access to a queue declared with options that differ from what was +set at creation. It can also be used to discard a queue quickly when a receiver +has been shut down for a long period. If duplicate suppression is active, then +the reception cache is also discarded.

+

The AMQP protocol defines other queue options which are not exposed +via sarracenia, because sarracenia itself picks appropriate values.

+
+
+

retryEmptyBeforeExit: <boolean> (default: False)

+

Used for sr_insects flow tests. Prevents Sarracenia from exiting while there are messages remaining in the retry queue(s). By default, a post will cleanly exit once it has created and attempted to publish messages for all files in the specified directory. If any messages are not successfully published, they will be saved to disk to retry later. If a post is only run once, as in the flow tests, these messages will never be retried unless retryEmptyBeforeExit is set to True.

+
+
+

retry_refilter <boolean> (default: False)

+

The retry_refilter option alters how messages are reloaded when they are retrieved from +a retry queue. The default way (value: False) is to repeat the transfer using exactly +the same message as before. If retry_refilter is set (value: True) then all the +message’s calculated fields will be discarded, and the processing re-started from the gather +phase (accept/reject processing will be repeated, destinations re-calculated.)

+

The normal retry behaviour is use when the remote has had a failure, and need to +re-send later, while the retry_refilter option is used when recovering from configuration +file errors, and some messages had incorrect selection or destination criteria.

+
+
+

retry_ttl <duration> (default: same as expire)

+

The retry_ttl (retry time to live) option indicates how long to keep trying to send +a file before it is aged out of a the queue. Default is two days. If a file has not +been transferred after two days of attempts, it is discarded.

+
+
+

sanity_log_dead <interval> (default: 1.5*housekeeping)

+

The sanity_log_dead option sets how long to consider too long before restarting +a component.

+
+
+

scheduled_interval,scheduled_hour,scheduled_minute

+

When working with scheduled flows, such as polls, one can configure a duration +(no units defaults to seconds, suffixes: m-minute, h-hour) at which to run a +given activity:

+
scheduled_interval 30
+
+
+

run the flow or poll every 30 seconds. If no duration is set, then the +flowcb.scheduled.Scheduled class will look for the other two time specifiers:

+
scheduled_hour 1,4,5,23
+scheduled_minute 14,17,29
+
+
+

which will have the poll run each day at: 01:14, 01:17, 01:29, then the same minutes +after each of 4h, 5h and 23h.

+
+
+

shim_defer_posting_to_exit (EXPERIMENTAL)

+
+

(option specific to libsrshim) +Postpones file posting until the process exits. +In cases where the same file is repeatedly opened and appended to, this +setting can avoid redundant notification messages. (default: False)

+
+
+
+

shim_post_minterval interval (EXPERIMENTAL)

+
+

(option specific to libsrshim) +If a file is opened for writing and closed multiple times within the interval, +it will only be posted once. When a file is written to many times, particularly +in a shell script, it makes for many notification messages, and shell script affects performance. +subscribers will not be able to make copies quickly enough in any event, so +there is little benefit, in say, 100 notification messages of the same file in the same second. +It is wise set an upper limit on the frequency of posting a given file. (default: 5s) +Note: if a file is still open, or has been closed after its previous post, then +during process exit processing it will be posted again, even if the interval +is not respected, in order to provide the most accurate final post.

+
+
+
+

shim_skip_parent_open_files (EXPERIMENTAL)

+
+

(option specific to libsrshim) +The shim_skip_ppid_open_files option means that a process checks +whether the parent process has the same file open, and does not +post if that is the case. (default: True)

+
+
+
+

sleep <time>

+

The time to wait between generating events. When files are written frequently, it is counter productive +to produce a post for every change, as it can produce a continuous stream of changes where the transfers +cannot be done quickly enough to keep up. In such circumstances, one can group all changes made to a file +in sleep time, and produce a single post.

+

When sleep is set > 0 for use with a poll it has the effect to setting scheduled_interval to that value +for compatibility reasons. It is better for poll to use scheduled settings explicitly going forward.

+
+
+

statehost <False|True> ( default: False )

+

In large data centres, the home directory can be shared among thousands of +nodes. Statehost adds the node name after the cache directory to make it +unique to each node. So each node has it’s own statefiles and logs. +example, on a node named goofy, ~/.cache/sarra/log/ becomes ~/.cache/sarra/goofy/log.

+
+
+

strip <count|regexp> (default: 0)

+

You can modify the relative mirrored directories with the strip option. +If set to N (an integer) the first ‘N’ directories from the relative path +are removed. For example:

+
http://dd.weather.gc.ca/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif
+
+  mirror    True
+  strip     3
+  directory /mylocaldirectory
+  accept    .*RADAR.*
+
+
+

would result in the creation of the directories and the file +/mylocaldirectory/WGJ/201312141900_WGJ_PRECIP_SNOW.gif +when a regexp is provide in place of a number, it indicates a pattern to be removed +from the relative path. For example if:

+
strip  .*?GIF/
+
+
+

Will also result in the file being placed the same location. +Note that strip settings can be changed between directory options.

+
+
NOTE::

with strip, use of ? modifier (to prevent regular expression greediness ) is often helpful. +It ensures the shortest match is used.

+

For example, given a file name: radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.GIF +The expression: .*?GIF matches: radar/PRECIP/GIF +whereas the expression: .*GIF matches the entire name.

+
+
+
+
+

sourceFromExchange <flag> (default: off)

+

The sourceFromExchange option is mainly for use by administrators. +If messages received are posted directly from a source, the exchange used +is ‘xs_<brokerSourceUsername>’. Such messages could be missing source and from_cluster +headings, or a malicious user may set the values incorrectly. +To protect against both problems, administrators should set the sourceFromExchange option.

+

When the option is set, values in the message for the source and from_cluster headers will then be overridden:

+
self.msg.headers['source']       = <brokerUser>
+self.msg.headers['from_cluster'] = cluster
+
+
+

replacing any values present in the message. This setting should always be used when ingesting data from a +user exchange. These fields are used to return reports to the origin of injected data. +It is commonly combined with:

+
*mirror true*
+*sourceFromExchange true*
+*directory ${PBD}/${YYYYMMDD}/${SOURCE}*
+
+
+

To have data arrive in the standard format tree.

+
+
+

sourceFromMessage <flag> (default: off)

+

The sourceFromMessage option is mainly for use by administrators. +Normally the source field from an inbound message is ignored. +When this option is set, the field in the message is accepted and used +for processing. (overrides source, and sourceFromExchange )

+

It defaults to off because malicious messages can misrepresent data +origin. To be used only with flows of responsibly curated, trustable +message flows.

+
+
+

subtopic <amqp pattern> (default: #)

+

Within an exchange’s postings, the subtopic setting narrows the product selection. +To give a correct value to the subtopic, +one has the choice of filtering using subtopic with only AMQP’s limited wildcarding and +length limited to 255 encoded bytes, or the more powerful regular expression +based accept/reject mechanisms described below. The difference being that the +AMQP filtering is applied by the broker itself, saving the notices from being delivered +to the client at all. The accept/reject patterns apply to messages sent by the +broker to the subscriber. In other words, accept/reject are client side filters, +whereas subtopic is server side filtering.

+

It is best practice to use server side filtering to reduce the number of notification messages sent +to the client to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all.

+

topicPrefix is primarily of interest during protocol version transitions, +where one wishes to specify a non-default protocol version of messages to +subscribe to.

+

Usually, the user specifies one exchange, and several subtopic options. +Subtopic is what is normally used to indicate messages of interest. +To use the subtopic to filter the products, match the subtopic string with +the relative path of the product.

+

For example, consuming from DD, to give a correct value to subtopic, one can +browse the our website http://dd.weather.gc.ca and write down all directories +of interest. For each directory tree of interest, write a subtopic +option as follow:

+
+

subtopic directory1.*.subdirectory3.*.subdirectory5.#

+
+
where:
+      *                matches a single directory name
+      #                matches any remaining tree of directories.
+
+
+
+
note:

When directories have these wild-cards, or spaces in their names, they +will be URL-encoded ( ‘#’ becomes %23 ) +When directories have periods in their name, this will change +the topic hierarchy.

+
+
FIXME:

hash marks are URL substituted, but did not see code for other values. +Review whether asterisks in directory names in topics should be URL-encoded. +Review whether periods in directory names in topics should be URL-encoded.

+
+
+
+
+

One can use multiple bindings to multiple exchanges as follows:

+
exchange A
+subtopic directory1.*.directory2.#
+
+exchange B
+subtopic *.directory4.#
+
+
+

Will declare two separate bindings to two different exchanges, and two different file trees. +While default binding is to bind to everything, some brokers might not permit +clients to set bindings, or one might want to use existing bindings. +One can turn off queue binding as follows:

+
subtopic None
+
+
+

(False, or off will also work.)

+
+
+

sundew_compat_regex_first_match_is_zero (default: off)

+

When numbering groups in match patterns, Sundew groups start from 0. +Python regular expressions use the zeroth group to represent the entire string, and each match +group starts from 1. It is considered less surprising to conform to Python conventions, +but doing so unilaterally would break compatbility. So here is a switch to use +when bridging between Sundew, sarra v2 and sr3. Eventually, this should always be off. +Examples:

+ +

in contrast, to get the same result:

+
    +
  • sundew_compat_regex_first_match_is_zero: False

  • +
  • directory setting: /tmp/meteocode/${3}/${1}/${2}

  • +
  • to get the same result.

  • +
+

folks who research python re will normally produce the latter version first.

+
+
+

timeCopy (default: on)

+

On unix-like systems, when the ls commend or a file browser shows modification or +access times, it is a display of the posix st_atime, and st_ctime elements of a +struct struct returned by stat(2) call. When timeCopy is on, headers +reflecting these values in the messages are used to restore the access and modification +times respectively on the subscriber system. To document delay in file reception, +this option can be turned off, and then file times on source and destination compared.

+

When set in a posting component, it has the effect of eliding the atime and mtime +headers from the messages.

+
+
+

topicCopy (default: off)

+

Setting topicCopy to true tells sarracenia pass topics through unaltered. +Sarracenia has a convention for how topics for products should be organized. There is +a topicPrefix, followed by subtopics derived from the relPath field of the message. +Some networks may choose to use different topic conventions, external to sarracenia.

+
+
+

timeout <interval> (default: 0)

+

The timeout option, sets the number of seconds to wait before aborting a +connection or download transfer (applied per buffer during transfer).

+
+
+

timezone <string> (default: UTC)

+

Interpret listings from an FTP server as being in the given timezone as per pytz +Examples: Canada/Pacific, Pacific/Nauru, Canada/Eastern, Europe/Paris +Has no effect other than in when polling an FTP server.

+
+
+

tlsRigour (default: medium)

+

tlsRigour can be set to: lax, medium, or strict, and gives a hint to the +application of how to configure TLS connections. TLS, or Transport Level +Security (used to be called Secure Socket Layer (SSL)) is the wrapping of +normal TCP sockets in standard encryption. There are many aspects of TLS +negotiations, hostname checking, Certificate checking, validation, choice of +ciphers. What is considered secure evolves over time, so settings which, a few +years ago, were considered secure, are currently aggressively deprecated. This +situation naturally leads to difficulties in communication due to different +levels of compliance with whatever is currently defined as rigourous encryption.

+

If a site being connected to, has, for example, and expired certificate, and +it is nevertheless necessary to use it, then set tlsRigour to lax and +the connection should succeed regardless.

+
+
+

topic <string>

+

Explicitly set a subscribing topic string, overriding the value usually +derived from a group of settings. For sarracenia data pumps, this should never be needed, +as the use of exchange, topicPrefix, and subtopic normally builds the right +value.

+
+
+

topicPrefix (default: v03)

+

prepended to the sub-topic to form a complete topic hierarchy. +This option applies to subscription bindings. +Denotes the version of messages received in the sub-topics. (v03 refers to sr3_post.7.html)

+
+
+

users <flag> (default: false)

+

As an adjunct when the declare action is used, to ask sr3 to declare users +on the broker, as well as queues and exchanges.

+
+
+

v2compatRenameDoublePost <flag> ( default: false)

+

version 3 of Sarracenia features improved logic around file renaming, using a single message per rename operation. +Version 2 required two posts. When posting, in a mirroring situation, for consumption by v2 clients, this flag +should be set.

+
+
+

varTimeOffset (default: 0)

+

For example:

+
varTimeOffset -7m
+
+
+

will cause variable substitions that involve the date/time substitutions. +so in a pattern like ${YYYY}/${MM}/${DD} will be evaluated to be the +date, evaluated seven minutes in the past.

+
+
+

vip - ACTIVE/PASSIVE OPTIONS

+

The vip option indicates that a configuration must be active on only +a single node in a cluster at a time, a singleton. This is typically +required for a poll component, but it can be used in senders or other +cases.

+

subscribe can be used on a single server node, or multiple nodes +could share responsibility. Some other, separately configured, high availability +software presents a vip (virtual ip) on the active server. Should +the server go down, the vip is moved on another server, and processing +then happens using the new server that now has the vip active. +Both servers would run an sr3 instance:

+
- **vip          <list>          []**
+
+
+

When you run only one sr3 instance on one server, these options are not set, +and subscribe will run in ‘standalone mode’.

+

In the case of clustered brokers, you would set the options for the +moving vip.

+

vip 153.14.126.3

+

When an sr3 instance does not find the vip, it sleeps for 5 seconds and retries. +If it does, it consumes and processes a message and than rechecks for the vip. +Multiple vips form a list, where any individual address being active is enough.

+
+
+

wololo

+

A command line option to overwite an existing sr3 configuration when converting +from v2.

+
+
+
+

SEE ALSO

+

sr3(1) - Sarracenia main command line interface.

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

sr3_cpump(1) - C implementation of the shovel component. (copy messages)

+

Formats:

+

sr3_post(7) - the format of notification messages.

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr3_post.1.html b/Reference/sr3_post.1.html new file mode 100644 index 000000000..b00c1044c --- /dev/null +++ b/Reference/sr3_post.1.html @@ -0,0 +1,585 @@ + + + + + + + Sr3_Post — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sr3_Post

+
+

Publish the Availability of Files

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPx Sarracenia Suite

+
+
+
+

SYNOPSIS

+

sr3_post|sr3_cpost [ OPTIONS ][ -pb|–post_broker broker ][ -pbu|–post_baseUrl url[,url]…* ] +[ -p|–path ] path1 path2…pathN ]

+

( also libsrshim.so )

+
+
+

DESCRIPTION

+

sr3_post posts the availability of a file by creating an notification message. +In contrast to most other sarracenia components that act as daemons, +sr3_post is a one shot invocation which posts and exits. +To make files +available to subscribers, sr3_post sends the notification messages +to an AMQP server, also called a broker.

+

This manual page is primarily concerned with the python implementation, +but there is also an implementation in C, which works nearly identically. +Differences:

+
+
    +
  • plugins are not supported in the C implementation.

  • +
  • C implementation uses POSIX regular expressions, python3 grammar is slightly different.

  • +
  • when the sleep option ( used only in the C implementation) is set to > 0, +it transforms sr_cpost into a daemon that works like the watch component +of sr3(1).

  • +
+
+
+

Mandatory Settings

+

The post_base_url url,url,… option specifies the location +subscribers will download the file from. There is usually one post per file. +Format of argument to the post_base_url option:

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+file:
+
+
+

When several urls are given as a comma separated list to post_base_url, the +url´s provided are used round-robin style, to provide a coarse form of load balancing.

+

The [-p|–path path1 path2 .. pathN] option specifies the path of the files +to be announced. There is usually one post per file. +Format of argument to the path option:

+
/absolute_path_to_the/filename
+or
+relative_path_to_the/filename
+
+
+

The -pipe option can be specified to have sr3_post read path names from standard +input as well.

+

An example invocation of sr3_post:

+
sr3_post --post_broker amqp://broker.com --post_baseUrl sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo
+
+
+

By default, sr3_post reads the file /data/shared/products/foo and calculates its checksum. +It then builds a post message, logs into broker.com as user ‘guest’ (default credentials) +and sends the post to defaults vhost ‘/’ and default exchange. The default exchange +is the prefix xs_ followed by the broker username, hence defaulting to ‘xs_guest’. +A subscriber can download the file /data/shared/products/foo by authenticating as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials. +The output of the command is as follows

+
[INFO] Published xs_guest v02.post.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' sum=d,82edc8eb735fd99598a1fe04541f558d parts=1,4574,1,0,0
+
+
+

In MetPX-Sarracenia, each post is published under a certain topic. +The log line starts with ‘[INFO]’, followed by the topic of the +post. Topics in AMQP are fields separated by dot. The complete topic starts with +a topicPrefix (see option), version V02, an action post, +followed by a subtopic (see option) here the default, the file path separated with dots +data.shared.products.foo.

+

The second field in the log line is the message notice. It consists of a time +stamp 20150813161959.854, and the source URL of the file in the last 2 fields.

+

The rest of the information is stored in AMQP message headers, consisting of key=value pairs. +The sum=d,82edc8eb735fd99598a1fe04541f558d header gives file fingerprint (or checksum +) information. Here, d means md5 checksum performed on the data, and 82edc8eb735fd99598a1fe04541f558d +is the checksum value. The parts=1,4574,1,0,0 state that the file is available in 1 part of 4574 bytes +(the filesize.) The remaining 1,0,0 is not used for transfers of files with only one part.

+

Another example:

+
sr3_post --post_broker mqtt://broker.com --post_baseDir /data/web/public_data --postBaseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric/SACN32_CWAO_123456
+
+
+

By default, sr3_post reads the file /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concatenating the post_base_dir and relative path of the source url to obtain the local file path) +and calculates its checksum. It then builds a post message, logs into broker.com as user ‘guest’ +(default credentials) and sends the post to defaults vhost ‘/’ and exchange ‘xs_guest’, resulting +in publication to the MQTT broker under the topic: xs_guest/v03/bulletins/alphanumeric/SACN32_CWAO_123456

+

A subscriber can download the file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca.

+
+
+
+

ARGUMENTS AND OPTIONS

+

Please refer to the sr3_options(7) manual page for a detailed description of +all settings, and methods of specifying them.

+
+

path path1 path2 … pathN

+
+

sr3_post evaluates the filesystem paths from the path option +and possibly the baseDir if the option is used.

+

If a path defines a file, this file is announced.

+

If a path defines a directory, then all files in that directory are +announced…

+
+
+
+

post_broker <broker>

+
+

the broker to which the post is sent.

+
+
+
+

post_baseDir <path>

+
+

The base_dir option supplies the directory path that, +when combined (or found) in the given path, +gives the local absolute path to the data file to be posted. +The document root part of the local path will be removed from the posted notification message. +For sftp URLs: it can be appropriate to specify a path relative to a user account. +Example of that usage would be: -dr ~user -post_base_url sftp:user@host +For file URLs: base_dir is usually not appropriate. To post an absolute path, +omit the -dr setting, and just specify the complete path as an argument.

+
+
+
+

post_exchange <exchange>

+
+

Sr_post publishes to an exchange named xs_”broker_username” by default. +Use the post_exchange option to override that default.

+
+
+
+

-h|–help

+
+

Display program options.

+
+
+
+

blocksize <value>

+

Not currently useful, will re-instate post v3

+

This option controls the partitioning strategy used to post files. +The value should be one of:

+
0 - autocompute an appropriate partitioning strategy (default)
+1 - always send entire files in a single part.
+<blocksize> - used a fixed partition size (example size: 1M )
+
+
+

Files can be announced as multiple parts. Each part has a separate checksum. +The parts and their checksums are stored in the cache. Partitions can traverse +the network separately, and in parallel. When files change, transfers are +optimized by only sending parts which have changed.

+

The value of the blocksize is an integer that may be followed by letter designator [B|K|M|G|T] meaning: +for Bytes, Kilobytes, Megabytes, Gigabytes, Terabytes respectively. All these references are powers of 2. +Files bigger than this value will get announced with blocksize sized parts.

+

The autocomputation algorithm determines a blocksize that encourages a reasonable number of parts +for files of various sizes. As the file size varies, the automatic computation will give different +results. This will result in resending information which has not changed as partitions of a different +size will have different sums, and therefore be tagged as different.

+

By default, sr3_post computes a reasonable blocksize that depends on the file size. +The user can set a fixed blocksize if it is better for its products or if he wants to +take advantage of the cache mechanism. In cases where large files are being appended to, for example, +it make sense to specify a fixed partition size so that the blocks in the cache will be the +same blocks as those generated when the file is larger, and so avoid re-transmission. So use +of ‘10M’ would make sense in that case.

+

In cases where a custom downloader is used which does not understand partitioning, it is necessary +to avoid having the file split into parts, so one would specify ‘1’ to force all files to be sent +as a single part.

+
+
+

post_baseUrl <url>

+

The url option sets the protocol, credentials, host and port under +which the product can be fetched.

+

The AMQP announcememet is made of the three fields, the notification message time, +this url value and the given path to which was withdrawn from the base_dir +if necessary.

+

The concatenation of the two last fields of the notification message defines +what the subscribers will use to download the product.

+
+
+

reset

+

When one has used –suppress_duplicates|–cache, this option empties the cache.

+
+
+

rename <path>

+

With the rename option, the user can suggest a destination path to its files. If the given +path ends with ‘/’ it suggests a directory path… If it doesn’t, the option specifies a file renaming.

+

sr3_post, and sr_watch use a file based model based on a process and a disk cache, +whose design is single threaded. The shim library is typically used by many processes +at once, and would have resource contention and/or corruption issues with the cache. +The shim library therefore has a purely memory-based cache, tunable with +the following shim_ options.

+
+
+

shim_defer_posting_to_exit EXPERIMENTAL

+
+

Postpones file posting until the process exits. +In cases where the same file is repeatedly opened and appended to, this +setting can avoid redundant notification messages. (default: False)

+
+
+
+

shim_post_minterval interval EXPERIMENTAL

+
+

If a file is opened for writing and closed multiple times within the interval, +it will only be posted once. When a file is written to many times, particularly +in a shell script, it makes for many notification messages, and shell script affects performance. +subscribers will not be able to make copies quickly enough in any event, so +there is little benefit, in say, 100 notification messages of the same file in the same second. +It is wise set an upper limit on the frequency of posting a given file. (default: 5s) +Note: if a file is still open, or has been closed after its previous post, then +during process exit processing it will be posted again, even if the interval +is not respected, in order to provide the most accurate final post.

+
+
+
+

shim_skip_parent_open_files EXPERIMENTAL

+
+

The shim_skip_ppid_open_files option means that a process checks +whether the parent process has the same file open, and does not +post if that is the case. (default: True)

+
+
+
+

sleep time

+
+

This option is only available in the c implementation (sr_cpost)

+

When the option is set, it transforms cpost into a sr_watch, with sleep being the time to wait between +generating events. When files are written frequently, it is counter productive to produce a post for +every change, as it can produce a continuous stream of changes where the transfers cannot be done quickly +enough to keep up. In such circumstances, one can group all changes made to a file +in sleep time, and produce a single post.

+
+
NOTE::

in sr_cpost, when combined with force_polling (see sr_watch(1) ) the sleep +interval should not be less than about five seconds, as it may miss posting some files.

+
+
+
+
+
+

subtopic <key>

+
+

The subtopic default can be overwritten with the subtopic option.

+
+
+
+

nodupe_ttl on|off|999

+
+

Avoid posting duplicates by comparing each file to those seen during the +suppress_duplicates interval. When posting directories, will cause +sr3_post post only files (or parts of files) that were new when invoked again.

+

Over time, the number of files in the cache can grow too large, and so it is cleaned out of +old entries. The default lifetime of a cache entry is five minutes (300 seconds). This +lifetime can be overridden with a time interval as argument ( the 999 above ).

+

If duplicate suppression is in use, one should ensure that a fixed blocksize is +used ( set to a value other than 0 ) as otherwise blocksize will vary as files grow, +and much duplicate data transfer will result.

+
+
+
+

identity <method>[,<value>]

+

All file notification messages include a checksum. The sum option specifies how to calculate the it. +It is a comma separated string. Valid Identity methods are

+
cod,x      - Calculate On Download applying x
+sha512     - do SHA512 on file content  (default)
+md5        - do md5sum on file content
+random     - invent a random value for each post.
+arbitrary  - apply the literal fixed value.
+
+
+
+

Note

+

The checksums are stored in extended file attributes (or Alternate Data Streams on Windows). +This is necessary for the arbitrary method to work, since we have no means of calculating it.

+
+
+
+

topicPrefix <key>

+

Not usually used +By default, the topic is made of the default topicPrefix : version V03 +followed by the default subtopic: the file path separated with dots (dot being the topic separator for amqp). +You can overwrite the topicPrefix by setting this option.

+
+
+

header <name>=<value>

+

Add a <name> header with the given value to notification messages. Used to pass strings as metadata.

+
+
+
+

SHIM LIBRARY USAGE

+

Rather than invoking a sr3_post to post each file to publish, one can have processes automatically +post the files they right by having them use a shim library intercepting certain file i/o calls to libc +and the kernel. To activate the shim library, in the shell environment add:

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

where shimpost.conf is an sr_cpost configuration file in +the ~/.config/sarra/post/ directory. An sr_cpost configuration file is the same +as an sr3_post one, except that plugins are not supported. With the shim +library in place, whenever a file is written, the accept/reject clauses of +the shimpost.conf file are consulted, and if accepted, the file is posted just +as it would be by sr3_post. If using with ssh, where one wants files which are +scp’d to be posted, one needs to include the activation in the .bashrc and pass +it the configuration to use:

+
expoert LC_SRSHIM=shimpost.conf
+
+
+

Then in the ~/.bashrc on the server running the remote command:

+
if [ "$LC_SRSHIM" ]; then
+    export SR_POST_CONFIG=$LC_SRSHIM
+    export LD_PRELOAD="libsrshim.so.1"
+fi
+
+
+

SSH will only pass environment variables that start with LC_ (locale) so to get it +passed with minimal effort, we use that prefix.

+
+

Shim Usage Tips

+

This method of notification does require some user environment setup. +The user environment needs to the LD_PRELOAD environment variable set +prior to launch of the process. Complications that remain as we have +been testing for two years since the shim library was first implemented:

+
    +
  • if we want to notice files created by remote scp processes (which create non-login shells) +then the environment hook must be in .bashrc. and using an environment +variable that starts with LC_ to have ssh transmit the configuration value without +having to modify sshd configuration in typical linux distributions. +( full discussion: https://github.com/MetPX/sarrac/issues/66 )

  • +
  • code that has certain weaknesses, such as in FORTRAN a lack of IMPLICIT NONE +https://github.com/MetPX/sarracenia/issues/69 may crash when the shim library +is introduced. The correction needed in those cases has so far been to correct +the application, and not the library. +( also: https://github.com/MetPX/sarrac/issues/12 )

  • +
  • codes using the exec call ot tcl/tk, by default considers any +output to file descriptor 2 (standard error) as an error condition. +these messages can be labelled as INFO, or WARNING priority, but it will +cause the tcl caller to indicate a fatal error has occurred. Adding +-ignorestderr to invocations of exec avoids such unwarranted aborts.

  • +
  • Complex shell scripts can experience an inordinate performance impact. +Since high performance shell scripts is an oxymoron, the best solution, +performance-wise is to re-write the scripts in a more efficient scripting +language such as python ( https://github.com/MetPX/sarrac/issues/15 )

  • +
  • Code bases that move large file hierarchies (e.g. mv tree_with_thousands_of_files new_tree ) +will see a much higher cost for this operation, as it is implemented as +a renaming of each file in the tree, rather than a single operation on the root. +This is currently considered necessary because the accept/reject pattern matching +may result in a very different tree on the destination, rather than just the +same tree mirrored. See Rename Processing below for details.

  • +
  • export SR_SHIMDEBUG=1 will get your more output than you want. use with care.

  • +
+
+
+

Rename Processing

+

It should be noted that file renaming is not as simple in the mirroring case as in the underlying +operating system. While the operation is a single atomic one in an operating system, when +using notifications, there are accept/reject cases that create four possible effects.

+ + + + + + + + + + + + + + + + + + +

old name is:

New name is:

Accepted

Rejected

Accepted

rename

copy

Rejected

remove

nothing

+

When a file is moved, two notifications are created:

+
    +
  • One notification has the new name in the relpath, while containing and oldname +field pointing at the old name. This will trigger activities in the top half of +the table, either a rename, using the oldname field, or a copy if it is not present +at the destination.

  • +
  • A second notification with the oldname in relpath which will be accepted +again, but this time it has the newname field, and process the remove action.

  • +
+

While the renaming of a directory at the root of a large tree is a cheap atomic operation +in Linux/Unix, mirroring that operation requires creating a rename posting for each file +in the tree, and thus is far more expensive.

+
+
+
+

ENVIRONMENT VARIABLES

+

In the C implementation (sr_cpost), if the SR_CONFIG_EXAMPLES variable is set, then the add directive can be used +to copy examples into the user’s directory for use and/or customization.

+

An entry in the ~/.config/sarra/default.conf (created via sr_subscribe edit default.conf ) +could be used to set the variable:

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

the value should be available from the output of a list command from the python +implementation.

+
+
+

SEE ALSO

+

sr3(1) - Sarracenia main command line interface.

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

sr3_cpump(1) - C implementation of the shovel component. (copy messages)

+

Formats:

+

sr3_credentials(7) - Convert logfile lines to .save Format for reload/resend.

+

sr3_options(7) - the configuration options

+

sr_post(7) - the format of notification messages.

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Reference/sr_post.7.html b/Reference/sr_post.7.html new file mode 100644 index 000000000..7fa151294 --- /dev/null +++ b/Reference/sr_post.7.html @@ -0,0 +1,824 @@ + + + + + + + SR_post — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR_post

+
+

Sarracenia v03 Notification Message Format/Protocol

+
+
Manual section:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

STATUS: Stable/Default

+

Sarracenia version 2 notification messages are the previous standard, used for terabytes +and millions of files per day of transfers. Version 3 is a proposal for a next +iteration of Sarracenia notification messages.

+

Most fields and their meaning is the same in version 3 as it was in version 2. +Some fields are changing as the protocol is exposed to wider review than previously.

+

The change in payload protocol is targetted at simplifying future implementations +and enabling use by messaging protocols other than pre-1.0 AMQP. +See v03 Changes for more details.

+

To generate notification messages in v03 format, use following setting:

+
post_topicPrefix v03
+
+
+

To select notification messages to consume in that format:

+
topicPrefix v03
+
+
+
+
+

SYNOPSIS

+

Version 03 format of file change notification messages for sr_post.

+

An sr3_post notification message consists of a topic, and the BODY

+
+

AMQP Topic: <version>.{<dir>.}

+
<version> = "v03" the version of the protocol or format.
+"post" = the type of notification message within the protocol.
+<dir> = a sub-directory leading to the file (perhaps many directories deep)
+
+
+
+
+

BODY: { <headers> } (JSON encoding.)

+

The headers are an array of name:value pairs:

+
MANDATORY:
+
+        "pubTime"       - YYYYMMDDTHHMMSS.ss - UTC date/timestamp.
+        "baseUrl"       - root of the url to download.
+        "relPath"       - relative path can be catenated to <base_url>
+
+   one of:
+
+        "identity"     - for changes in file contents, an identifier for de-duplication purposes.
+        {
+           "method" : "md5" | "sha512" | "cod" | "random" ,
+           "value"  : "base64 encoded checksum value"
+        }
+  or:
+        "fileOp"   - to describe non-data file update operations.
+        {
+           "link" : "symbolic link value (target) string"
+           "remove" : ""     - flag present when removing a file (argument ignored.)
+           "hlink" : "hardlink value string (file being linked to.)"
+           "rename" : "name of file before rename."
+           "directory": ""  - flag presend for directory creation and remove events.
+        }
+  or:
+       nothing... If neither of these is present, then duplication
+       suppression will rely on supplied meta data, such as the modification
+       time, the size, and the publication Time to prevent loops.
+       It is strongly recommended that all data services provide identity
+       checksums. Failure to do so results in a data service than cannot
+       be reliably replicated.
+
+  both may be present in cases where file content is being updated, as
+  well as metadata.
+
+OPTIONAL:
+
+        for GeoJSON compatibility:
+
+        "type": "Feature",
+        "geometry": RFC 7946 (geoJSON) compatible geographic specification.
+
+        "size"          - the number of bytes being advertised.
+        "blocks"        - if the file being advertised is partitioned, then:
+        {
+            "method"    : "inplace" | "partitioned" , - is the file already in parts?
+            "size"      : "9999", - nominal number of bytes in each block.
+            "number"    : "9999", - which block is this.
+            "manifest"   - metadata for each block in the file.
+            {
+                0: {                 - size and checksum of each block in the file.
+                    "size": 9999,    - may not match blocksize (e.g. last block of file.)
+                    "identity": encoded checksum of block (same format as identity value)
+                },
+                .
+                .
+                .
+            }
+        }
+        "atime" : date string - last access time of a file (optional)
+        "mtime" : date string - last modification time of a file (optional)
+        "mode"  : mode string - permission bits (optional)
+        "rename"        - name to write file locally.
+        "retrievePath"       - relative retrieval path can be catenated to <base_url> to override relPath
+                          used for API cases.
+        "topic"         - copy of topic from AMQP header (in the envelope in protocol messages)
+        "source"        - the originating entity of the notification message.
+        "from_cluster"  - the originating cluster of a notification message.
+        "to_clusters"   - a destination specification.
+
+        "content"       - for smaller files, the content may be embedded.
+        {
+            "encoding" : "utf-8" | "base64" | "iso-8859-1" ,
+            "value"    " "encoded file content"
+        }
+        Note that the iso-8859-1 encoding is only an allowance for legacy data flows.
+        Should normally not be used.
+
+        "contentType" : "string" - MIME-type information referring to the data.
+
+        For "v03.report" topic notification messages the following addtional
+        headers will be present:
+
+        "report" { "code": 999  - HTTP style response code.
+                   "timeCompleted": "YYYYMMDDTHHMMSS.ss" - UTC date/timestamp.
+                   "message" :  - status report message documented in `Report Messages`_
+                 }
+
+        additional user defined name:value pairs are permitted.
+
+
+
+
NOTE:

The parts header has not yet been reviewed by others. We started on the discussion of size, +but there was no conclusion.

+
+
+
+
+
+

DESCRIPTION

+

Sources create notification messages in the sr_post format to announce file changes. Subscribers +read the post to decide whether a download of the content being announced is warranted. This +manual page completely describes the format of those notification messages. The notification messages are payloads +for an Advanced Message Queuing Protocol (AMQP) message bus, but file data transport +is separate, using more common protocols such as SFTP, HTTP, HTTPS, or FTP (or other?). +Files are transported as pure byte streams, no metadata beyond the file contents is +transported (permission bits, extended attributes, etc…). Permissions of files +on the destination system are upto the receiver to decide.

+

With this method, AMQP messages provide a ‘control plane’ for data transfers. While each notification message +is essentially point to point, data pumps can be transitively linked together to make arbitrary +networks. Each posting is consumed by the next hop in the chain. Each hop re-advertises +(creates a new post for) the data for later hops. The notification messages flow in the same direction as the +data. If consumers permit it, report messages also flow through the control path, +but in the opposite direction, allowing sources to know the entire disposition of their +files through a network.

+

The minimal layer over raw AMQP provides more complete file transfer functionality:

+
+
Source Filtering (use of TOPIC exchanges)

The notification messages make use of topic exchanges from AMQP, where topics are hierarchies +meant to represent subjects of interest to a consumer. A consumer may upload the +selection criteria to the broker so that only a small subset of postings +are forwarded to the client. When there are many users interested in only +small subsets of data, the savings in traffic are large.

+
+
Fingerprint Winnowing (use of the identity header)

Each product has an identity fingerprint and size intended to identify it uniquely, +referred to as a fingerprint. If two files have the same fingerprint, they +are considered equivalent. In cases where multiple sources of equivalent data are +available but downstream consumers would prefer to receive single notification messages +of files, intermediate processes may elect to publish notifications of the first +product with a given fingerprint, and ignore subsequent ones. +Propagating only the first occurrence of a datum received downstream, based on +its fingerprint, is termed: Fingerprint Winnowing.

+

Fingerprint Winnowing is the basis for a robust strategy for high availability: setting up +multiple sources for the same data, consumers accept notification messages from all of them, but only +forwarding the first one received downstream. In normal operation, one source may be faster +than the others, and so the other sources’ files are usually ‘winnowed’. When one source +disappears, the other sources’ data is automatically selected, as the fingerprints +are now fresh and used, until a faster source becomes available.

+

The advantage of this method for high availability is that no A/B decision is required. +The time to switchover is zero. Other strategies are subject to considerable delays +in making the decision to switchover, and pathologies one could summarize as flapping, +and/or deadlocks.

+

Fingerprint Winnowing also permits mesh-like, or any to any networks, where one simply +interconnects a node with others, and notification messages propagate. Their specific path through the +network is not defined, but each participant will download each new datum from the first +node that makes it available to them. Keeping the notification messages small and separate from data +is optimal for this usage.

+
+
Partitioning (use of the parts Header)

In any store and forward data pumping network that transports entire files limits the maximum +file size to the minimum available on any intervening node. To avoid defining a maximum +file size, a segmentation standard is specified, allowing intervening nodes to hold +only segments of the file, and forward them as they are received, rather than being +forced to hold the entire file.

+

Partitioning also permits multiple streams to transfer portions of the file in parallel. +Multiple streams can provide an effective optimization over long links.

+
+
+
+
+

TOPIC

+

In topic based AMQP exchanges, every notification message has a topic header. AMQP defines the ‘.’ character +as a hierarchical separator (like ‘' in a windows path name, or ‘/’ on linux) there is also a +pair of wildcards defined by the standard: ‘*’ matches a single topic, ‘#’ matches the rest of +the topic string. To allow for changes in the notification message body in the future, topic trees begin with +the version number of the protocol.

+

AMQP allows server side topic filtering using wildcards. Subscribers specify topics of +interest (which correspond to directories on the server), allowing them to pare down the +number of notifications sent from server to client.

+

The root of the topic tree is the version specifier: “v03”. Next comes the notification message type specifier. +These two fields define the protocol that is in use for the rest of the notification message. +The notification message type for notification messages is “post”. After the fixed topic prefix, +the remaining sub-topics are the path elements of the file on the web server. +For example, if a file is placed on http://www.example.com/a/b/c/d/foo.txt, +then the complete topic of the notification message will be: v03.a.b.c.d +AMQP fields are limited to 255 characters, and the characters in the field are utf8 +encoded, so actual length limit may be less than that.

+

note:

+
Sarracenia relies on brokers to interpret the topic header. Brokers interpret protocol
+specific headers *AMQP), and will not efficiently decode the payload to extract headers.
+Therefore the topic header is stored in an AMQP header, rather than the payload to permit
+server-side filtering. To avoid sending the same information twice, this header is
+omitted from the JSON payload.
+
+Many client-side implementation will, once the notification message is loaded, set the *topic* header
+in the in-memory structure, so it would be very unwise to to set the *topic* header
+in an application even though it isn't visible in the on-wire payload.
+
+
+
+

Mapping to MQTT

+

One goal of v03 format is to have a payload format that works with more than just AMQP. +Message Queing Telemetry Transport (MQTT v3.11) is an iso standard ( https://www.iso.org/standard/69466.html +protocol that can easily support the same pub/sub messaging pattern, but a few details +differ, so a mapping is needed.

+

Firstly, the topic separate in MQTT is a forward slash (/), instead of the period (.) used in AMQP.

+

Second, with AMQP, one can establish separate topic hierarchies using topic-based exchanges. +MQTT has no similar concept, there is simply one hierarchy, so when mapping, place the exchange +name at the root of the topic hierarchy to achieve the same effect:

+
AMQP:   Exchange: <exchange name>
+           topic: v03.<directory>...
+
+MQTT:   topic: <exchange name>/v03/<directory>...
+
+
+
+
+
+

THE FIXED HEADERS

+

The notification message is a single JSON encoded array, with a mandatory set of fields, while allowing +for use of arbitrary other fields. Mandatory fields must be present in every notification message, and

+
+
    +
  • “pubTime” : “<date stamp>” : the publication date the posting was emitted. Format: YYYYMMDDTHHMMSS. <decimalseconds>

  • +
+

Note: The datestamp is always in the UTC timezone.

+
    +
  • “baseUrl” : “<base_url>” – the base URL used to retrieve the data.

  • +
  • “relPath” : “<relativepath>” – the variable part of the URL, usually appended to baseUrl.

  • +
+
+

The URL consumers will use to download the data. Example of a complete URL:

+
sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif
+
+
+

Additional fields:

+
+

from_cluster=<cluster_name>

+
+

The from_cluster header defines the name of the source cluster where the +data was introduced into the network. It is used to return the logs back +to the cluster whenever its products are used.

+
+
+ +
+

size and blocks

+
+
"size":<sz> ,
+
+"blocks" :
+{
+       "method": "inplace" or "partitioned",
+       "size": <bsz>,
+       "count": <blktot>,
+       "remainder": <brem>,
+       "number": <bno>
+}
+
+
+
+

header indicating the method and parameters for partitioning applied for the file. +Partitioning is used to send a single file as a collection of segments, rather than as +a single entity. Partitioning is used to accelerate transfers of large data sets by using +multiple streams, and/or to reduce storage use for extremely large files.

+

When transferring partitioned files, each partition is advertised and potentially transported +independently across a data pumping network.

+
+

<method>

+

Indicates what partitioning method, if any, was used in transmission.

+ + + + + + + + + + + + + + + +

Method

Description

p - partitioned

File is partitioned, individual part files are created.

i - inplace

File is partitioned, but blocks are read from a single file, +rather than parts.

1 - <sizeonly>

File is in a single part (no partitioning). +in v03, only size header will be present. blocks is omitted

+
    +
  • analogous to rsync options: –inplace, –partial,

  • +
+

<blocksize in bytes>: bsz

+
+

The number of bytes in a block. When using method 1, the size of the block is the size of the file. +Remaining fields only useful for partitioned files.

+
+

<blocks in total>: blktot +the integer total number of blocks in the file (last block may be partial)

+

<remainder>: brem +normally 0, on the last block, remaining bytes in the file +to transfer.

+
+
+
– if (fzb=1 and brem=0)

then bsz=fsz in bytes in bytes. +– entire files replaced. +– this is the same as rsync’s –whole-file mode.

+
+
+
+

<block#>: bno +0 origin, the block number covered by this posting.

+
+
+
+

rename=<relpath>

+
+

The relative path from the current directory in which to place the file.

+
+
+
+

fileOp { ‘rename’:<path> … }

+
+

when a file is renamed at the source, to send it to subscribers, two notification messages +result: one notification message is announced with the new name as the base_url, +and the oldname header set to the previous file name. +Another notification message is sent with the old name as the src path, and the newname +as a header. This ensures that accept/reject clauses are correctly +interpreted, as a rename may result in a download if the former name +matches a reject clause, or a file removal if the new name +matches a reject clause.

+

Hard links are also handled as an ordinary post of the file with a hlink +header set.

+

Note that directories and links can be renamed not just regular files. The fileOp field +will have ‘rename’ and ‘link’ or ‘directory’ elements in that case.

+
+
+
+

identity

+

The identity field gives a checksum useful for identifying the contents +of a file:

+
"identity" : { "method" : <method>, "value": <value> }
+
+
+

The identity field is a signature computed to allow receivers to determine +if they have already downloaded the product from elsewhere.

+
+
+

<method> - string field indicating the checksum method used.

+
+ + + + + + + + + + + + + + + + + + + + + + + + +

Method

Description

random

No checksums (unconditional copy.) Skips reading file (faster)

arbitrary

arbitrary, application defined value which cannot be calculated

md5

Checksum the entire data (MD-5 as per IETF RFC 1321)

sha512

Checksum the entire data (SHA512 as per IETF RFC 6234)

cod

Checksum on download, with algorithm as argument +Example: cod,sha512 means download, applying SHA512 checksum, and +advertise with that calculated checksum when propagating further.

<name>

Checksum with some other algorithm, named <name> +<name> should be registered in the data pumping network. +Registered means that all downstream subscribers can obtain the +algorithm to validate the checksum.

+
+
+
<value> The value is computed by applying the given method to the partition being transferred.

for algorithms for which no value makes sense, a random integer is generated to support +checksum based load balancing.

+
+
+
+
+
+

Report Messages

+

Some clients may return telemetry to the origin of downloaded data for troubleshooting +and statistical purposes. Such notification messages, have the v03.report topic, and have a report +header which is a JSON object with four fields:

+
+

{ “elapsedTime”: <report_time>, “resultCode”: <report_code>, “host”: <report_host>, “user”: <report_user>* }

+
    +
  • <report_code> result codes describe in the next session

  • +
  • <report_time> time the report was generated.

  • +
  • <report_host> hostname from which the retrieval was initiated.

  • +
  • <report_user> broker username from which the retrieval was initiated.

  • +
+
+

Report messages should never include the content header (no file embedding in reports.)

+
+

Report_Code

+

The report code is a three digit status code, adopted from the HTTP protocol (w3.org/IETF RFC 2616) +encoded as text. As per the RFC, any code returned should be interpreted as follows:

+
+
    +
  • 2xx indicates successful completion,

  • +
  • 3xx indicates further action is required to complete the operation.

  • +
  • 4xx indicates a permanent error on the client prevented a successful operation.

  • +
  • 5xx indicates a problem on the server prevented successful operation.

  • +
+
+
+

Note

+

FIXME: need to validate whether our use of error codes co-incides with the general intent +expressed above… does a 3xx mean we expect the client to do something? does 5xx mean +that the failure was on the broker/server side?

+
+

The specific error codes returned, and their meanings are implementation-dependent. +For the sarracenia implementation, the following codes are defined:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Code

Corresponding text and meaning for sarracenia implementation

201

Download successful. (variations: Downloaded, Inserted, Published, Copied, or Linked)

203

Non-Authoritative Information: transformed during download.

205

Reset Content: truncated. File is shorter than originally expected (changed length +during transfer) This only arises during multi-part transfers.

205

Reset Content: checksum recalculated on receipt.

304

Not modified (Checksum validated, unchanged, so no download resulted.)

307

Insertion deferred (writing to temporary part file for the moment.)

417

Expectation Failed: invalid message (corrupt headers)

496

failure: During send, other protocol failure.

497

failure: During send, other protocol failure.

499

Failure: Not Copied. SFTP/FTP/HTTP download problem

499

Failure: Not Copied. SFTP/FTP/HTTP download problem

503

Service unavailable. delete (File removal not currently supported.)

503

Unable to process: Service unavailable

503

Unsupported transport protocol specified in posting.

xxx

Message and file validation status codes are script dependent

+
+
+

Other Report Fields

+

<report_message> a string.

+
+
+
+

Optional Headers

+

for the file mirroring use case, additional headers will be present:

+
+

atime,mtime,mode

+
+

man 2 stat - the linux/unix standard file metadata: +access time, modification time, and permission (mode bits) +the times are in the same date format as the pubTime field. +the permission string is four characters intended to be interpreted as +traditional octal linux/unix permissions.

+
+

Headers which are unknown to a given broker MUST be forwarded without modification.

+

Sarracenia provides a mechanism for users to include arbitrary other headers in +notification messages, to amplify metadata for more detailed decision making about downloading data. +For example:

+
"PRINTER" : "name_of_corporate_printer",
+
+"GeograpicBoundingBox" :
+ {
+         "top_left" : { "lat": 40.73, "lon": -74.1 } ,
+         "bottom_right": { "lat": -40.01, "lon": -71.12 }
+ }
+
+
+

would permit the client to apply more elaborate and precise client side filtering, +and/or processing. Intervening implementation may know nothing about the header, +but they should not be stripped, as some consumers may understand and process them.

+
+
+
+

EXAMPLE

+
AMQP TOPIC: v03.NRDPS.GIF
+MQTT TOPIC: exchange/v03/NRDPS/GIF/
+Body: { "pubTime": "201506011357.345", "baseUrl": "sftp://afsiext@cmcdataserver", "relPath": "/data/NRPDS/outputs/NRDPS_HiRes_000.gif",
+   "rename": "NRDPS/GIF/", "parts":"p,457,1,0,0", "identity" : { "method":"md5", "value":"<md5sum-base64>" }, "source": "ec_cmc" }
+
+       - v03 - version of protocol
+       - post - indicates the type of notification message
+       - version and type together determine format of following topics and the notification message body.
+
+       - blocksize is 457  (== file size)
+       - block count is 1
+       - remainder is 0.
+       - block number is 0.
+       - d - checksum was calculated on the body of the file.
+       - complete source URL specified (does not end in '/')
+       - relative path specified for
+
+       pull from:
+               sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif
+
+       complete relative download path:
+               NRDPS/GIF/NRDPS_HiRes_000.gif
+
+               -- takes file name from base_url.
+               -- may be modified by validation process.
+
+
+
+
+

Another example

+

The post resulting from the following sr3 watch command, noticing creation of the file ‘foo’:

+
sr3_post --sleep 10 --pbu sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo --pb amqp://broker.com
+
+
+

Here, sr_watch checks if the file /data/shared/products/foo is modified. +When it happens, sr_watch reads the file /data/shared/products/foo and calculates its checksum. +It then builds a notification message, logs into broker.com as user ‘guest’ (default credentials) +and sends the post to defaults vhost ‘/’ and exchange ‘sx_guest’ (default exchange).

+

A subscriber can download the file /data/shared/products/foo by logging in as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials.

+

The output of the command is as follows

+
AMQP Topic: v03.20150813.data.shared.products
+MQTT Topic: <exchange>/v03/20150813/data/shared/products
+Body: { "pubTime":"20150813T161959.854", "baseUrl":"sftp://stanley@mysftpserver.com/",
+        "relPath": "/data/shared/products/foo", "parts":"1,256,1,0,0",
+        "sum": "d,25d231ec0ae3c569ba27ab7a74dd72ce", "source":"guest" }
+
+
+

Posts are published on AMQP topic exchanges, meaning every notification message has a topic header. +The body consists of a time 20150813T161959.854, followed by the two parts of the +retrieval URL. The headers follow with first the parts, a size in bytes 256, +the number of block of that size 1, the remaining bytes 0, the +current block 0, a flag d meaning the md5 checksum is +performed on the data, and the checksum 25d231ec0ae3c569ba27ab7a74dd72ce.

+
+

Optimization Possibilities

+

optimization goal is for readabilty and ease of implementation, much more +than efficiency or performance. There are many optimizations to reduce +overheads of various sorts, all of which will increase implementation +complexity. examples: gzip the payload would save perhaps 50% size, +also grouping fixed headers together, (‘body’ header could contain +all fixed fields: “pubtime, baseurl, relpath, sum, parts”, and another +field ‘meta’ could contain: atime, mtime, mode so there would be fewer +named fields and save perhaps 40 bytes of overhead per notice. But +all the changes increase complexity, make notification messages more involved to parse.

+
+
+
+

Standards

+
+
    +
  • Sarracenia relies on AMQP pre 1.0 +as the 1.0 standard eliminated concepts: broker, exchange, queue, and +binding. The 1.0 feature set is below the minimum needed to support +Sarracenia’s pub-sub architecture.

  • +
  • MQTT refers to MQTT v5.0 +and MQTT v3.1.1, +MQTT v5 has important extension: shared subscriptions (heavily used in Sarracenia.) +so v5 is highly recommended. v3.1 support is only for legacy support reasons.

  • +
  • JSON is defined by IETF RFC 7159. +JSON standard includes mandatory use of UNICODE character set (ISO 10646) +JSON default character set is UTF-8, but allows multiple character +encodings (UTF-8, UTF-16, UTF-32), but also prohibits presence of +byte order markings (BOM.)

  • +
  • the same as Sarracenia v02, UTF-8 is mandatory. Sarracenia restricts JSON format +by requiring of UTF-8 encoding, (IETF RFC 3629) which does not need/use BOM. +No other encoding is permitted.

  • +
  • URL encoding, as per IETF RFC 1738, is used to escape unsafe characters +where appropriate.

  • +
+
+
+
+

SEE ALSO

+

sr3(1) - Sarracenia main command line interface.

+

sr3_post(1) - post file notification messages (python implementation.)

+

sr3_cpost(1) - post file announcemensts (C implementation.)

+

sr3_cpump(1) - C implementation of the shovel component. (copy notification messages)

+

Formats:

+

sr3_credentials(7) - Convert logfile lines to .save Format for reload/resend.

+

sr3_options(7) - the configuration options

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia: a real-time pub/sub data sharing management toolkit

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/1_CLI_introduction.html b/Tutorials/1_CLI_introduction.html new file mode 100644 index 000000000..2b7c80860 --- /dev/null +++ b/Tutorials/1_CLI_introduction.html @@ -0,0 +1,649 @@ + + + + + + + Downloading Using the Command Line — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Downloading Using the Command Line

+

This jupyter notebook introduces Sarracenia version 3 usage from the command line (mostly on Linux, but should be similar on Windows and Mac also, main difference being different conventions for where preferences and logs are stored.) This is probably the easiest way to work with Sarracenia. You configure a flow to download files into a directory, and you can read the directory to process the files there.

+
+
[1]:
+
+
+
+import sarracenia
+!mkdir -p ~/.config/sr3/subscribe
+!mkdir -p ~/.cache/sr3/log
+
+
+
+
+

Prerequisites

+

The above is just a way to get jupyter notebooks to install metpx-sr3 on a server. Creating some directories in case people use API access without running things through the API. The basic pre-requisite is to have metpx-sr3 installed somehow, either as a .deb package, or using pip (or pip3) available to the environment used by jupyter.

+

The rest of this notebook assumes metpx-sr3 is installed.

+
+
+

SR3

+

The command line interface is called sr3 (short for Sarracenia version 3). One defines flows to run using configuration files in a simple format: keyword value format. There are example configurations to get you started:

+
+
[2]:
+
+
+
+!sr3 list examples
+
+
+
+
+
+
+
+
+Sample Configurations: (from: /net/local/home/shakerm/sr3/sarracenia/examples )
+cpump/cno_trouble_f00.inc        flow/amserver.conf
+flow/opg.conf                    flow/poll.inc
+flow/post.inc                    flow/report.inc
+flow/sarra.inc                   flow/sender.inc
+flow/shovel.inc                  flow/subscribe.inc
+flow/watch.inc                   flow/winnow.inc
+poll/airnow.conf                 poll/aws-nexrad.conf
+poll/copernicus_odata.conf       poll/mail.conf
+poll/nasa-mls-nrt.conf           poll/nasa_cmr_opendap.conf
+poll/nasa_cmr_other.conf         poll/nasa_cmr_podaac.conf
+poll/noaa.conf                   poll/soapshc.conf
+poll/usgs.conf                   post/WMO_mesh_post.conf
+sarra/wmo_mesh.conf              sender/am_send.conf
+sender/ec2collab.conf            sender/pitcher_push.conf
+shovel/no_trouble_f00.inc        subscribe/aws-nexrad.conf
+subscribe/dd_2mqtt.conf          subscribe/dd_all.conf
+subscribe/dd_amis.conf           subscribe/dd_aqhi.conf
+subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf
+subscribe/dd_cmml.conf           subscribe/dd_gdps.conf
+subscribe/dd_radar.conf          subscribe/dd_rdps.conf
+subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+subscribe/ddc_normal.conf        subscribe/download_all_nasa_earthdata.conf
+subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf
+subscribe/get_copernicus.conf    subscribe/hpfxWIS2DownloadAll.conf
+subscribe/hpfx_amis.conf         subscribe/hpfx_citypage.conf
+subscribe/local_sub.conf         subscribe/ping.conf
+subscribe/pitcher_pull.conf      subscribe/sci2ec.conf
+subscribe/subnoaa.conf           subscribe/subsoapshc.conf
+subscribe/subusgs.conf           watch/master.conf
+watch/pitcher_client.conf        watch/pitcher_server.conf
+watch/sci2ec.conf
+
+
+

There are different kinds for flows: the examples are classified by flow type (poll, post, sarra, sender, shovel, etc.) A subscribe is used by clients to download from a data pump. Let’s pick one of those.

+
+
[3]:
+
+
+
+!sr3 add subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+add: 2024-03-06 23:48:56,706 2118966 [INFO] sarracenia.sr add copying: /net/local/home/shakerm/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /net/local/home/shakerm/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

The files that are active for you are placed in ~/.config/sr3/<flow_type>/config_name. You can browse there and modify them with an editor if you like. You can also do that with sr3 edit subscribe/hpfx_amis.conf.

+
# this is a feed of wmo bulletin (a set called AMIS in the old times)
+
+broker amqps://hpfx.collab.science.gc.ca/
+exchange xpublic
+
+# instances: number of downloading processes to run at once.  Defaults to 1. Not enough for this case
+instances 5
+
+# expire, in operational use, should be longer than longest expected interruption
+expire 10m
+
+topicPrefix v02.post
+subtopic *.WXO-DD.bulletins.alphanumeric.#
+mirror false
+directory /tmp/hpfx_amis/
+
+
+

Add the messageCountMax, so it doesn’t run forever:

+
+
[4]:
+
+
+
+!mkdir /tmp/hpfx_amis
+!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

The root directory where files are to be placed needs to exist before you start. The above commands are to configure on a Linux machine, you might need something else on a mac or windows.

+

You can then run a flow interactively with the foreground action, and it will end quickly, like so:

+
+
[5]:
+
+
+
+!sr3 foreground subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2024-03-06 23:49:06,570 2118978 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+.2024-03-06 23:49:06,841 [INFO] 2118981 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+2024-03-06 23:49:06,846 [INFO] 2118981 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+2024-03-06 23:49:06,846 [INFO] 2118981 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_post', 'on_housekeeping', 'after_work', 'after_accept'},  logMessageDump: False
+2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flow run pid: 2118981 subscribe/hpfx_amis instance: 0
+2024-03-06 23:49:06,906 [INFO] 2118981 sarracenia.moth.amqp _queueDeclare queue declared q_anonymous_subscribe.hpfx_amis.16789186.78043112 (as: amqps://anonymous@hpfx.collab.science.gc.ca/), (messages waiting: 0)
+2024-03-06 23:49:06,906 [INFO] 2118981 sarracenia.moth.amqp getSetup binding q_anonymous_subscribe.hpfx_amis.16789186.78043112 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2024-03-06 23:49:06,918 [INFO] 2118981 sarracenia.flow run now active on vip ['AnyAddressIsFine']
+2024-03-06 23:49:15,332 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 4.84 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/WV/MMMX/23/WVMX31_MMMX_062348___03321
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 7.61 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___32108
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SX/KWAL/23/SXCN40_KWAL_062348___23784
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___45099
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___35424
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___47804
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRME20_KWAL_062348___48533
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.19 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRME20_KWAL_062348___768
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 1.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___19124
+2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 1.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SO/KWAL/23/SOLC10_KWAL_062348___60057
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/WVMX31_MMMX_062348___03321
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___32108
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_062348___23784
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___45099
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___35424
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___47804
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRME20_KWAL_062348___48533
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRME20_KWAL_062348___768
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___19124
+2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOLC10_KWAL_062348___60057
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow run starting last pass (without gather) through loop for cleanup.
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow run on_housekeeping pid: 2118981 subscribe/hpfx_amis instance: 0
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flowcb.gather.message on_housekeeping messages: good: 10 bad: 0 bytes: 1.4 KiB average: 138 Bytes
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flowcb.retry on_housekeeping on_housekeeping
+2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping
+2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping No retry in list
+2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.002104
+2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping
+2024-03-06 23:49:15,460 [INFO] 2118981 sarracenia.diskqueue on_housekeeping No retry in list
+2024-03-06 23:49:15,460 [INFO] 2118981 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.001996
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.24 system=0.03
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 79.3 MiB, accumulating count (10 or 10/100 so far) before self-setting threshold
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats version: 3.00.52rc2, started: 8 seconds ago, last_housekeeping:  8.6 seconds ago
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats messages received: 10, accepted: 10, rejected: 0   rate accepted: 100.0% or 1.2 m/s
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats files transferred: 10 bytes: 2.0 KiB rate: 243 Bytes/sec
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats lag: average: 2.97, maximum: 7.61
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log on_housekeeping housekeeping
+2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flow run clean stop from run loop
+2024-03-06 23:49:15,462 [INFO] 2118981 sarracenia.flowcb.gather.message on_stop closing
+2024-03-06 23:49:15,462 [INFO] 2118981 sarracenia.flow close flow/close completed cleanly pid: 2118981 subscribe/hpfx_amis instance: 0
+
+
+
+

As you can see, it downloaded five files to /tmp/amis. The foreground action is intended to help with debugging, rather than real operations.

+
+
[6]:
+
+
+
+!sr3 status
+
+
+
+
+
+
+
+
+2024-03-06 23:49:30,243 2118998 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+status:
+Component/Config                         Processes   Connection        Lag                              Rates
+                                         State   Run Retry  msg data   Queued  LagMax LagAvg  Last  %rej     pubsub messages   RxData     TxData
+                                         -----   --- -----  --- ----   ------  ------ ------  ----  ----     ------ --------   ------     ------
+subscribe/hpfx_amis                      stop    0/0          -          -         -     -     -          -        -
+      Total Running Configs:   0 ( Processes: 0 missing: 0 stray: 0 )
+                     Memory: uss:0 Bytes rss:0 Bytes vms:0 Bytes
+                   CPU Time: User:0.00s System:0.00s
+           Pub/Sub Received: 0 msgs/s (0 Bytes/s), Sent:  0 msgs/s (0 Bytes/s) Queued: 0 Retry: 0, Mean lag: 0.00s
+              Data Received: 0 Files/s (0 Bytes/s), Sent: 0 Files/s (0 Bytes/s)
+
+
+

Above, you can see there is 1 configuration in your list. You can have hundreds. The columns on the right refer to how many instances you have for each configuration. In the example above, instances is set to 5, so one would expect to see 5 running instances when it would be running. You can start specifc configurations (in this case a subscribe config) with sr3 start subscribe/<config>, or start all active configs from all components (sarra, subscribe, watch, winnow, etc.) with sr3 start

+
+
[7]:
+
+
+
+!sr3 log subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2024-03-06 23:45:56,401 2118802 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+tail: cannot open '/net/local/home/shakerm/.cache/sr3/log/subscribe_hpfx_amis_01.log' for reading: No such file or directory
+tail: no files remaining
+2024-03-06 23:45:56,406 2118802 [CRITICAL] root run_command subprocess.run failed err=Command '['tail', '-f', '/net/local/home/shakerm/.cache/sr3/log/subscribe_hpfx_amis_01.log']' returned non-zero exit status 1.
+
+
+
+

When running in the background, output needs to go a log file. Since we have only ran this configuration file in the foreground, asking to see the log prints an error about the log being missing. This tells you that the logs are in the ~/.cache/sr3/log directory. Logs can be monitored in real-time with traditional tools such as tail -f or grep.

+

sr3 stop does what you expect.

+

Processes can crash. In the sr3 status output above, if the number of processes in the Run column is less than in the Exp (for Expected) one, then it means that some instances have crashed. You can repair it (just start the missing instances) with:

+

sr3 sanity – start missing instances, also kill strays if any found.

+

So that’s it, an introduction to running configurations in Sarracenia from the command line.

+
+
+

Conclusion

+

If all you want to do is obtain data from a data pump in real-time, the easiest way to go is using the command line interface to control some processes that run all the time so that they dump files in a certain directory.

+

It isn’t very efficient though. When dealing with a large number of files and aiming for high-speed processing, it’s more efficient to have your own application receive notifications about file arrivals rather than scanning a directory. This approach reduces CPU and I/O overhead while improving processing speed.

+

The easiest way to do that is to add some callbacks to your flows. We’ll cover that next.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/1_CLI_introduction.ipynb b/Tutorials/1_CLI_introduction.ipynb new file mode 100644 index 000000000..22db81f07 --- /dev/null +++ b/Tutorials/1_CLI_introduction.ipynb @@ -0,0 +1,363 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "chubby-tenant", + "metadata": {}, + "source": [ + "# Downloading Using the Command Line\n", + "\n", + "This [jupyter notebook](https://jupyter.org) introduces [Sarracenia version 3](https://metpx.github.io/sarracenia) usage from the command line (mostly on Linux, but should be similar on Windows and Mac also, main difference being different conventions for where preferences and logs are stored.) This is probably the easiest way to work with Sarracenia. You configure a flow to download files into a directory, and you can read the directory to process the files there.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "neither-shannon", + "metadata": {}, + "outputs": [], + "source": [ + "import sarracenia\n", + "!mkdir -p ~/.config/sr3/subscribe\n", + "!mkdir -p ~/.cache/sr3/log" + ] + }, + { + "cell_type": "markdown", + "id": "varying-armor", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "\n", + "## Prerequisites\n", + "\n", + "The above is just a way to get jupyter notebooks to install metpx-sr3 on a server.\n", + "Creating some directories in case people use API access without running things through the API. The basic pre-requisite is to have metpx-sr3 installed somehow, either as a .deb package, or using pip (or pip3) available to the environment used by jupyter.\n", + "\n", + "The rest of this notebook assumes [metpx-sr3](https://metpx.github.io/sarracenia) is installed." + ] + }, + { + "cell_type": "markdown", + "id": "absolute-integral", + "metadata": {}, + "source": [ + "## SR3\n", + "\n", + "The command line interface is called [sr3](../Reference/sr3.1.rst) (short for Sarracenia version 3). One defines\n", + "flows to run using configuration files in a simple format: _keyword_ _value_ format.\n", + "There are example configurations to get you started:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "drawn-opposition", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample Configurations: (from: /net/local/home/shakerm/sr3/sarracenia/examples )\n", + "cpump/cno_trouble_f00.inc flow/amserver.conf \n", + "flow/opg.conf flow/poll.inc \n", + "flow/post.inc flow/report.inc \n", + "flow/sarra.inc flow/sender.inc \n", + "flow/shovel.inc flow/subscribe.inc \n", + "flow/watch.inc flow/winnow.inc \n", + "poll/airnow.conf poll/aws-nexrad.conf \n", + "poll/copernicus_odata.conf poll/mail.conf \n", + "poll/nasa-mls-nrt.conf poll/nasa_cmr_opendap.conf \n", + "poll/nasa_cmr_other.conf poll/nasa_cmr_podaac.conf \n", + "poll/noaa.conf poll/soapshc.conf \n", + "poll/usgs.conf post/WMO_mesh_post.conf \n", + "sarra/wmo_mesh.conf sender/am_send.conf \n", + "sender/ec2collab.conf sender/pitcher_push.conf \n", + "shovel/no_trouble_f00.inc subscribe/aws-nexrad.conf \n", + "subscribe/dd_2mqtt.conf subscribe/dd_all.conf \n", + "subscribe/dd_amis.conf subscribe/dd_aqhi.conf \n", + "subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf \n", + "subscribe/dd_cmml.conf subscribe/dd_gdps.conf \n", + "subscribe/dd_radar.conf subscribe/dd_rdps.conf \n", + "subscribe/dd_swob.conf subscribe/ddc_cap-xml.conf \n", + "subscribe/ddc_normal.conf subscribe/download_all_nasa_earthdata.conf \n", + "subscribe/downloademail.conf subscribe/ec_ninjo-a.conf \n", + "subscribe/get_copernicus.conf subscribe/hpfxWIS2DownloadAll.conf \n", + "subscribe/hpfx_amis.conf subscribe/hpfx_citypage.conf \n", + "subscribe/local_sub.conf subscribe/ping.conf \n", + "subscribe/pitcher_pull.conf subscribe/sci2ec.conf \n", + "subscribe/subnoaa.conf subscribe/subsoapshc.conf \n", + "subscribe/subusgs.conf watch/master.conf \n", + "watch/pitcher_client.conf watch/pitcher_server.conf \n", + "watch/sci2ec.conf \n" + ] + } + ], + "source": [ + "!sr3 list examples" + ] + }, + { + "cell_type": "markdown", + "id": "affecting-marking", + "metadata": {}, + "source": [ + "There are different kinds for flows: the examples are classified by flow type (poll, post, sarra, sender, shovel, etc.)\n", + "A _subscribe_ is used by clients to download from a data pump. Let's pick one of those." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "egyptian-suicide", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "add: 2024-03-06 23:48:56,706 2118966 [INFO] sarracenia.sr add copying: /net/local/home/shakerm/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /net/local/home/shakerm/.config/sr3/subscribe/hpfx_amis.conf \n", + "\n" + ] + } + ], + "source": [ + "!sr3 add subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "d1179254-ac4c-49f8-b1a8-30f96d09290b", + "metadata": {}, + "source": [ + "The files that are active for you are placed in ~/.config/sr3/\\/config_name. You can browse there \n", + "and modify them with an editor if you like. You can also do that with _sr3 edit subscribe/hpfx_amis.conf_.\n", + "\n", + " # this is a feed of wmo bulletin (a set called AMIS in the old times)\n", + "\n", + " broker amqps://hpfx.collab.science.gc.ca/\n", + " exchange xpublic\n", + "\n", + " # instances: number of downloading processes to run at once. Defaults to 1. Not enough for this case\n", + " instances 5\n", + " \n", + " # expire, in operational use, should be longer than longest expected interruption\n", + " expire 10m\n", + "\n", + " topicPrefix v02.post\n", + " subtopic *.WXO-DD.bulletins.alphanumeric.#\n", + " mirror false\n", + " directory /tmp/hpfx_amis/\n", + "\n", + "Add the messageCountMax, so it doesn't run forever:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "primary-score", + "metadata": {}, + "outputs": [], + "source": [ + "!mkdir /tmp/hpfx_amis\n", + "!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "ancient-scholarship", + "metadata": {}, + "source": [ + "The root directory where files are to be placed needs to exist before you start.\n", + "The above commands are to configure on a Linux machine, you might need something else on a mac or windows.\n", + "\n", + "You can then run a flow interactively with the _foreground_ action, and it will end quickly, like so:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "nominated-nerve", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-03-06 23:49:06,570 2118978 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + ".2024-03-06 23:49:06,841 [INFO] 2118981 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "2024-03-06 23:49:06,846 [INFO] 2118981 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "2024-03-06 23:49:06,846 [INFO] 2118981 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_post', 'on_housekeeping', 'after_work', 'after_accept'}, logMessageDump: False\n", + "2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2024-03-06 23:49:06,855 [INFO] 2118981 sarracenia.flow run pid: 2118981 subscribe/hpfx_amis instance: 0\n", + "2024-03-06 23:49:06,906 [INFO] 2118981 sarracenia.moth.amqp _queueDeclare queue declared q_anonymous_subscribe.hpfx_amis.16789186.78043112 (as: amqps://anonymous@hpfx.collab.science.gc.ca/), (messages waiting: 0)\n", + "2024-03-06 23:49:06,906 [INFO] 2118981 sarracenia.moth.amqp getSetup binding q_anonymous_subscribe.hpfx_amis.16789186.78043112 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)\n", + "2024-03-06 23:49:06,918 [INFO] 2118981 sarracenia.flow run now active on vip ['AnyAddressIsFine']\n", + "2024-03-06 23:49:15,332 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 4.84 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/WV/MMMX/23/WVMX31_MMMX_062348___03321\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 7.61 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___32108\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SX/KWAL/23/SXCN40_KWAL_062348___23784\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___45099\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___35424\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___47804\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRME20_KWAL_062348___48533\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 2.19 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRME20_KWAL_062348___768\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 1.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SR/KWAL/23/SRCN40_KWAL_062348___19124\n", + "2024-03-06 23:49:15,333 [INFO] 2118981 sarracenia.flowcb.log after_accept accepted: (lag: 1.43 ) https://hpfx.collab.science.gc.ca /20240306/WXO-DD/bulletins/alphanumeric/20240306/SO/KWAL/23/SOLC10_KWAL_062348___60057\n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/WVMX31_MMMX_062348___03321 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___32108 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_062348___23784 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___45099 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___35424 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___47804 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRME20_KWAL_062348___48533 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRME20_KWAL_062348___768 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_062348___19124 \n", + "2024-03-06 23:49:15,455 [INFO] 2118981 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOLC10_KWAL_062348___60057 \n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow run starting last pass (without gather) through loop for cleanup.\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flow run on_housekeeping pid: 2118981 subscribe/hpfx_amis instance: 0\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flowcb.gather.message on_housekeeping messages: good: 10 bad: 0 bytes: 1.4 KiB average: 138 Bytes\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.flowcb.retry on_housekeeping on_housekeeping\n", + "2024-03-06 23:49:15,456 [INFO] 2118981 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping\n", + "2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.002104\n", + "2024-03-06 23:49:15,458 [INFO] 2118981 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping\n", + "2024-03-06 23:49:15,460 [INFO] 2118981 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-03-06 23:49:15,460 [INFO] 2118981 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.001996\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.24 system=0.03\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 79.3 MiB, accumulating count (10 or 10/100 so far) before self-setting threshold\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats version: 3.00.52rc2, started: 8 seconds ago, last_housekeeping: 8.6 seconds ago \n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats messages received: 10, accepted: 10, rejected: 0 rate accepted: 100.0% or 1.2 m/s\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats files transferred: 10 bytes: 2.0 KiB rate: 243 Bytes/sec\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log stats lag: average: 2.97, maximum: 7.61 \n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flowcb.log on_housekeeping housekeeping\n", + "2024-03-06 23:49:15,461 [INFO] 2118981 sarracenia.flow run clean stop from run loop\n", + "2024-03-06 23:49:15,462 [INFO] 2118981 sarracenia.flowcb.gather.message on_stop closing\n", + "2024-03-06 23:49:15,462 [INFO] 2118981 sarracenia.flow close flow/close completed cleanly pid: 2118981 subscribe/hpfx_amis instance: 0\n", + "\n" + ] + } + ], + "source": [ + "!sr3 foreground subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "foreign-european", + "metadata": {}, + "source": [ + "As you can see, it downloaded five files to /tmp/amis.\n", + "The _foreground_ action is intended to help with debugging, rather than real operations." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "split-writing", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-03-06 23:49:30,243 2118998 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "status: \n", + "Component/Config Processes Connection Lag Rates \n", + " State Run Retry msg data Queued LagMax LagAvg Last %rej pubsub messages RxData TxData \n", + " ----- --- ----- --- ---- ------ ------ ------ ---- ---- ------ -------- ------ ------ \n", + "subscribe/hpfx_amis stop 0/0 - - - - - - -\n", + " Total Running Configs: 0 ( Processes: 0 missing: 0 stray: 0 )\n", + " Memory: uss:0 Bytes rss:0 Bytes vms:0 Bytes \n", + " CPU Time: User:0.00s System:0.00s \n", + "\t Pub/Sub Received: 0 msgs/s (0 Bytes/s), Sent: 0 msgs/s (0 Bytes/s) Queued: 0 Retry: 0, Mean lag: 0.00s\n", + "\t Data Received: 0 Files/s (0 Bytes/s), Sent: 0 Files/s (0 Bytes/s) \n" + ] + } + ], + "source": [ + "!sr3 status" + ] + }, + { + "cell_type": "markdown", + "id": "rocky-unemployment", + "metadata": {}, + "source": [ + "Above, you can see there is 1 configuration in your list. You can have hundreds. The columns on the right refer to how many instances you have for each configuration. In the example above, _instances_ is set to 5, so one would expect to see 5 running instances when it would be running. You can start specifc configurations (in this case a subscribe config) with _sr3 start subscribe/\\_, or start all active configs from all components (sarra, subscribe, watch, winnow, etc.) with _sr3 start_" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "neural-laugh", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-03-06 23:45:56,401 2118802 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "tail: cannot open '/net/local/home/shakerm/.cache/sr3/log/subscribe_hpfx_amis_01.log' for reading: No such file or directory\n", + "tail: no files remaining\n", + "2024-03-06 23:45:56,406 2118802 [CRITICAL] root run_command subprocess.run failed err=Command '['tail', '-f', '/net/local/home/shakerm/.cache/sr3/log/subscribe_hpfx_amis_01.log']' returned non-zero exit status 1.\n", + "\n" + ] + } + ], + "source": [ + "!sr3 log subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "leading-matthew", + "metadata": {}, + "source": [ + "When running in the background, output needs to go a log file. Since we have only ran this configuration file in the foreground, asking to see the log prints an error about the log being missing. This tells you that the logs are in the _~/.cache/sr3/log_ directory. Logs can be monitored in real-time with traditional tools such as _tail -f_ or _grep_.\n", + "\n", + "_sr3 stop_ does what you expect.\n", + "\n", + "Processes can crash. In the _sr3 status_ output above, if the number of processes in the Run column is less than in the Exp (for Expected) one, then it means that some instances have crashed. You can repair it (just start the missing instances) with:\n", + "\n", + "_sr3 sanity_ -- start missing instances, also kill strays if any found.\n", + "\n", + "So that's it, an introduction to running configurations in Sarracenia from the command line.\n", + "\n", + "\n", + "## Conclusion\n", + "\n", + "If all you want to do is obtain data from a data pump in real-time, the easiest way to go is using the command line interface to control some processes that run all the time so that they dump files in a certain directory.\n", + "\n", + "It isn't very efficient though. When dealing with a large number of files and aiming for high-speed processing, it’s more efficient to have your own application receive notifications about file arrivals rather than scanning a directory. This approach reduces CPU and I/O overhead while improving processing speed.\n", + "\n", + "The easiest way to do that is to add some callbacks to your flows. We'll cover that next." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Tutorials/2_CLI_with_flowcb_demo.html b/Tutorials/2_CLI_with_flowcb_demo.html new file mode 100644 index 000000000..32823f7a4 --- /dev/null +++ b/Tutorials/2_CLI_with_flowcb_demo.html @@ -0,0 +1,908 @@ + + + + + + + Customize File handling with Callbacks. — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Customize File handling with Callbacks.

+

All Sarracenia components implement the Flow algorithm, with different callbacks, in the Python programming language. Sarracenia’s main (Python) class is sarracenia.flow and the a great deal of core functionality is implemented using the class created to add custom processing to a flow, the flowcb (flow callback) class.

+

For a detailed discussion of the flow algorithm itself, have a look at Concepts manual. For any flow, one can add custom processing at a variety of times during processing by sub-classing the sarracenia.flowcb class.

+

Briefly, the algorithm has the following steps:

+
    +
  • init(self, options) – when the import happens, traditional python initialization

  • +
  • on_start – when an instance is started.

  • +
  • loop forever

    +
      +
    • gather – collect messages to be processed called: worklist.incoming

    • +
    • poll – another way to collect messages, only in the poll component.

    • +
    • filter – apply accept/reject regular expression matches to the message list. moves messages for files not to download from worklist.incoming to worklist.reject

      +
        +
      • after_accept callback entry point. process worklist.incoming, potentially rejecting some more.

      • +
      +
    • +
    • ack – worklist.rejected messages are acknowledged to upstream source as processing is complete.

    • +
    • work – perform a transfer or transformation on a file.

    • +
    • ack – worklist.ok messages for successfully transferred files are acknowledged to upstream source.

      +
        +
      • after_work callback entry point

      • +
      +
    • +
    • ack – worklist.failed messages for files which not successfully transferred are acknowledged.

    • +
    • post – post the result of the work done for the next step.

    • +
    • occasionaly… **on_housekeeping – do periodic cleanups…

    • +
    +
  • +
  • on_stop – shutdown processing.

  • +
+

for more details about flowcb entry points available, have a look at the source code:

+ +

Lets look at using the class in a configuration:

+
+
[1]:
+
+
+
+!sr3 remove subscribe/hpfx_amis.conf
+!sr3 add subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2024-01-12 15:51:55,713 127453 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+2024-01-12 15:51:55,714 127453 [INFO] root remove removing subscribe/hpfx_amis
+
+add: 2024-01-12 15:51:56,975 127456 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+
+
[2]:
+
+
+
+!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

have the flow stop after 10 messages are consumed.

+
+
[3]:
+
+
+
+!sr3 list fcb
+
+
+
+
+
+
+
+
+2024-01-12 15:52:22,624 127524 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/accept/auth_NASA_Earthdata.py flowcb/accept/auth_copernicus.py
+flowcb/accept/auth_eumetsat.py   flowcb/accept/dateappend.py
+flowcb/accept/delete.py          flowcb/accept/downloadbaseurl.py
+flowcb/accept/hourtree.py        flowcb/accept/httptohttps.py
+flowcb/accept/longflow.py        flowcb/accept/pathreplace.py
+flowcb/accept/posthourtree.py    flowcb/accept/postoverride.py
+flowcb/accept/printlag.py        flowcb/accept/rename4jicc.py
+flowcb/accept/renamedmf.py       flowcb/accept/renamewhatfn.py
+flowcb/accept/save.py            flowcb/accept/speedo.py
+flowcb/accept/sundewpxroute.py   flowcb/accept/testretry.py
+flowcb/accept/toclusters.py      flowcb/accept/tohttp.py
+flowcb/accept/tolocal.py         flowcb/accept/tolocalfile.py
+flowcb/accept/trim_legacy_fields.py flowcb/accept/wmotypesuffix.py
+flowcb/amserver.py               flowcb/block_reassembly.py
+flowcb/clamav.py                 flowcb/destfn/replace.py
+flowcb/destfn/sample.py          flowcb/download/mail_ingest.py
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py
+flowcb/filter/pclean_f90.py      flowcb/filter/pclean_f92.py
+flowcb/filter/wmo2msc.py         flowcb/gather/file.py
+flowcb/gather/message.py         flowcb/housekeeping/resources.py
+flowcb/log.py                    flowcb/mdelaylatest.py
+flowcb/nodupe/data.py            flowcb/nodupe/disk.py
+flowcb/nodupe/name.py            flowcb/nodupe/redis.py
+flowcb/pclean.py                 flowcb/poll/airnow.py
+flowcb/poll/eumetsat.py          flowcb/poll/mail.py
+flowcb/poll/nasa_mls_nrt.py      flowcb/poll/nexrad.py
+flowcb/poll/noaa_hydrometric.py  flowcb/poll/odata.py
+flowcb/poll/poll_NASA_CMR.py     flowcb/poll/rate_limit.py
+flowcb/poll/s3bucket.py          flowcb/poll/usgs.py
+flowcb/post/message.py           flowcb/report.py
+flowcb/retry.py                  flowcb/rootchown.py
+flowcb/run.py                    flowcb/rxqueue_gzip.py
+flowcb/sample.py                 flowcb/scheduled/wiski.py
+flowcb/send/am.py                flowcb/send/email.py
+flowcb/shiftdir2baseurl.py       flowcb/trace_on_stop.py
+flowcb/v2wrapper.py              flowcb/wistree.py
+flowcb/work/age.py               flowcb/work/check.py
+flowcb/work/citypage_check.py    flowcb/work/delete.py
+flowcb/work/rxpipe.py            flowcb/work/send_egc_les.py
+
+
+

Adding that line to the configuration means that the wistree flowcb subclass (source above) will be added to the flow, changing processing by having its routines called… the main one being after_accept

+
+
[4]:
+
+
+
+!echo callback accept.posthourtree >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+
+
[5]:
+
+
+
+!sr3 foreground subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2024-01-12 15:52:39,635 127558 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+.2024-01-12 15:52:40,036 [INFO] 127562 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+2024-01-12 15:52:40,038 [INFO] 127562 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}
+2024-01-12 15:52:40,038 [INFO] 127562 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']
+2024-01-12 15:52:40,041 [INFO] 127562 sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_accept', 'on_housekeeping', 'after_work', 'after_post', 'post'},  logMessageDump: False
+2024-01-12 15:52:40,042 [INFO] 127562 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']
+2024-01-12 15:52:40,042 [INFO] 127562 sarracenia.flow run pid: 127562 subscribe/hpfx_amis instance: 0
+2024-01-12 15:52:40,441 [INFO] 127562 sarracenia.moth.amqp _queueDeclare queue declared q_anonymous_subscribe.hpfx_amis.68942404.82515581 (as: amqps://anonymous@hpfx.collab.science.gc.ca/), (messages waiting: 0)
+2024-01-12 15:52:40,441 [INFO] 127562 sarracenia.moth.amqp getSetup binding q_anonymous_subscribe.hpfx_amis.68942404.82515581 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2024-01-12 15:52:40,487 [INFO] 127562 sarracenia.flow run now active on vip ['AnyAddressIsFine']
+2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.82 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___32878
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.28 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SO/KWNB/20/SOVD83_KWNB_121900_RRX__26978
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.27 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SM/KWNB/20/SMVD20_KWNB_121800_RRX__52297
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.27 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SM/KWNB/20/SMVD20_KWNB_121800_RRX__48301
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 3.97 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___22701
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 3.97 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SN/KWNB/20/SNVD20_KWNB_121900_RRX__17918
+2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flow do_download missing destination directories, makedirs: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___32878
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SOVD83_KWNB_121900_RRX__26978
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SMVD20_KWNB_121800_RRX__52297
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SMVD20_KWNB_121800_RRX__48301
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___22701
+2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD20_KWNB_121900_RRX__17918
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 1.53 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRMN70_KWAL_122052___39567
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 1.53 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___5395
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 0.52 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRWA20_KWAL_122052___2278
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SX/KWAL/20/SXCN40_KWAL_122052___60928
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRME20_KWAL_122052___58721
+2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SN/KWNB/20/SNVD22_KWNB_121900_RRX__60599
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRMN70_KWAL_122052___39567
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___5395
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRWA20_KWAL_122052___2278
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SXCN40_KWAL_122052___60928
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRME20_KWAL_122052___58721
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD22_KWNB_121900_RRX__60599
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flow please_stop ok, telling 5 callbacks about it.
+2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flow run starting last pass (without gather) through loop for cleanup.
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flow please_stop ok, telling 5 callbacks about it.
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flow run on_housekeeping pid: 127562 subscribe/hpfx_amis instance: 0
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flowcb.gather.message on_housekeeping messages: good: 12 bad: 0 bytes: 1.6 KiB average: 140 Bytes
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flowcb.retry on_housekeeping on_housekeeping
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping No retry in list
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000168
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping No retry in list
+2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000106
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.44 system=0.07
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 92.3 MiB, accumulating count (12 or 12/100 so far) before self-setting threshold
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats version: 3.00.51rc5, started: 5 seconds ago, last_housekeeping:  5.7 seconds ago
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats messages received: 12, accepted: 12, rejected: 0   rate accepted: 100.0% or 2.1 m/s
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats files transferred: 12 bytes: 2.2 KiB rate: 386 Bytes/sec
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats lag: average: 3.30, maximum: 6.28
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log on_housekeeping housekeeping
+2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flow run clean stop from run loop
+2024-01-12 15:52:45,763 [INFO] 127562 sarracenia.flowcb.gather.message on_stop closing
+2024-01-12 15:52:45,764 [INFO] 127562 sarracenia.flow close flow/close completed cleanly pid: 127562 subscribe/hpfx_amis instance: 0
+
+
+
+

Without the plugin, the download would put all files directly the reception directory. with the addition of the wistree callback, it puts places the file in /tmp/hpfx_amis. With the change it puts it in the WIS tree of directories, and adds a file type suffix.

+
+

Config File Entries and Callbacks

+

flowcb.log

+

To add a callback to a a flow, a line is added to the flows’s configuration file:

+
flowcb sarracenia.flowcb.log.Log
+
+
+

If you follow the convention, and the name of the class is a capitalized version (Log) of the file name (log), then a shorthand is available:

+
callback log
+
+
+

Either way it is done, it will cause Sarracenia to import the class and then look for entry points in the class to call at appropriate times.

+

The class constructor accepts a sarracenia.config.Config class object, called options, that stores all the settings to be used by the running flow. Options is used to override default behaviour of both flows and callbacks. The argument to the flowcb is a standard python class that needs to be in the normal python path for python import, and the last element is the name of the class in within the file that needs to be instantiated as a flowcb instance.

+

a setting for a callback is declared as follows:

+
set sarracenia.flowcb.filter.log.Log.logLevel debug
+
+
+

(the prefix for the setting matches the type hierarchy in flowCallback)

+

when the constructor for the callback is called, it’s options argument will contain:

+
options.logLevel = 'debug'
+
+
+

If no module specific override is present, then the more global setting is used.

+

So usage of callbacks can be made without much python knowledge at all, just the ability to create configuration files.

+

Beyond this point, we find advice for people who want to write their own callbacks in Python. Callbacks are ordinary Python, with a few wrinkles:

+
+
+

Writing Your Own Callbacks

+

A flow callback, is a python class built with routines named to indicate when the programmer wants them to be called. To do that, create a routine which subclasses sarracenia.flowcb.FlowCB so the class will normally have:

+

from sarracenia.flowcb import FlowCB

+

in among the imports near the top of the file. In the main part of the file, there will be the custom callback classes:

+

class Myclass(FlowCB):

+

declared as a subclass as FlowCB. The main routines in the class are entry points that will be called at the time their name implies. If you a class is missing a given entry point, it will just not be called. The init() class is used to initialize things for the callback class:

+
def __init__(self, options):
+
+    self.o = options
+
+    logging.basicConfig(format=self.o.logFormat,
+                        level=getattr(logging, self.o.logLevel.upper()))
+    logger.setLevel(getattr(logging, self.o.logLevel.upper()))
+
+    self.o.add_option( 'myoption', 'str', 'usuallyThis')
+
+
+

The logging setup lines in init allow setting a specific logging level for this flowCallback class. Once the logging boiler-plate is done, the add_option routine to define settings to for the class. users can include them in configuration files, just like built-in options:

+
myoption IsReallyNeeded
+
+
+

The result of such a setting is that the self.o.myoption = ‘IsReallyNeeded’. If no value is set in the configuration, self.o.myoption will default to ‘usuallyThis’ There are various kinds of options, where the declared type modifies the parsing:

+
'count'    integer count type.
+'duration' a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)
+           modified by a unit suffix ( m-minute, h-hour, w-week )
+'flag'     boolean (True/False) option.
+'list'     a list of string values, each succeeding occurrence catenates to the total.
+           all v2 plugin options are declared of type list.
+'size'     integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.
+'str'      an arbitrary string value, as will all of the above types, each
+           succeeding occurrence overrides the previous one.
+
+
+
+
+

Worklists

+

Besides options, the other main argument to after_accept and after_work callback routines is the worklist. The worklist is given to entry points occurring during message processing, and is a number of worklists of messages:

+
worklist.incoming --> messages to process (either new or retries.)
+worklist.ok       --> successfully processed
+worklist.rejected --> messages to not be further processed.
+worklist.failed   --> messages for which processing failed.
+                      failed messages will be retried.
+worklist.directories_ok --> list of directories created during processing.
+
+
+

Initially, all messages are placed in worklists.incoming. if a plugin decides:

+
    +
  • a message is not relevant, moved it to the rejected worklist.

  • +
  • a no further processing of the message is needed, move it to ok worklist.

  • +
  • an operation failed and it should be retried later, move to failed worklist.

  • +
+

Do not remove from all lists, only move messages between the worklists. it is necessary to put rejected messages in the appropriate worklist so that they are acknowledged as received. Messages can only removed after the acknowledgement has been taken care of.

+
+
+

Logging

+

Python has great built-in logging, and once has to just use the module in a normal, pythonic way, with:

+

import logging

+

After all imports in your python source file, then define a logger for the source file:

+

logger = logging.getLogger(_name_)

+

As is normal with the Python logging module, messages can then be posted to the log:

+

logger.debug(‘got here’)

+

Each message in the log will be prefixed with the class and routine emitting the log message, as well as the date/time.

+
+
+

Sample Flow Callback Class

+

With the above information about option handling, worklists, and logging, we are ready to understand the wistree module we just used. As a very simple example, here is the source code of the callback used above is given below:

+
+
[ ]:
+
+
+
+"""
+Plugin posthourtree.py:
+    When posting a file, insert an hourly directory into the delivery path hierarchy.
+
+Example:
+    input A/B/c.gif  --> output A/B/<hour>/c.gif
+
+Usage:
+    callback accept.posthourtree
+
+"""
+import logging
+import sys, os, os.path, time, stat
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+class Posthourtree(FlowCB):
+
+    def after_accept(self, worklist):
+        for message in worklist.incoming:
+            datestr = time.strftime('%H', time.gmtime())  # pick the hour
+            # insert the hour into the rename header of the message to be posted.
+            message['new_dir'] += '/' + datestr
+            logger.info(  f"post_hour_tree: new_dir: {message['new_dir']}" )
+
+
+
+
+
+

Sample Flowcb Sub-Class

+

This wistree.py class, shows more aspects of the callback API, with an init.py as well as bringing in an externam python module, as well as adding fields to the messages. The Wistree class accepts files whose names begin with AHL’s (World Meteorological Organization Abbreviated Header Lines for meteorological products), and renames the directory tree to a different standard, the evolving one for the WMO WIS 2.0 (for more information on that module: https://github.com/wmo-im/GTStoWIS2)

+
+
[6]:
+
+
+
+  from sarracenia.flowcb import FlowCB
+  import logging
+  import GTStoWIS2
+
+  logger = logging.getLogger(__name__)
+
+
+  class Wistree(FlowCB):
+
+    def __init__(self, options):
+
+        if hasattr(options, 'logLevel'):
+            logger.setLevel(getattr(logging, options.logLevel.upper()))
+        else:
+            logger.setLevel(logging.INFO)
+        self.topic_builder=GTStoWIS2.GTStoWIS2()
+        self.o = options
+
+
+    def after_accept(self, worklist):
+
+        new_incoming=[]
+
+        for msg in worklist.incoming:
+
+            # fix file name suffix.
+            type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )
+            tpfx=msg['subtopic']
+
+            # input has relpath=/YYYYMMDDTHHMM/... + pubTime
+            # need to move the date from relPath to BaseDir, adding the T hour from pubTime.
+            try:
+                new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]
+                t='.'.join(tpfx[0:2])+'.'+new_baseSubDir
+                new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir
+                new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])
+                new_dir = new_baseDir + os.sep + new_relDir
+
+                if msg['new_file'][-len(type_suffix):] != type_suffix:
+                    new_file = msg['new_file']+type_suffix
+                else:
+                    new_file = msg['new_file']
+
+                msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, new_file )
+            except Exception as ex:
+                logger.error( "skipped" , exc_info=True )
+                worklist.failed.append(msg)
+                continue
+
+            msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )
+            new_incoming.append(msg)
+
+        worklist.incoming=new_incoming
+
+
+
+
+
+
+
+

Plugins That Change How a File is Downloaded

+

The after_accept routine is one of the two most common ones in use. It is used to change processing prior to a file being downloaded or sent. To process the file after it has been downloaded, the after_work entry point is used to process the worklist.ok (files that were successfully downloaded) list.

+

The after_accept routine has an outer loop that cycles through the entire list of incoming messages. It builds a new list of incoming messages from the ones it accepts, while appending all the rejected ones to worklist.failed. The list is just a list of messages, where each message is a python dictionary with all the fields stored in a v03 format message. In the message there are, for example, baseURL and relPath fields:

+
    +
  • baseURL - the baseURL of the resource from which a file would be obtained.

  • +
  • relPath - the relative path to append to the baseURL to get the complete download URL.

  • +
+

This is happenning before transfer (download or sent, or processing) of the file has occurred, so one can change the behaviour by modifying fields in the message. Normally, the download paths (called new_dir, and new_file) will reflect the intent to mirror the original source tree. so if you have a/b/c.txt on the source tree, and are downloading in to directory mine on the local system, the new_dir would be mine/a/b and new_file would be c.txt.

+
+
+

Plugins that Process a file after it is Downloaded

+

A common use case is for plugins with an after_work entry point to read the file after it is downloaded and transform it into some derived product with a different name. So the new file is created as in the previous section. The message for the downloaded file still needs to be moved onto a list to ensure that it is acknowledged to the broker. Such an entry point would look like this:

+
+
[9]:
+
+
+
+
+    def after_work(self, worklist):
+
+        new_ok=[]
+        for m in worklist.ok:
+             success=do_something()
+             if success:
+                   new_ok.append(m)
+             # since it is already acknowledged, we can just drop it from ok.
+
+
+        worklist.ok = new_ok
+        # the messages on worklist.ok will get posted in the next algorithm phase.
+
+
+
+
+
+

Plugins that Rename Files

+

The plugin above changes the layout of the files that are to be downloaded, based on the GTStoWIS class, which prescribes a different directory tree on output. There are a lot of fields to update when changing file placement, so best to use:

+

msg.updatePaths( self.o, new_dir, new_file )

+

to update all necessary fields in the message properly. It will update ‘new_baseURL’, ‘new_relPath’, ‘new_subtopic’ for use when posting.

+

The try/except part of the routine deals with the case that, should a file arrive with a name from which a topic tree cannot be built, then a python exception may occur, and the message is added to the failed worklist, and will not be processed by later plugins.

+
+
+

Plugins That Create New Files

+

The routine above is perfect when a file is just renamed. If a plugin needs to create new files only vaguely derived from the input file, then you want to create new messages for these files from scratch:

+
import sarracenia
+
+m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )
+
+
+

The msg_fromFileData routine will use self.o to apply the appropriate posting settings. no knowledge of message formats, or construction of fields is needed. If the file is not local, such as when writing a poll callback, an alternate routing can be used:

+
m = sarracenia.Message.fromFileInfo(sample_fileName, self.o, fake_stat_info )
+
+
+

the fake stat record (as per the stat(2) man page or python os.stat() ) can be built from other fields, starting with:

+
import paramiko
+
+fake_stat = paramiko.SFTPAttributes()
+fake_stat.st_mtime = ... something else... perhaps an http header?
+fake_stat.st_size = ... again will vary by context.
+
+
+

Either way, once you have the message, it can be appended to the incoming list.

+
+
+

Other Examples

+

Subclassing of Sarracenia.flowcb is used internally to do a lot of core work. It’s a good idea to look at the sarracenia source code itself. For example:

+
    +
  • sarracenia.flowcb have a look at the init.py file in there, which provides this information on a more programmatically succinct format.

  • +
  • sarracenia.flowcb.gather.file is a class that implements file posting and directory watching, in the sense of a callback that implements the gather entry point, by reading a file system and building a list of messages for processing.

  • +
  • sarracenia.flowcb.gather.message is a class that implements reception of messages from message queue protocol flows.

  • +
  • sarracenia.flowcb.gather.nodupe This modules removes duplicates from message flows based on Identity checksums.

  • +
  • sarracenia.flowcb.post.message is a class that implements posting messages to Message queue protocol flows

  • +
  • sarracenia.flowcb.retry when the transfer of a file fails. Sarracenia needs to persist the relevant message to a state file for a later time when it can be tried again.

  • +
+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/2_CLI_with_flowcb_demo.ipynb b/Tutorials/2_CLI_with_flowcb_demo.ipynb new file mode 100644 index 000000000..84d1ead12 --- /dev/null +++ b/Tutorials/2_CLI_with_flowcb_demo.ipynb @@ -0,0 +1,715 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "acoustic-deviation", + "metadata": {}, + "source": [ + "# Customize File handling with Callbacks.\n", + "\n", + "\n", + "All Sarracenia components implement *the Flow* algorithm, with different\n", + "callbacks, in the Python programming language. Sarracenia's main (Python) \n", + "class is [sarracenia.flow](../Reference/code.html#module-sarracenia.flow) and the a great deal of core functionality is \n", + "implemented using the class created to add custom processing to a flow, the \n", + "flowcb (flow callback) class.\n", + "\n", + "For a detailed discussion of the flow algorithm itself, have a look\n", + "at [Concepts](../Explanation/Concepts.html) manual. For any flow, one can\n", + "add custom processing at a variety of times during processing by sub-classing\n", + "the [sarracenia.flowcb](../Reference/flowcb.html) class.\n", + "\n", + "Briefly, the algorithm has the following steps:\n", + "\n", + "* **__init__(self, options)** -- when the import happens, traditional python initialization\n", + "* **on_start** -- when an instance is started.\n", + "* loop forever\n", + " * **gather** -- collect messages to be processed called: worklist.incoming\n", + " * **poll** -- another way to collect messages, only in the poll component.\n", + " * **filter** -- apply accept/reject regular expression matches to the message list.\n", + " moves messages for files not to download from worklist.incoming to worklist.reject\n", + " * *after_accept* callback entry point. process worklist.incoming, potentially rejecting some more.\n", + " * **ack** -- worklist.rejected messages are acknowledged to upstream source as processing is complete.\n", + " * **work** -- perform a transfer or transformation on a file.\n", + " * **ack** -- worklist.ok messages for successfully transferred files are acknowledged to upstream source.\n", + " * *after_work* callback entry point\n", + " * **ack** -- worklist.failed messages for files which not successfully transferred are acknowledged.\n", + " * **post** -- post the result of the work done for the next step.\n", + " * occasionaly... **on_housekeeping -- do periodic cleanups...\n", + "* **on_stop** -- shutdown processing.\n", + "\n", + "for more details about flowcb entry points available, have a look at the source code: \n", + "\n", + "* [flowcb](../Reference/flowcb.html)\n" + ] + }, + { + "cell_type": "markdown", + "id": "external-mention", + "metadata": {}, + "source": [ + "Lets look at using the class in a configuration:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "coordinated-cocktail", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-01-12 15:51:55,713 127453 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "2024-01-12 15:51:55,714 127453 [INFO] root remove removing subscribe/hpfx_amis\n", + "\n", + "add: 2024-01-12 15:51:56,975 127456 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf \n", + "\n" + ] + } + ], + "source": [ + "!sr3 remove subscribe/hpfx_amis.conf\n", + "!sr3 add subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "tired-north", + "metadata": {}, + "outputs": [], + "source": [ + "!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "psychological-ratio", + "metadata": {}, + "source": [ + "have the flow stop after 10 messages are consumed." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "greater-nevada", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-01-12 15:52:22,624 127524 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\r\n", + "Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia ) \r\n", + "flowcb/accept/auth_NASA_Earthdata.py flowcb/accept/auth_copernicus.py \r\n", + "flowcb/accept/auth_eumetsat.py flowcb/accept/dateappend.py \r\n", + "flowcb/accept/delete.py flowcb/accept/downloadbaseurl.py \r\n", + "flowcb/accept/hourtree.py flowcb/accept/httptohttps.py \r\n", + "flowcb/accept/longflow.py flowcb/accept/pathreplace.py \r\n", + "flowcb/accept/posthourtree.py flowcb/accept/postoverride.py \r\n", + "flowcb/accept/printlag.py flowcb/accept/rename4jicc.py \r\n", + "flowcb/accept/renamedmf.py flowcb/accept/renamewhatfn.py \r\n", + "flowcb/accept/save.py flowcb/accept/speedo.py \r\n", + "flowcb/accept/sundewpxroute.py flowcb/accept/testretry.py \r\n", + "flowcb/accept/toclusters.py flowcb/accept/tohttp.py \r\n", + "flowcb/accept/tolocal.py flowcb/accept/tolocalfile.py \r\n", + "flowcb/accept/trim_legacy_fields.py flowcb/accept/wmotypesuffix.py \r\n", + "flowcb/amserver.py flowcb/block_reassembly.py \r\n", + "flowcb/clamav.py flowcb/destfn/replace.py \r\n", + "flowcb/destfn/sample.py flowcb/download/mail_ingest.py \r\n", + "flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py \r\n", + "flowcb/filter/pclean_f90.py flowcb/filter/pclean_f92.py \r\n", + "flowcb/filter/wmo2msc.py flowcb/gather/file.py \r\n", + "flowcb/gather/message.py flowcb/housekeeping/resources.py \r\n", + "flowcb/log.py flowcb/mdelaylatest.py \r\n", + "flowcb/nodupe/data.py flowcb/nodupe/disk.py \r\n", + "flowcb/nodupe/name.py flowcb/nodupe/redis.py \r\n", + "flowcb/pclean.py flowcb/poll/airnow.py \r\n", + "flowcb/poll/eumetsat.py flowcb/poll/mail.py \r\n", + "flowcb/poll/nasa_mls_nrt.py flowcb/poll/nexrad.py \r\n", + "flowcb/poll/noaa_hydrometric.py flowcb/poll/odata.py \r\n", + "flowcb/poll/poll_NASA_CMR.py flowcb/poll/rate_limit.py \r\n", + "flowcb/poll/s3bucket.py flowcb/poll/usgs.py \r\n", + "flowcb/post/message.py flowcb/report.py \r\n", + "flowcb/retry.py flowcb/rootchown.py \r\n", + "flowcb/run.py flowcb/rxqueue_gzip.py \r\n", + "flowcb/sample.py flowcb/scheduled/wiski.py \r\n", + "flowcb/send/am.py flowcb/send/email.py \r\n", + "flowcb/shiftdir2baseurl.py flowcb/trace_on_stop.py \r\n", + "flowcb/v2wrapper.py flowcb/wistree.py \r\n", + "flowcb/work/age.py flowcb/work/check.py \r\n", + "flowcb/work/citypage_check.py flowcb/work/delete.py \r\n", + "flowcb/work/rxpipe.py flowcb/work/send_egc_les.py \r\n" + ] + } + ], + "source": [ + "!sr3 list fcb" + ] + }, + { + "cell_type": "markdown", + "id": "efficient-picture", + "metadata": {}, + "source": [ + "Adding that line to the configuration means that the wistree flowcb subclass (source above) will be added to \n", + "the flow, changing processing by having its routines called... the main one being *after_accept*" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "external-commercial", + "metadata": {}, + "outputs": [], + "source": [ + "!echo callback accept.posthourtree >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "insured-fetish", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-01-12 15:52:39,635 127558 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + ".2024-01-12 15:52:40,036 [INFO] 127562 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "2024-01-12 15:52:40,038 [INFO] 127562 sarracenia.config finalize overriding batch for consistency with messageCountMax: {self.batch}\n", + "2024-01-12 15:52:40,038 [INFO] 127562 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']\n", + "2024-01-12 15:52:40,041 [INFO] 127562 sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_accept', 'on_housekeeping', 'after_work', 'after_post', 'post'}, logMessageDump: False\n", + "2024-01-12 15:52:40,042 [INFO] 127562 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']\n", + "2024-01-12 15:52:40,042 [INFO] 127562 sarracenia.flow run pid: 127562 subscribe/hpfx_amis instance: 0\n", + "2024-01-12 15:52:40,441 [INFO] 127562 sarracenia.moth.amqp _queueDeclare queue declared q_anonymous_subscribe.hpfx_amis.68942404.82515581 (as: amqps://anonymous@hpfx.collab.science.gc.ca/), (messages waiting: 0)\n", + "2024-01-12 15:52:40,441 [INFO] 127562 sarracenia.moth.amqp getSetup binding q_anonymous_subscribe.hpfx_amis.68942404.82515581 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)\n", + "2024-01-12 15:52:40,487 [INFO] 127562 sarracenia.flow run now active on vip ['AnyAddressIsFine']\n", + "2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,644 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.82 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___32878\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.28 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SO/KWNB/20/SOVD83_KWNB_121900_RRX__26978\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.27 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SM/KWNB/20/SMVD20_KWNB_121800_RRX__52297\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 6.27 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SM/KWNB/20/SMVD20_KWNB_121800_RRX__48301\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 3.97 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___22701\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 3.97 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SN/KWNB/20/SNVD20_KWNB_121900_RRX__17918\n", + "2024-01-12 15:52:44,645 [INFO] 127562 sarracenia.flow do_download missing destination directories, makedirs: /tmp/hpfx_amis/20 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___32878 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SOVD83_KWNB_121900_RRX__26978 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SMVD20_KWNB_121800_RRX__52297 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SMVD20_KWNB_121800_RRX__48301 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___22701 \n", + "2024-01-12 15:52:45,153 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD20_KWNB_121900_RRX__17918 \n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 1.53 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRMN70_KWAL_122052___39567\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 1.53 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRCN40_KWAL_122052___5395\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 0.52 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRWA20_KWAL_122052___2278\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SX/KWAL/20/SXCN40_KWAL_122052___60928\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SR/KWAL/20/SRME20_KWAL_122052___58721\n", + "2024-01-12 15:52:45,263 [INFO] 127562 sarracenia.flowcb.log after_accept accepted: (lag: 2.14 ) https://hpfx.collab.science.gc.ca /20240112/WXO-DD/bulletins/alphanumeric/20240112/SN/KWNB/20/SNVD22_KWNB_121900_RRX__60599\n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRMN70_KWAL_122052___39567 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_122052___5395 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRWA20_KWAL_122052___2278 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SXCN40_KWAL_122052___60928 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRME20_KWAL_122052___58721 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD22_KWNB_121900_RRX__60599 \n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flow please_stop ok, telling 5 callbacks about it.\n", + "2024-01-12 15:52:45,735 [INFO] 127562 sarracenia.flow run starting last pass (without gather) through loop for cleanup.\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flow please_stop ok, telling 5 callbacks about it.\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flow run on_housekeeping pid: 127562 subscribe/hpfx_amis instance: 0\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flowcb.gather.message on_housekeeping messages: good: 12 bad: 0 bytes: 1.6 KiB average: 140 Bytes\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.flowcb.retry on_housekeeping on_housekeeping\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000168\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-01-12 15:52:45,741 [INFO] 127562 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000106\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.44 system=0.07\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 92.3 MiB, accumulating count (12 or 12/100 so far) before self-setting threshold\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats version: 3.00.51rc5, started: 5 seconds ago, last_housekeeping: 5.7 seconds ago \n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats messages received: 12, accepted: 12, rejected: 0 rate accepted: 100.0% or 2.1 m/s\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats files transferred: 12 bytes: 2.2 KiB rate: 386 Bytes/sec\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log stats lag: average: 3.30, maximum: 6.28 \n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flowcb.log on_housekeeping housekeeping\n", + "2024-01-12 15:52:45,742 [INFO] 127562 sarracenia.flow run clean stop from run loop\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2024-01-12 15:52:45,763 [INFO] 127562 sarracenia.flowcb.gather.message on_stop closing\n", + "2024-01-12 15:52:45,764 [INFO] 127562 sarracenia.flow close flow/close completed cleanly pid: 127562 subscribe/hpfx_amis instance: 0\n", + "\n" + ] + } + ], + "source": [ + "!sr3 foreground subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "stretch-directive", + "metadata": {}, + "source": [ + "Without the plugin, the download would put all files directly the reception directory. with the addition of the wistree callback, it puts places the file in /tmp/hpfx_amis. With the change it puts it in the WIS tree of directories, and adds a file type suffix.\n" + ] + }, + { + "cell_type": "markdown", + "id": "funny-isolation", + "metadata": {}, + "source": [ + "## Config File Entries and Callbacks\n", + "\n", + "\n", + "[flowcb.log](../Reference/flowcb.html#module-sarracenia.flowcb.log)\n", + "\n", + "To add a callback to a a flow, a line is added to the flows's configuration file:\n", + "\n", + " flowcb sarracenia.flowcb.log.Log\n", + "\n", + "If you follow the convention, and the name of the class is a capitalized\n", + "version (Log) of the file name (log), then a shorthand is available:\n", + "\n", + " callback log \n", + "\n", + "Either way it is done, it will cause Sarracenia to import the class and then\n", + "look for entry points in the class to call at appropriate times.\n", + "\n", + "The class constructor accepts a sarracenia.config.Config class object,\n", + "called options, that stores all the settings to be used by the running flow.\n", + "Options is used to override default behaviour of both flows and callbacks.\n", + "The argument to the flowcb is a standard python class that needs to be\n", + "in the normal python path for python *import*, and the last element\n", + "is the name of the class in within the file that needs to be instantiated\n", + "as a flowcb instance.\n", + "\n", + "a setting for a callback is declared as follows:\n", + "\n", + " set sarracenia.flowcb.filter.log.Log.logLevel debug\n", + "\n", + "(the prefix for the setting matches the type hierarchy in flowCallback)\n", + "\n", + "when the constructor for the callback is called, it's options\n", + "argument will contain:\n", + "\n", + " options.logLevel = 'debug'\n", + "\n", + "If no module specific override is present, then the more global\n", + "setting is used.\n", + "\n", + "So usage of callbacks can be made without much python knowledge at all,\n", + "just the ability to create configuration files.\n", + "\n", + "Beyond this point, we find advice for people who want to write their\n", + "own callbacks in Python. Callbacks are ordinary Python, with a few wrinkles:" + ] + }, + { + "cell_type": "markdown", + "id": "shared-album", + "metadata": {}, + "source": [ + "## Writing Your Own Callbacks\n", + "\n", + "\n", + "A flow callback, is a python class built with routines named to\n", + "indicate when the programmer wants them to be called.\n", + "To do that, create a routine which subclasses *sarracenia.flowcb.FlowCB*\n", + "so the class will normally have:\n", + "\n", + " from sarracenia.flowcb import FlowCB\n", + "\n", + "in among the imports near the top of the file.\n", + "In the main part of the file, there will be the\n", + "custom callback classes:\n", + "\n", + " class Myclass(FlowCB):\n", + "\n", + "declared as a subclass as FlowCB. The main routines in the class are entry points\n", + "that will be called at the time their name implies. If you a class is missing a\n", + "given entry point, it will just not be called. The __init__() class is used to\n", + "initialize things for the callback class:\n", + "\n", + " def __init__(self, options):\n", + "\n", + " self.o = options\n", + "\n", + " logging.basicConfig(format=self.o.logFormat,\n", + " level=getattr(logging, self.o.logLevel.upper()))\n", + " logger.setLevel(getattr(logging, self.o.logLevel.upper()))\n", + "\n", + " self.o.add_option( 'myoption', 'str', 'usuallyThis')\n", + "\n", + "The logging setup lines in __init__ allow setting a specific logging level\n", + "for this flowCallback class. Once the logging boiler-plate is done,\n", + "the add_option routine to define settings to for the class.\n", + "users can include them in configuration files, just like built-in options:\n", + "\n", + " myoption IsReallyNeeded\n", + "\n", + "The result of such a setting is that the *self.o.myoption = 'IsReallyNeeded'*.\n", + "If no value is set in the configuration, *self.o.myoption* will default to *'usuallyThis'*\n", + "There are various *kinds* of options, where the declared type modifies the parsing:\n", + " \n", + " 'count' integer count type. \n", + " 'duration' a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)\n", + " modified by a unit suffix ( m-minute, h-hour, w-week ) \n", + " 'flag' boolean (True/False) option.\n", + " 'list' a list of string values, each succeeding occurrence catenates to the total.\n", + " all v2 plugin options are declared of type list.\n", + " 'size' integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.\n", + " 'str' an arbitrary string value, as will all of the above types, each \n", + " succeeding occurrence overrides the previous one.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "determined-medicare", + "metadata": {}, + "source": [ + "## Worklists\n", + "\n", + "Besides *options*, the other main argument to after_accept and after_work callback\n", + "routines is the worklist. The worklist is given to entry points occurring during message\n", + "processing, and is a number of worklists of messages:\n", + "\n", + " worklist.incoming --> messages to process (either new or retries.)\n", + " worklist.ok --> successfully processed\n", + " worklist.rejected --> messages to not be further processed.\n", + " worklist.failed --> messages for which processing failed.\n", + " failed messages will be retried.\n", + " worklist.directories_ok --> list of directories created during processing.\n", + "\n", + "Initially, all messages are placed in worklists.incoming.\n", + "if a plugin decides:\n", + "\n", + "- a message is not relevant, moved it to the rejected worklist.\n", + "- a no further processing of the message is needed, move it to ok worklist.\n", + "- an operation failed and it should be retried later, move to failed worklist.\n", + "\n", + "Do not remove from all lists, only move messages between the worklists.\n", + "it is necessary to put rejected messages in the appropriate worklist\n", + "so that they are acknowledged as received. Messages can only removed\n", + "after the acknowledgement has been taken care of." + ] + }, + { + "cell_type": "markdown", + "id": "advised-ordinance", + "metadata": {}, + "source": [ + "## Logging\n", + "\n", + "\n", + "Python has great built-in logging, and once has to just use the module\n", + "in a normal, pythonic way, with::\n", + "\n", + " import logging\n", + "\n", + "After all imports in your python source file, then define a logger\n", + "for the source file::\n", + "\n", + " logger = logging.getLogger(\\__name\\__)\n", + "\n", + "As is normal with the Python logging module, messages can then\n", + "be posted to the log::\n", + "\n", + " logger.debug('got here')\n", + "\n", + "Each message in the log will be prefixed with the class and routine\n", + "emitting the log message, as well as the date/time.\n", + "\n", + "## Sample Flow Callback Class\n", + "\n", + "With the above information about option handling, worklists, and logging, we\n", + "are ready to understand the wistree module we just used. As a very simple example,\n", + "here is the source code of the callback used above is given below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27954f45", + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "Plugin posthourtree.py:\n", + " When posting a file, insert an hourly directory into the delivery path hierarchy.\n", + "\n", + "Example:\n", + " input A/B/c.gif --> output A/B//c.gif\n", + "\n", + "Usage:\n", + " callback accept.posthourtree\n", + "\n", + "\"\"\"\n", + "import logging\n", + "import sys, os, os.path, time, stat\n", + "from sarracenia.flowcb import FlowCB\n", + "\n", + "logger = logging.getLogger(__name__)\n", + "\n", + "\n", + "class Posthourtree(FlowCB):\n", + "\n", + " def after_accept(self, worklist):\n", + " for message in worklist.incoming:\n", + " datestr = time.strftime('%H', time.gmtime()) # pick the hour\n", + " # insert the hour into the rename header of the message to be posted.\n", + " message['new_dir'] += '/' + datestr \n", + " logger.info( f\"post_hour_tree: new_dir: {message['new_dir']}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "million-smoke", + "metadata": {}, + "source": [ + "## Sample Flowcb Sub-Class\n", + "\n", + "\n", + "This wistree.py class, shows more aspects of the callback API, with an __init__.py as well as bringing in an externam python module, as well as adding fields to the messages.\n", + "The Wistree class accepts files whose names begin with AHL's (World Meteorological Organization Abbreviated Header Lines for meteorological products), and renames the directory tree to a different standard, the evolving one for the WMO WIS 2.0 (for more information on that module:\n", + "https://github.com/wmo-im/GTStoWIS2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "related-consensus", + "metadata": {}, + "outputs": [], + "source": [ + " from sarracenia.flowcb import FlowCB\n", + " import logging\n", + " import GTStoWIS2\n", + "\n", + " logger = logging.getLogger(__name__)\n", + "\n", + "\n", + " class Wistree(FlowCB):\n", + "\n", + " def __init__(self, options):\n", + "\n", + " if hasattr(options, 'logLevel'):\n", + " logger.setLevel(getattr(logging, options.logLevel.upper()))\n", + " else:\n", + " logger.setLevel(logging.INFO)\n", + " self.topic_builder=GTStoWIS2.GTStoWIS2()\n", + " self.o = options\n", + "\n", + "\n", + " def after_accept(self, worklist):\n", + "\n", + " new_incoming=[]\n", + "\n", + " for msg in worklist.incoming:\n", + "\n", + " # fix file name suffix.\n", + " type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )\n", + " tpfx=msg['subtopic']\n", + " \n", + " # input has relpath=/YYYYMMDDTHHMM/... + pubTime\n", + " # need to move the date from relPath to BaseDir, adding the T hour from pubTime.\n", + " try:\n", + " new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]\n", + " t='.'.join(tpfx[0:2])+'.'+new_baseSubDir\n", + " new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir\n", + " new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])\n", + " new_dir = new_baseDir + os.sep + new_relDir\n", + " \n", + " if msg['new_file'][-len(type_suffix):] != type_suffix: \n", + " new_file = msg['new_file']+type_suffix\n", + " else:\n", + " new_file = msg['new_file']\n", + " \n", + " msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, new_file )\n", + " except Exception as ex:\n", + " logger.error( \"skipped\" , exc_info=True )\n", + " worklist.failed.append(msg)\n", + " continue\n", + " \n", + " msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )\n", + " new_incoming.append(msg)\n", + "\n", + " worklist.incoming=new_incoming \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "offshore-student", + "metadata": {}, + "source": [ + "\n", + "## Plugins That Change How a File is Downloaded\n", + "\n", + "\n", + "The *after_accept* routine is one of the two most common ones in use. It is used to change processing prior to a file being downloaded or sent. To process the file after it has been downloaded, the *after_work* entry point is used to process the worklist.ok (files that were successfully downloaded) list.\n", + "\n", + "The after_accept routine has an outer loop that cycles through the entire list of incoming messages. It builds a new list of incoming messages from the ones it accepts, while appending all the rejected ones to *worklist.failed.* The list is just a list of messages, where each message is a python dictionary with all the fields stored in a v03 format message. In the message there are, for example, *baseURL* and *relPath* fields:\n", + "\n", + "* baseURL - the baseURL of the resource from which a file would be obtained.\n", + "* relPath - the relative path to append to the baseURL to get the complete download URL.\n", + "\n", + "This is happenning before transfer (download or sent, or processing) of the file has occurred, so one can change the behaviour by modifying fields in the message. Normally, the download paths (called new_dir, and new_file) will reflect the intent to mirror the original source tree. so if you have *a/b/c.txt* on the source tree, and are downloading in to directory *mine* on the local system, the new_dir would be *mine/a/b* and new_file would be *c.txt*.\n", + "\n", + "## Plugins that Process a file after it is Downloaded\n", + "\n", + "\n", + "A common use case is for plugins with an *after_work* entry point to read the file after it is downloaded and transform it into some derived product with a different name. So the new file is created as in the previous section. The message for the downloaded file still needs to be moved onto a list to ensure that it is acknowledged to the broker. Such an entry point would look like this:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "disciplinary-dublin", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + " def after_work(self, worklist):\n", + "\n", + " new_ok=[]\n", + " for m in worklist.ok:\n", + " success=do_something()\n", + " if success:\n", + " new_ok.append(m)\n", + " # since it is already acknowledged, we can just drop it from ok.\n", + " \n", + " \n", + " worklist.ok = new_ok\n", + " # the messages on worklist.ok will get posted in the next algorithm phase." + ] + }, + { + "cell_type": "markdown", + "id": "coastal-moses", + "metadata": {}, + "source": [ + "\n", + "## Plugins that Rename Files\n", + "\n", + "\n", + "The plugin above changes the layout of the files that are to be downloaded, based on the [GTStoWIS](https://github.com/wmo-im/GTStoWIS) class, which prescribes a different directory tree on output. There are a lot of fields to update when changing file placement, so best to use:\n", + "\n", + " msg.updatePaths( self.o, new_dir, new_file )\n", + "\n", + "to update all necessary fields in the message properly. It will update 'new_baseURL', 'new_relPath', 'new_subtopic' for use when posting.\n", + "\n", + "The try/except part of the routine deals with the case that, should a file arrive with a name from which a topic tree cannot be built, then a python exception may occur, and the message is added to the failed worklist, and will not be processed by later plugins.\n", + "\n", + "## Plugins That Create New Files\n", + "\n", + "\n", + "The routine above is perfect when a file is just renamed. If a plugin needs to create new files only vaguely derived from the input file, then you want to create new messages for these files from scratch:\n", + "\n", + " import sarracenia\n", + "\n", + " m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )\n", + " \n", + "The msg_fromFileData routine will use self.o to apply the appropriate posting settings.\n", + "no knowledge of message formats, or construction of fields is needed. If the file is not\n", + "local, such as when writing a poll callback, an alternate routing can be used:\n", + "\n", + " m = sarracenia.Message.fromFileInfo(sample_fileName, self.o, fake_stat_info )\n", + "\n", + "the fake stat record (as per the stat(2) man page or python os.stat() ) can be built from other fields, starting with:\n", + "\n", + " import paramiko\n", + "\n", + " fake_stat = paramiko.SFTPAttributes()\n", + " fake_stat.st_mtime = ... something else... perhaps an http header?\n", + " fake_stat.st_size = ... again will vary by context.\n", + " \n", + "Either way, once you have the message, it can be appended to the incoming list.\n" + ] + }, + { + "cell_type": "markdown", + "id": "inclusive-scope", + "metadata": {}, + "source": [ + "## Other Examples\n", + "\n", + "\n", + "Subclassing of [Sarracenia.flowcb](../Reference/flowcb.html) is used internally to do a lot of core work. It's a good idea to look at the sarracenia source code itself. For example:\n", + "\n", + "* [sarracenia.flowcb](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/__init__.py) have a look at the __init__.py file in there, which\n", + " provides this information on a more programmatically succinct format.\n", + "\n", + "* [sarracenia.flowcb.gather.file](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/gather/file.py)\n", + " is a class that implements\n", + " file posting and directory watching, in the sense of a callback that\n", + " implements the *gather* entry point, by reading a file system and building a\n", + " list of messages for processing.\n", + "\n", + "* [sarracenia.flowcb.gather.message](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/gather/message.py)\n", + "is a class that implements reception of messages from message queue protocol flows.\n", + "\n", + "* [sarracenia.flowcb.gather.nodupe](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/nodupe)\n", + "This modules removes duplicates from message\n", + " flows based on Identity checksums.\n", + "\n", + "* [sarracenia.flowcb.post.message](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/post/message.py)\n", + "is a class that implements posting\n", + " messages to Message queue protocol flows\n", + "\n", + "* [sarracenia.flowcb.retry](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/retry.py)\n", + "when the transfer of a file fails. Sarracenia needs to persist the relevant message to a state file for\n", + " a later time when it can be tried again. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "expensive-yellow", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Tutorials/3_api_flow_demo.html b/Tutorials/3_api_flow_demo.html new file mode 100644 index 000000000..a7d430f7c --- /dev/null +++ b/Tutorials/3_api_flow_demo.html @@ -0,0 +1,596 @@ + + + + + + + flow API Example — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

flow API Example

+

The sarracenia.flow class provides built in accept/reject filtering for messages, supports built-in downloading in several protocols, retries on failure, and allows the creation of callbacks, to customize processing.

+

You need to provide a configuration as an argument when instantiating a subscriber. the sarracenia.config.no_file_config() returns an empty configuration without consulting any of the sr3 configuration file tree.

+

After adding the modifications needed to the configuration, the subscriber is then initiated and run.

+
+
[2]:
+
+
+
+!mkdir /tmp/flow_demo
+
+
+
+

make a directory for the files you are going to download. the root of the directory tree to must exist.

+
+
[3]:
+
+
+
+import re
+import sarracenia.config
+from sarracenia.flow.subscribe import Subscribe
+import sarracenia.flowcb
+import sarracenia.credentials
+
+cfg = sarracenia.config.no_file_config()
+
+cfg.broker = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+cfg.topicPrefix = [ 'v02', 'post']
+cfg.component = 'subscribe'
+cfg.config = 'flow_demo'
+cfg.action = 'start'
+cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]
+cfg.queueName='q_anonymous.subscriber_test2'
+cfg.download=True
+cfg.batch=1
+cfg.messageCountMax=5
+
+# set the instance number for the flow class.
+cfg.no=0
+
+# set other settings based on provided ones, so it is ready for use.
+
+cfg.finalize()
+
+# accept/reject patterns:
+pattern=".*"
+#              to_match, write_to_dir, DESTFN, regex_to_match, accept=True,mirror,strip, pstrip,flatten
+cfg.masks= [ ( pattern, "/tmp/flow_demo", None, re.compile(pattern), True, False, False, False, '/' ) ]
+
+
+
+
+
+
+
+

starters.

+

the broker, bindings, and queueName settings are explained in the moth notebook.

+
+
+

cfg.download

+

Whether you want the flow to download the files corresponding to the messages. If true, then it will download the files.

+
+
+

cfg.batch

+

Messages are processed in batches. The number of messages to retrieve per call to newMessages() is limited by the batch setting. We set it to 1 here so you can see each file being downloaded immediately when the corresponding message is downloaded. you can leave this blank, and it defaults to 25. Settings are matter of taste and use case.

+
+
+

cfg.messageCountMax

+

Normally we just leave this setting at it’s default (0) which has no effect on processing. for demonstration purposes, we limit the number of messages the subscriber will process with this setting. after messageCountMax messages have been received, stop processing.

+
+
+

cfg.masks

+

masks are a compiled form of accept/reject directives. a relPath is compared to the regex in the mask. If the regex matches, and accept is true, then the message is accepted for further processing. If the regex matches, but accept is False, then processing of the message is stopped (the message is rejected.)

+

masks are a tuple. the meaning can be looked up in the sr3(1) man page.

+
    +
  • pattern_string, the input regular expression string, to be compiled by re routines.

  • +
  • directory, where to put the files downloaded (root of the tree, when mirroring)

  • +
  • fn, transformation of filename to do. None is the 99% use case.

  • +
  • regex, compiled regex version of the pattern_string

  • +
  • accept(True/False), if pattern matches then accept message for further processing.

  • +
  • mirror(True/False), when downloading build a complete tree to mirror the source, or just dump in directory

  • +
  • strip(True/False), modify the relpath by stripping entries from the left.

  • +
  • pstrip(True/False), strip entries based on patterm

  • +
  • flatten(char … ‘/’ means do not flatten.) )

  • +
+
+
+

cfg.no, cfg.pid_filename

+

These settings are needed because they would ordinarily be set by the sarracenia.instance class which is normally used to launch flows. They allow setting up of run-time paths for retry_queues, and statefiles, to remember settings if need be between runs.

+
+
[4]:
+
+
+
+subscriber = sarracenia.flow.subscribe.Subscribe( cfg )
+
+subscriber.run()
+
+
+
+
+
+
+
+
+2024-01-29 15:00:37,351 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2024-01-29 15:00:37,354 [DEBUG] sarracenia.flowcb.retry __init__ sr_retry __init__
+2024-01-29 15:00:37,354 [DEBUG] sarracenia.config add_option []0 retry_driver declared as type:<class 'str'> value:disk
+2024-01-29 15:00:37,355 [DEBUG] sarracenia.diskqueue __init__  work_retry_00 __init__
+2024-01-29 15:00:37,357 [DEBUG] sarracenia.config add_option []0 MemoryMax declared as type:<class 'int'> value:0
+2024-01-29 15:00:37,357 [DEBUG] sarracenia.config add_option []0 MemoryBaseLineFile declared as type:<class 'int'> value:100
+2024-01-29 15:00:37,358 [DEBUG] sarracenia.config add_option []0 MemoryMultiplier declared as type:<class 'float'> value:3
+2024-01-29 15:00:37,359 [DEBUG] sarracenia.config add_option []0 logEvents declared as type:<class 'set'> value:{'after_work', 'on_housekeeping', 'after_accept', 'after_post'}
+2024-01-29 15:00:37,359 [DEBUG] sarracenia.config add_option []0 logMessageDump declared as type:<class 'bool'> value:False
+2024-01-29 15:00:37,359 [INFO] sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_work', 'on_housekeeping', 'after_accept', 'after_post'},  logMessageDump: False
+2024-01-29 15:00:37,360 [DEBUG] sarracenia.config check_undeclared_options missing defaults: {'post_exchangeSplit', 'follow_symlinks', 'topic', 'post_exchange', 'reconnect', 'sendTo', 'pollUrl', 'logMessageDump', 'MemoryBaseLineFile', 'exchange_suffix', 'force_polling', 'exchangeSplit', 'post_topic', 'save', 'inplace', 'retry_driver', 'post_on_start', 'header', 'blocksize', 'restore', 'MemoryMultiplier', 'report_exchange', 'cluster', 'nodupe_basis', 'post_exchangeSuffix', 'identity', 'MemoryMax', 'count', 'notify_only', 'feeder', 'realpathFilter'}
+2024-01-29 15:00:37,360 [INFO] sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2024-01-29 15:00:37,360 [INFO] sarracenia.flow run pid: 3567801 subscribe/flow_demo instance: 0
+2024-01-29 15:00:37,448 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']
+2024-01-29 15:00:37,493 [DEBUG] amqp __init__ using channel_id: 1
+2024-01-29 15:00:37,514 [DEBUG] amqp _on_open_ok Channel open
+2024-01-29 15:00:37,514 [DEBUG] amqp __init__ using channel_id: 2
+2024-01-29 15:00:37,535 [DEBUG] amqp _on_open_ok Channel open
+2024-01-29 15:00:37,634 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']
+2024-01-29 15:00:37,681 [DEBUG] amqp __init__ using channel_id: 1
+2024-01-29 15:00:37,699 [DEBUG] amqp _on_open_ok Channel open
+2024-01-29 15:00:37,699 [DEBUG] amqp __init__ using channel_id: 2
+2024-01-29 15:00:37,730 [DEBUG] amqp _on_open_ok Channel open
+2024-01-29 15:00:37,749 [INFO] sarracenia.moth.amqp _queueDeclare queue declared q_anonymous.subscriber_test2 (as: amqps://anonymous@hpfx.collab.science.gc.ca), (messages waiting: 50000)
+2024-01-29 15:00:37,749 [INFO] sarracenia.moth.amqp getSetup binding q_anonymous.subscriber_test2 with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2024-01-29 15:00:37,765 [DEBUG] sarracenia.moth.amqp getSetup getSetup ... Done!
+2024-01-29 15:00:37,786 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_f2884f4dfeb89a44ec2ccbcc1c154702:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CXCK'], 'identity': {'method': 'md5', 'value': 'sZvG3KgpfENZc15YSMHvbQ=='}, 'size': 9326, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}
+2024-01-29 15:00:37,787 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.01 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml
+2024-01-29 15:00:37,787 [INFO] sarracenia.flow run now active on vip ['AnyAddressIsFine']
+2024-01-29 15:00:37,788 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CXCK
+2024-01-29 15:00:37,789 [DEBUG] sarracenia.config add_option []0 accelWgetCommand declared as type:<class 'str'> value:/usr/bin/wget %s -o - -O %d
+2024-01-29 15:00:37,887 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml
+2024-01-29 15:00:37,912 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_fc8051d6b19291e9b02b8da5f6fc3d2f:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CZKD'], 'identity': {'method': 'md5', 'value': 'yU3e4yc2eVtN+qwiiohaLQ=='}, 'size': 9440, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}
+2024-01-29 15:00:37,912 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.13 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml
+2024-01-29 15:00:37,913 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CZKD
+2024-01-29 15:00:38,000 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml
+2024-01-29 15:00:38,024 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_fe4c49c3c2cc0493ae7473d321d25199:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CVBB'], 'identity': {'method': 'md5', 'value': 'Hwu7CE6asjaQMz7veEmUXA=='}, 'size': 8399, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}
+2024-01-29 15:00:38,025 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.25 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml
+2024-01-29 15:00:38,025 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CVBB
+2024-01-29 15:00:38,114 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml
+2024-01-29 15:00:38,139 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174356', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_8067f0a1a5b4711ab86e481341b26590:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174356', 'source': 'anonymous', 'mtime': '20240129T174357.781', 'atime': '20240129T174357.781', 'pubTime': '20240129T174357.781', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CWLJ'], 'identity': {'method': 'md5', 'value': 'uDrzi9GLNnhEgGvSylHu9g=='}, 'size': 9428, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}
+2024-01-29 15:00:38,140 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8200.36 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml
+2024-01-29 15:00:38,141 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ
+2024-01-29 15:00:38,242 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml
+2024-01-29 15:00:38,262 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_6f203257347d4f090abc1d7557864cb7:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174357.267', 'atime': '20240129T174357.267', 'pubTime': '20240129T174357.267', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CAMS'], 'identity': {'method': 'md5', 'value': 'H/h4jm6MTzMSp1oCeDS1jA=='}, 'size': 9826, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}
+2024-01-29 15:00:38,263 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.00 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml
+2024-01-29 15:00:38,263 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CAMS
+2024-01-29 15:00:38,356 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml
+2024-01-29 15:00:38,357 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2024-01-29 15:00:38,357 [INFO] sarracenia.flow run starting last pass (without gather) through loop for cleanup.
+2024-01-29 15:00:38,358 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2024-01-29 15:00:38,359 [INFO] sarracenia.flow run on_housekeeping pid: 3567801 subscribe/flow_demo instance: 0
+2024-01-29 15:00:38,359 [INFO] sarracenia.flowcb.gather.message on_housekeeping messages: good: 5 bad: 0 bytes: 730 Bytes average: 146 Bytes
+2024-01-29 15:00:38,359 [INFO] sarracenia.flowcb.retry on_housekeeping on_housekeeping
+2024-01-29 15:00:38,360 [INFO] sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping
+2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping No retry in list
+2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000548
+2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping
+2024-01-29 15:00:38,362 [INFO] sarracenia.diskqueue on_housekeeping No retry in list
+2024-01-29 15:00:38,362 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000741
+2024-01-29 15:00:38,363 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.76 system=0.17
+2024-01-29 15:00:38,363 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 790.2 MiB, accumulating count (5 or 5/100 so far) before self-setting threshold
+2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats version: 3.00.51rc6, started: a second ago, last_housekeeping:  1.0 seconds ago
+2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats messages received: 5, accepted: 5, rejected: 0   rate accepted: 100.0% or 5.0 m/s
+2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats files transferred: 5 bytes: 45.3 KiB rate: 45.1 KiB/sec
+2024-01-29 15:00:38,365 [INFO] sarracenia.flowcb.log stats lag: average: 8200.95, maximum: 8201.25
+2024-01-29 15:00:38,366 [INFO] sarracenia.flowcb.log on_housekeeping housekeeping
+2024-01-29 15:00:38,366 [INFO] sarracenia.flow run clean stop from run loop
+2024-01-29 15:00:38,385 [DEBUG] amqp collect Closed channel #1
+2024-01-29 15:00:38,386 [DEBUG] amqp collect Closed channel #2
+2024-01-29 15:00:38,386 [INFO] sarracenia.flowcb.gather.message on_stop closing
+2024-01-29 15:00:38,386 [INFO] sarracenia.flow close flow/close completed cleanly pid: 3567801 subscribe/flow_demo instance: 0
+
+
+
+
+

Conclusion:

+

With the sarracenia.flow class, an async method of operation is supported, it can be customized using flowcb (flow callback) class to introduce specific processing at specific times. It is just like invocation of a single instance from the command line, except all configuration is done within python by setting cfg fields, rather than using the configuration language.

+

What is lost vs. using the command line tool:

+
    +
  • ability to use the configuration language (slightly simpler than assigning values to the cfg object)

  • +
  • easy running of multiple instances,

  • +
  • co-ordinated monitoring of the instances (restarts on failure, and a programmable number of subscribers started per configuration.)

  • +
  • log file management.

  • +
+

The command line tool provides those additional features.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/3_api_flow_demo.ipynb b/Tutorials/3_api_flow_demo.ipynb new file mode 100644 index 000000000..fd2db152e --- /dev/null +++ b/Tutorials/3_api_flow_demo.ipynb @@ -0,0 +1,271 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "informative-conservation", + "metadata": {}, + "source": [ + "# flow API Example\n", + "\n", + "The [sarracenia.flow class](../Reference/code.rst#module-sarracenia.flow) provides built in accept/reject filtering for messages, supports built-in downloading in several protocols, retries on failure, and allows the creation of callbacks, to customize processing.\n", + "\n", + "You need to provide a configuration as an argument when instantiating a subscriber.\n", + "the _sarracenia.config.no_file_config()_ returns an empty configuration without consulting\n", + "any of the sr3 configuration file tree.\n", + "\n", + "After adding the modifications needed to the configuration, the subscriber is then initiated and run." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "weekly-terminology", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "!mkdir /tmp/flow_demo" + ] + }, + { + "cell_type": "markdown", + "id": "exterior-folks", + "metadata": {}, + "source": [ + "make a directory for the files you are going to download.\n", + "the root of the directory tree to must exist." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "aggregate-election", + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "import sarracenia.config\n", + "from sarracenia.flow.subscribe import Subscribe\n", + "import sarracenia.flowcb\n", + "import sarracenia.credentials\n", + "\n", + "cfg = sarracenia.config.no_file_config()\n", + "\n", + "cfg.broker = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')\n", + "cfg.topicPrefix = [ 'v02', 'post']\n", + "cfg.component = 'subscribe'\n", + "cfg.config = 'flow_demo'\n", + "cfg.action = 'start'\n", + "cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]\n", + "cfg.queueName='q_anonymous.subscriber_test2'\n", + "cfg.download=True\n", + "cfg.batch=1\n", + "cfg.messageCountMax=5\n", + "\n", + "# set the instance number for the flow class.\n", + "cfg.no=0\n", + "\n", + "# set other settings based on provided ones, so it is ready for use.\n", + "\n", + "cfg.finalize()\n", + "\n", + "# accept/reject patterns:\n", + "pattern=\".*\"\n", + "# to_match, write_to_dir, DESTFN, regex_to_match, accept=True,mirror,strip, pstrip,flatten\n", + "cfg.masks= [ ( pattern, \"/tmp/flow_demo\", None, re.compile(pattern), True, False, False, False, '/' ) ]\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "legitimate-necessity", + "metadata": {}, + "source": [ + "\n", + "## starters.\n", + "the broker, bindings, and queueName settings are explained in the moth notebook.\n", + "\n", + "## cfg.download\n", + "\n", + "Whether you want the flow to download the files corresponding to the messages.\n", + "If true, then it will download the files.\n", + "\n", + "## cfg.batch\n", + "\n", + "Messages are processed in batches. The number of messages to retrieve per call to newMessages()\n", + "is limited by the _batch_ setting. We set it to 1 here so you can see each file being downloaded immediately when the corresponding message is downloaded. you can leave this blank, and it defaults to 25. Settings are matter of taste and use case.\n", + "\n", + "## cfg.messageCountMax\n", + "\n", + "Normally we just leave this setting at it's default (0) which has no effect on processing.\n", + "for demonstration purposes, we limit the number of messages the subscriber will process with this setting.\n", + "after _messageCountMax_ messages have been received, stop processing.\n", + "\n", + "\n", + "## cfg.masks\n", + "masks are a compiled form of accept/reject directives. a relPath is compared to the regex in the mask.\n", + "If the regex matches, and accept is true, then the message is accepted for further processing.\n", + "If the regex matches, but accept is False, then processing of the message is stopped (the message is rejected.)\n", + "\n", + "masks are a tuple. the meaning can be looked up in the sr3(1) man page.\n", + "\n", + "* pattern_string, the input regular expression string, to be compiled by re routines.\n", + "* directory, where to put the files downloaded (root of the tree, when mirroring)\n", + "* fn, transformation of filename to do. None is the 99% use case.\n", + "* regex, compiled regex version of the pattern_string\n", + "* accept(True/False), if pattern matches then accept message for further processing.\n", + "* mirror(True/False), when downloading build a complete tree to mirror the source, or just dump in directory\n", + "* strip(True/False), modify the relpath by stripping entries from the left.\n", + "* pstrip(True/False), strip entries based on patterm\n", + "* flatten(char ... '/' means do not flatten.) )\n", + "\n", + "## cfg.no, cfg.pid_filename\n", + "\n", + "These settings are needed because they would ordinarily be set by the sarracenia.instance class which is\n", + "normally used to launch flows. They allow setting up of run-time paths for retry_queues, and statefiles,\n", + "to remember settings if need be between runs.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "musical-discrimination", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-01-29 15:00:37,351 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2024-01-29 15:00:37,354 [DEBUG] sarracenia.flowcb.retry __init__ sr_retry __init__\n", + "2024-01-29 15:00:37,354 [DEBUG] sarracenia.config add_option []0 retry_driver declared as type: value:disk\n", + "2024-01-29 15:00:37,355 [DEBUG] sarracenia.diskqueue __init__ work_retry_00 __init__\n", + "2024-01-29 15:00:37,357 [DEBUG] sarracenia.config add_option []0 MemoryMax declared as type: value:0\n", + "2024-01-29 15:00:37,357 [DEBUG] sarracenia.config add_option []0 MemoryBaseLineFile declared as type: value:100\n", + "2024-01-29 15:00:37,358 [DEBUG] sarracenia.config add_option []0 MemoryMultiplier declared as type: value:3\n", + "2024-01-29 15:00:37,359 [DEBUG] sarracenia.config add_option []0 logEvents declared as type: value:{'after_work', 'on_housekeeping', 'after_accept', 'after_post'}\n", + "2024-01-29 15:00:37,359 [DEBUG] sarracenia.config add_option []0 logMessageDump declared as type: value:False\n", + "2024-01-29 15:00:37,359 [INFO] sarracenia.flowcb.log __init__ subscribe initialized with: logEvents: {'after_work', 'on_housekeeping', 'after_accept', 'after_post'}, logMessageDump: False\n", + "2024-01-29 15:00:37,360 [DEBUG] sarracenia.config check_undeclared_options missing defaults: {'post_exchangeSplit', 'follow_symlinks', 'topic', 'post_exchange', 'reconnect', 'sendTo', 'pollUrl', 'logMessageDump', 'MemoryBaseLineFile', 'exchange_suffix', 'force_polling', 'exchangeSplit', 'post_topic', 'save', 'inplace', 'retry_driver', 'post_on_start', 'header', 'blocksize', 'restore', 'MemoryMultiplier', 'report_exchange', 'cluster', 'nodupe_basis', 'post_exchangeSuffix', 'identity', 'MemoryMax', 'count', 'notify_only', 'feeder', 'realpathFilter'}\n", + "2024-01-29 15:00:37,360 [INFO] sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2024-01-29 15:00:37,360 [INFO] sarracenia.flow run pid: 3567801 subscribe/flow_demo instance: 0\n", + "2024-01-29 15:00:37,448 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']\n", + "2024-01-29 15:00:37,493 [DEBUG] amqp __init__ using channel_id: 1\n", + "2024-01-29 15:00:37,514 [DEBUG] amqp _on_open_ok Channel open\n", + "2024-01-29 15:00:37,514 [DEBUG] amqp __init__ using channel_id: 2\n", + "2024-01-29 15:00:37,535 [DEBUG] amqp _on_open_ok Channel open\n", + "2024-01-29 15:00:37,634 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']\n", + "2024-01-29 15:00:37,681 [DEBUG] amqp __init__ using channel_id: 1\n", + "2024-01-29 15:00:37,699 [DEBUG] amqp _on_open_ok Channel open\n", + "2024-01-29 15:00:37,699 [DEBUG] amqp __init__ using channel_id: 2\n", + "2024-01-29 15:00:37,730 [DEBUG] amqp _on_open_ok Channel open\n", + "2024-01-29 15:00:37,749 [INFO] sarracenia.moth.amqp _queueDeclare queue declared q_anonymous.subscriber_test2 (as: amqps://anonymous@hpfx.collab.science.gc.ca), (messages waiting: 50000)\n", + "2024-01-29 15:00:37,749 [INFO] sarracenia.moth.amqp getSetup binding q_anonymous.subscriber_test2 with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)\n", + "2024-01-29 15:00:37,765 [DEBUG] sarracenia.moth.amqp getSetup getSetup ... Done!\n", + "2024-01-29 15:00:37,786 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_f2884f4dfeb89a44ec2ccbcc1c154702:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CXCK'], 'identity': {'method': 'md5', 'value': 'sZvG3KgpfENZc15YSMHvbQ=='}, 'size': 9326, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}\n", + "2024-01-29 15:00:37,787 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.01 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml\n", + "2024-01-29 15:00:37,787 [INFO] sarracenia.flow run now active on vip ['AnyAddressIsFine']\n", + "2024-01-29 15:00:37,788 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CXCK \n", + "2024-01-29 15:00:37,789 [DEBUG] sarracenia.config add_option []0 accelWgetCommand declared as type: value:/usr/bin/wget %s -o - -O %d\n", + "2024-01-29 15:00:37,887 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CXCK/2024-01-29-1743-CXCK-AUTO-minute-swob.xml \n", + "2024-01-29 15:00:37,912 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_fc8051d6b19291e9b02b8da5f6fc3d2f:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CZKD'], 'identity': {'method': 'md5', 'value': 'yU3e4yc2eVtN+qwiiohaLQ=='}, 'size': 9440, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}\n", + "2024-01-29 15:00:37,912 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.13 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml\n", + "2024-01-29 15:00:37,913 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CZKD \n", + "2024-01-29 15:00:38,000 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CZKD/2024-01-29-1743-CZKD-AUTO-minute-swob.xml \n", + "2024-01-29 15:00:38,024 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_fe4c49c3c2cc0493ae7473d321d25199:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174356.779', 'atime': '20240129T174356.779', 'pubTime': '20240129T174356.779', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CVBB'], 'identity': {'method': 'md5', 'value': 'Hwu7CE6asjaQMz7veEmUXA=='}, 'size': 8399, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2024-01-29 15:00:38,025 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.25 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml\n", + "2024-01-29 15:00:38,025 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CVBB \n", + "2024-01-29 15:00:38,114 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CVBB/2024-01-29-1743-CVBB-AUTO-minute-swob.xml \n", + "2024-01-29 15:00:38,139 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174356', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_8067f0a1a5b4711ab86e481341b26590:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174356', 'source': 'anonymous', 'mtime': '20240129T174357.781', 'atime': '20240129T174357.781', 'pubTime': '20240129T174357.781', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CWLJ'], 'identity': {'method': 'md5', 'value': 'uDrzi9GLNnhEgGvSylHu9g=='}, 'size': 9428, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}\n", + "2024-01-29 15:00:38,140 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8200.36 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml\n", + "2024-01-29 15:00:38,141 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ \n", + "2024-01-29 15:00:38,242 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CWLJ/2024-01-29-1743-CWLJ-AUTO-minute-swob.xml \n", + "2024-01-29 15:00:38,262 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'source', '_format', 'exchange', 'subtopic', 'local_offset', 'ack_id'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_6f203257347d4f090abc1d7557864cb7:DMS:WXO_RENAMED_SWOB2:MSC:XML::20240129174355', 'source': 'anonymous', 'mtime': '20240129T174357.267', 'atime': '20240129T174357.267', 'pubTime': '20240129T174357.267', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml', 'subtopic': ['20240129', 'WXO-DD', 'observations', 'swob-ml', '20240129', 'CAMS'], 'identity': {'method': 'md5', 'value': 'H/h4jm6MTzMSp1oCeDS1jA=='}, 'size': 9826, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}\n", + "2024-01-29 15:00:38,263 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 8201.00 ) https://hpfx.collab.science.gc.ca /20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml\n", + "2024-01-29 15:00:38,263 [INFO] sarracenia.flow do_download missing destination directories, makedirs: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CAMS \n", + "2024-01-29 15:00:38,356 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/20240129/WXO-DD/observations/swob-ml/20240129/CAMS/2024-01-29-1743-CAMS-AUTO-minute-swob.xml \n", + "2024-01-29 15:00:38,357 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2024-01-29 15:00:38,357 [INFO] sarracenia.flow run starting last pass (without gather) through loop for cleanup.\n", + "2024-01-29 15:00:38,358 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2024-01-29 15:00:38,359 [INFO] sarracenia.flow run on_housekeeping pid: 3567801 subscribe/flow_demo instance: 0\n", + "2024-01-29 15:00:38,359 [INFO] sarracenia.flowcb.gather.message on_housekeeping messages: good: 5 bad: 0 bytes: 730 Bytes average: 146 Bytes\n", + "2024-01-29 15:00:38,359 [INFO] sarracenia.flowcb.retry on_housekeeping on_housekeeping\n", + "2024-01-29 15:00:38,360 [INFO] sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping\n", + "2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000548\n", + "2024-01-29 15:00:38,361 [INFO] sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping\n", + "2024-01-29 15:00:38,362 [INFO] sarracenia.diskqueue on_housekeeping No retry in list\n", + "2024-01-29 15:00:38,362 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000741\n", + "2024-01-29 15:00:38,363 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.76 system=0.17\n", + "2024-01-29 15:00:38,363 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 790.2 MiB, accumulating count (5 or 5/100 so far) before self-setting threshold\n", + "2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats version: 3.00.51rc6, started: a second ago, last_housekeeping: 1.0 seconds ago \n", + "2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats messages received: 5, accepted: 5, rejected: 0 rate accepted: 100.0% or 5.0 m/s\n", + "2024-01-29 15:00:38,364 [INFO] sarracenia.flowcb.log stats files transferred: 5 bytes: 45.3 KiB rate: 45.1 KiB/sec\n", + "2024-01-29 15:00:38,365 [INFO] sarracenia.flowcb.log stats lag: average: 8200.95, maximum: 8201.25 \n", + "2024-01-29 15:00:38,366 [INFO] sarracenia.flowcb.log on_housekeeping housekeeping\n", + "2024-01-29 15:00:38,366 [INFO] sarracenia.flow run clean stop from run loop\n", + "2024-01-29 15:00:38,385 [DEBUG] amqp collect Closed channel #1\n", + "2024-01-29 15:00:38,386 [DEBUG] amqp collect Closed channel #2\n", + "2024-01-29 15:00:38,386 [INFO] sarracenia.flowcb.gather.message on_stop closing\n", + "2024-01-29 15:00:38,386 [INFO] sarracenia.flow close flow/close completed cleanly pid: 3567801 subscribe/flow_demo instance: 0\n" + ] + } + ], + "source": [ + "subscriber = sarracenia.flow.subscribe.Subscribe( cfg )\n", + "\n", + "subscriber.run()" + ] + }, + { + "cell_type": "markdown", + "id": "passive-biotechnology", + "metadata": {}, + "source": [ + "## Conclusion:\n", + "\n", + "With the sarracenia.flow class, an async method of operation is supported, it can be customized using flowcb (flow callback) class to introduce specific processing at specific times. It is just like invocation of a single instance from the command line, except all configuration is done within python by setting cfg fields, rather than using the configuration language.\n", + "\n", + "What is lost vs. using the command line tool: \n", + "\n", + "* ability to use the configuration language (slightly simpler than assigning values to the cfg object) \n", + "* easy running of multiple instances, \n", + "* co-ordinated monitoring of the instances (restarts on failure, and a programmable number of subscribers started per configuration.) \n", + "* log file management.\n", + "\n", + "The command line tool provides those additional features." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Tutorials/4_api_moth_sub_demo.html b/Tutorials/4_api_moth_sub_demo.html new file mode 100644 index 000000000..dde6c0543 --- /dev/null +++ b/Tutorials/4_api_moth_sub_demo.html @@ -0,0 +1,716 @@ + + + + + + + A first Example using Sarracenia Moth API — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

A first Example using Sarracenia Moth API

+

Sarracenia is a package built to announce the availability of new data, usually as files. We put files on standard servers, making them available via web or sftp, and tell users that they have arrived using messages.

+

Sarracenia uses existing standard message passing protocols, like rabbitmq/AMQP to transport the messages, and in message passing circles, as server that distributes messages is called a broker.

+

We call the combination of a message broker, and a file server (which can be a single server, or a large cluster) a data pump.

+

Assuming you have installed the metpx-sr3 package, either as a debian package, or via pip, One way access announcements to use with sarracenia.moth (Messages Organized by Topic Headers) class, which allows a python program to connect to a Sarracenia server, and start receiving messages that announce resources.

+

The factory to build sarracenia.moth objects requires a dictionary of settings as an argument:

+
    +
  • options: a dictionary of other settings the class might use.

    +
      +
    • ‘broker’: an object (Credential) containing a url pointing to the message server that is announcing products, and other related options.

    • +
    +
  • +
+

The example below builds a call to an broker anyone can access, and just request 5 announcements.

+

You can run it, and then we can discuss a few settings:

+
+
[4]:
+
+
+
+import sarracenia.moth
+import sarracenia.moth.amqp
+import sarracenia.credentials
+
+import time
+import socket
+
+options = sarracenia.moth.default_options
+
+options.update(sarracenia.moth.amqp.default_options)
+
+options['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+options['topicPrefix'] = [ 'v02', 'post' ]
+options['bindings'] = [('xpublic', ['v02', 'post'], ['#'])]
+options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_SomethingHelpfulToYou'
+
+print('options: %s' % options)
+
+
+
+
+
+
+
+
+
+
+options: {'acceptUnmatched': True, 'batch': 25, 'bindings': [('xpublic', ['v02', 'post'], ['#'])], 'broker': <sarracenia.credentials.Credential object at 0x7fd5fa4000a0>, 'dry_run': False, 'exchange': None, 'expire': None, 'inline': False, 'inlineEncoding': 'guess', 'inlineByteMax': 4096, 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', 'logLevel': 'info', 'messageDebugDump': False, 'message_strategy': {'reset': True, 'stubborn': True, 'failure_duration': '5m'}, 'message_ttl': 0, 'topicPrefix': ['v02', 'post'], 'tlsRigour': 'normal', 'auto_delete': False, 'durable': True, 'exchangeDeclare': True, 'prefetch': 25, 'queueName': 'q_anonymous_fractal_SomethingHelpfulToYou', 'queueBind': True, 'queueDeclare': True, 'reset': False, 'subtopic': [], 'vhost': '/'}
+
+
+

The broker setting is an object containing a conventional URL and other options, indicating the messaging protocol to be used to access the upstream server. When you connect to a broker, you need to tell it what messages you are interested in. In Moth, all the brokers we are accessing are expected to use topic hierarchies. You can see them if you successfully ran the example above, there should be in the message print outs a “topic” element in dictionaries. Here is an example of one:

+

v02.post.20210213.WXO-DD.observations.swob-ml.20210213.CTZR

+

This divides into two parts:

+
    +
  • topic_prefix: v02.post

  • +
  • the rest of the topic tree is a reflection of the path to the announced product, relative to a base directory.

  • +
+

In AMQP, there is the concept of “exchanges” which are sort of analogous to television channels… they are groupings of announcements. so to connect to an AMQP broker, one needs to specify:

+
    +
  • exchange: Sarracenia promulgates xpublic as a conventional default.

  • +
  • topic_prefix: decide which version of messages you want to obtain. This server is producing v02 ones.

  • +
  • subtopic: what subset of topic_prefix messages do we want to subscribe to.

  • +
+
+

Bindings

+

The bindings option sets out the three values above. in the example, The bindings are:

+
    +
  • topic_prefix: v02.post (get v02 messages.)

  • +
  • exchange: xpublic (the default one.)

  • +
  • subtopic: # ( an AMQP wildcard meaning everything. )

  • +
+

we connect to the

+

amqp://hpfx.collab.science.gc.ca broker, on the xpublic exchange, and the we will be interested in all messages matching the v02.post.# topic specification… (which is all v02 messages available.)

+
+

subtopic

+

The subtopic here ( # ) is matches everything produced on the server. The wider the subtopic, the more messages have to be sent, and the more processing done. It is better to make it narrower. Taking the example above, if we are interested in swob, a subtopic like:

+
    +
  • *.WXO-DD.observations.swob-ml.#

  • +
+

would match all of the swobs similar to the one above, but avoid sending messages for non-swobs to you.

+
+
+
+

queueName

+

By convention in brokers administered by Sarracenia, users can only create queues that start with q_ followed by their user name. we connected as anonymous, and so q_anonymous must be used. After that, the rest can be whatever you want, but there are a few considerations:

+
    +
  • If you want to start up multiple python processes to share a data feed, they all specify the same queue_name, and they will share the flow of messages. It scales well for a few dozen co-operating downloaders, but does not scale infinitely, do not expect more than 99 or so processes to be able to effectively share a load from a single queue. To scale beyond that with AMQP, multiple selections are better.

  • +
  • if you are going to ask for help from the data pump admins… you are going to need to supply them the name of the queue, and they may need to be able to pick it out of hundreds or thousands that are on the server.

  • +
+
+
+

Messages

+

Different messaging protocols have different storage structures and conventions. the MoTH class returns messages as python dictionaries regardless of what protocol is used to obtain them or, if forwarding them, to transmit them. One can add fields for programmatic use to the messages just by adding elements to the dictionary. If they are only for internal use, then add the name of the dictionary element to the special ‘_deleteOnPost’ key, so that the dictionary element will be dropped when +forwarding the message.

+
+
+

Ack

+

Messages are marked in transit by the broker, and if you do not acknowledge them, the data pump will hold onto them, and eventually re-dispatch them. keeping pending messages in memory will also slow down processing of all messages. One should acknowledge receipt of messages as soon as practicable, but not so soon that you will lose data if the the program is interrupted. In the example, we acknowledge after we have done our work of printing the message.

+
+
[6]:
+
+
+
+h = sarracenia.moth.Moth.subFactory(options)
+
+count=0
+good=0
+while count < 5:
+    m = h.getNewMessage()  #get only one Message
+    if m is not None:
+        print("message %d: %s" % (count,m) )
+        content = m.getContent()
+        print("first 50 bytes of corresponding file: %s" % content[0:50])
+        good +=1
+        h.ack(m)
+    time.sleep(0.1)
+    count += 1
+
+h.cleanup() # remove server-side queue defined by Factory.
+h.close()
+print( f"obtained {good} product announcements")
+
+
+
+
+
+
+
+
+2023-05-27 10:55:22,559 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_SomethingHelpfulToYou (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-27 10:55:22,560 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_SomethingHelpfulToYou with v02.post.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+
+
+
+
+
+
+
+message 0: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145518', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_34df392aeffc9c678011f3fd30193bb6:CMC:REGIONAL:GRIB2:BIN::20230527145518', 'source': 'WXO-DD', 'mtime': '20230527T145520.791', 'atime': '20230527T145520.791', 'pubTime': '20230527T145520.791', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_WIND_ISBL_30_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'U1vVZnatrCeK3bLrXshb2g=='}, 'size': 554100, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}
+first 50 bytes of corresponding file: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x08tt\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1b\x0c\x00\x00\x00\x02\x00\x00\x00A\x03\x00\x00\x0b\xc1\x88\x00\x00\x00'
+message 1: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_752eb4e8803503704990563d84030e67:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145520.292', 'atime': '20230527T145520.292', 'pubTime': '20230527T145520.292', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_HGT_ISBL_250_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'j6bh9dbE4QbJAXEOejw0Tw=='}, 'size': 377005, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}
+first 50 bytes of corresponding file: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x05\xc0\xad\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1b\x0c\x00\x00\x00\x02\x00\x00\x00A\x03\x00\x00\x0b\xc1\x88\x00\x00\x00'
+message 2: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_55f121bb28e822cffb6e61196cd924eb:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145521.260', 'atime': '20230527T145521.260', 'pubTime': '20230527T145521.260', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_RH_ISBL_700_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'V7goy/doL6Gle68s1zoVEA=='}, 'size': 808438, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}
+first 50 bytes of corresponding file: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x0cU\xf6\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1b\x0c\x00\x00\x00\x02\x00\x00\x00A\x03\x00\x00\x0b\xc1\x88\x00\x00\x00'
+message 3: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145518', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_dac300cf33756ba816e030f99fc9dc22:CMC:REGIONAL:GRIB2:BIN::20230527145518', 'source': 'WXO-DD', 'mtime': '20230527T145519.586', 'atime': '20230527T145519.586', 'pubTime': '20230527T145519.586', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_UGRD_ISBL_225_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'MI8XzT1uam5OUf7QlDZ4FA=='}, 'size': 487411, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}
+first 50 bytes of corresponding file: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x07o\xf3\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1b\x0c\x00\x00\x00\x02\x00\x00\x00A\x03\x00\x00\x0b\xc1\x88\x00\x00\x00'
+message 4: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_c5e84748169e0a6dce8f3b884ffdf059:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145520.651', 'atime': '20230527T145520.651', 'pubTime': '20230527T145520.651', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_RH_ISBL_550_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'zukdtksA5I0C5oq/ieiXbQ=='}, 'size': 774394, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}
+
+
+
+
+
+
+
+2023-05-27 10:55:24,022 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_SomethingHelpfulToYou
+
+
+
+
+
+
+
+first 50 bytes of corresponding file: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x0b\xd0\xfa\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1b\x0c\x00\x00\x00\x02\x00\x00\x00A\x03\x00\x00\x0b\xc1\x88\x00\x00\x00'
+obtained 5 product announcements
+
+
+

2nd example … combine baseURL + relPath (talk about retPath) and retrieve data… use newMessages() instead of getNewMessage to show alternate consumption ui. talk about http, and how retrieval will vary depending on the protocol listed in the baseUrl, and can get complicated.

+
+
[7]:
+
+
+
+import urllib.request
+import xml.etree.ElementTree as ET
+
+
+options['bindings'] = [('xpublic', [ 'v02', 'post'], \
+        [ '*', 'WXO-DD', 'observations', 'swob-ml', '#'] )]
+
+h = sarracenia.moth.Moth.subFactory(options)
+
+count=0
+
+while count < 10:
+    messages = h.newMessages()  #get all received Messages, upto options['batch'] of them at a time.
+    for m in messages:
+        dataUrl = m['baseUrl']
+        if 'retPath' in m:
+           dataUrl += m['retPath']
+        else:
+           dataUrl += m['relPath']
+
+        print("url %d: %s" % (count,dataUrl) )
+        with urllib.request.urlopen( dataUrl ) as f:
+            vxml = f.read().decode('utf-8')
+            xmlData = ET.fromstring(vxml)
+
+            stn_name=''
+            tc_id=''
+            lat=''
+            lon=''
+            air_temp=''
+
+            for i in xmlData.iter():
+                name = i.get('name')
+                if name == 'stn_nam' :
+                   stn_name= i.get('value')
+                elif name == 'tc_id' :
+                   tc_id = i.get('value')
+                elif name == 'lat' :
+                   lat =  i.get('value')
+                elif name == 'long' :
+                   lon  = i.get('value')
+                elif name == 'air_temp' :
+                   air_temp = i.get('value')
+
+            print( 'station: %s, tc_id: %s, lat: %s, long: %s, air_temp: %s' %
+                   ( stn_name, tc_id, lat, lon, air_temp  ))
+        h.ack(m)
+        count += 1
+        if count > 10:
+            break
+    time.sleep(1)
+
+h.cleanup() # remove server-side queue defined by Factory.
+h.close()
+print("obtained 10 product temperatures")
+
+
+
+
+
+
+
+
+
+2023-05-27 11:00:20,655 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_SomethingHelpfulToYou (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-27 11:00:20,656 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_SomethingHelpfulToYou with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+
+
+
+
+
+
+
+url 0: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CMTH/2023-05-27-1459-CMTH-AUTO-minute-swob.xml
+station: THETFORD MINES RCS, tc_id: MTH, lat: 46.049134, long: -71.266448, air_temp: 17.3
+url 1: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWPZ/2023-05-27-1459-CWPZ-AUTO-minute-swob.xml
+station: BURNS LAKE DECKER LAKE, tc_id: WPZ, lat: 54.383092, long: -125.95879, air_temp: 7.2
+url 2: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWRK/2023-05-27-1459-CWRK-AUTO-minute-swob.xml
+station: BANCROFT AUTO, tc_id: WRK, lat: 45.071498, long: -77.879936, air_temp: 22.6
+url 3: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWSV/2023-05-27-1459-CWSV-AUTO-minute-swob.xml
+station: BLUE RIVER CS, tc_id: WSV, lat: 52.128917, long: -119.289848, air_temp: 14.7
+url 4: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWSL/2023-05-27-1459-CWSL-AUTO-minute-swob.xml
+station: SALMON ARM CS, tc_id: WSL, lat: 50.703, long: -119.290677, air_temp: 15.2
+url 5: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CPRJ/2023-05-27-1459-CPRJ-AUTO-minute-swob.xml
+station: YORKTON RCS, tc_id: PRJ, lat: 51.260003, long: -102.461318, air_temp: 14.1
+url 6: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWFE/2023-05-27-1459-CWFE-AUTO-minute-swob.xml
+station: ELK ISLAND NAT PARK, tc_id: WFE, lat: 53.682619, long: -112.868105, air_temp: 15.6
+url 7: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWQC/2023-05-27-1459-CWQC-AUTO-minute-swob.xml
+station: PORT ALBERNI AIRPORT (AUT), tc_id: WQC, lat: 49.316698, long: -124.926912, air_temp: 15.1
+url 8: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CVBB/2023-05-27-1459-CVBB-AUTO-minute-swob.xml
+station: DELTA BURNS BOG, tc_id: VBB, lat: 49.125992, long: -123.002436, air_temp: 14.9
+url 9: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWXA/2023-05-27-1459-CWXA-AUTO-minute-swob.xml
+station: BOW VALLEY, tc_id: WXA, lat: 51.0831, long: -115.066, air_temp: 13.3
+url 10: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWJL/2023-05-27-1459-CWJL-AUTO-minute-swob.xml
+station: FORT LIARD, tc_id: WJL, lat: 60.235775, long: -123.472672, air_temp: 14.1
+
+
+
+
+
+
+
+2023-05-27 11:00:23,960 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_SomethingHelpfulToYou
+
+
+
+
+
+
+
+obtained 10 product temperatures
+
+
+
+
+

Downloading Data with Python

+

You can use the urllib python library to download data, and then parse it. In this example, the data is an XML structure per message downloaded and read into memory. Some station data is then printed.

+

This works well with urllib for hyper-test transport protocol resources, but other resources may be announced using other protocols, such as sftp, or ftp. The python code will need to be expanded to deal with other protocols, as well as error conditions, such as temporary failures.

+
+
+

Conclusion

+

Sarracenia.moth.amqp is the lightest-weight way to add consumption of Sarracenia messages to your existing python stack. You explicitly ask for new messages when ready to use them.

+

Things this type of integration does not provide:

+
    +
  • data retrieval: you need your own code to download the corresponding data,

  • +
  • error recovery: if there are transient errors, then you need to build error recovery code (for recovering partial downloads.)

  • +
  • async/event/data driven: a way to say “do this every time you get a file” … define callbacks to be run when a particular event happens, rather than the sequential flow shown above.

  • +
+

The sarracenia.flow class, provides downloads, error recovery, and an asynchronous API using the sarracenia.flowcb (flowCallback) class.

+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/4_api_moth_sub_demo.ipynb b/Tutorials/4_api_moth_sub_demo.ipynb new file mode 100644 index 000000000..35bfe8080 --- /dev/null +++ b/Tutorials/4_api_moth_sub_demo.ipynb @@ -0,0 +1,426 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "annoying-preservation", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# A first Example using Sarracenia Moth API\n", + "\n", + "Sarracenia is a package built to announce the availability of new data, usually as files.\n", + "We put files on standard servers, making them available via web or sftp, and tell\n", + "users that they have arrived using messages. \n", + "\n", + "Sarracenia uses existing standard message passing protocols, like rabbitmq/AMQP to transport the messages,\n", + "and in message passing circles, as server that distributes messages is called a *broker*.\n", + "\n", + "We call the combination of a message broker, and a file server (which can be a single server, or a large cluster) a **data pump**.\n", + "\n", + "Assuming you have installed the **metpx-sr3** package, either as a debian package, or via pip,\n", + "One way access announcements to use with sarracenia.moth (Messages Organized by Topic Headers) class,\n", + "which allows a python program to connect to a Sarracenia server, and start receiving \n", + "messages that announce resources.\n", + "\n", + "The factory to build sarracenia.moth objects requires a dictionary of settings as an argument: \n", + "\n", + "\n", + "* options: a dictionary of other settings the class might use.\n", + "\n", + " * 'broker': an object (Credential) containing a url pointing to the message server that is announcing products, and other related options.\n", + "\n", + " \n", + "\n", + "The example below builds a call to an broker anyone can access, and just request\n", + "5 announcements.\n", + "\n", + "You can run it, and then we can discuss a few settings:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "romance-handy", + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "options: {'acceptUnmatched': True, 'batch': 25, 'bindings': [('xpublic', ['v02', 'post'], ['#'])], 'broker': , 'dry_run': False, 'exchange': None, 'expire': None, 'inline': False, 'inlineEncoding': 'guess', 'inlineByteMax': 4096, 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', 'logLevel': 'info', 'messageDebugDump': False, 'message_strategy': {'reset': True, 'stubborn': True, 'failure_duration': '5m'}, 'message_ttl': 0, 'topicPrefix': ['v02', 'post'], 'tlsRigour': 'normal', 'auto_delete': False, 'durable': True, 'exchangeDeclare': True, 'prefetch': 25, 'queueName': 'q_anonymous_fractal_SomethingHelpfulToYou', 'queueBind': True, 'queueDeclare': True, 'reset': False, 'subtopic': [], 'vhost': '/'}\n" + ] + } + ], + "source": [ + "import sarracenia.moth\n", + "import sarracenia.moth.amqp\n", + "import sarracenia.credentials\n", + "\n", + "import time\n", + "import socket\n", + "\n", + "options = sarracenia.moth.default_options\n", + "\n", + "options.update(sarracenia.moth.amqp.default_options)\n", + "\n", + "options['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')\n", + "options['topicPrefix'] = [ 'v02', 'post' ]\n", + "options['bindings'] = [('xpublic', ['v02', 'post'], ['#'])]\n", + "options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_SomethingHelpfulToYou'\n", + "\n", + "print('options: %s' % options)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "figured-estimate", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "The **broker** setting is an object containing a conventional URL and other options, indicating the messaging protocol to be used to access the upstream server. When you connect to a broker, you need to tell it what messages you are interested in.\n", + "In Moth, all the brokers we are accessing are expected to use topic hierarchies. You can see them if you\n", + "successfully ran the example above, there should be in the message print outs a \"topic\" element in \n", + "dictionaries. Here is an example of one:\n", + "\n", + "__v02.post.20210213.WXO-DD.observations.swob-ml.20210213.CTZR__\n", + "\n", + "This divides into two parts:\n", + "\n", + "* topic_prefix: v02.post\n", + "* the rest of the topic tree is a reflection of the path to the announced product, relative to a base directory.\n", + "\n", + "\n", + "In AMQP, there is the concept of \"exchanges\" which are sort of analogous to television channels... they are groupings of announcements. so to connect to an AMQP broker, one needs to specify: \n", + "\n", + "* exchange: Sarracenia promulgates xpublic as a conventional default.\n", + "* topic_prefix: decide which version of messages you want to obtain. This server is producing v02 ones.\n", + "* subtopic: what subset of topic_prefix messages do we want to subscribe to.\n", + "\n", + "\n", + "## Bindings\n", + "\n", + "The bindings option sets out the three values above. in the example, The bindings are:\n", + "\n", + "* topic_prefix: v02.post (get v02 messages.)\n", + "* exchange: xpublic (the default one.)\n", + "* subtopic: # ( an AMQP wildcard meaning everything. )\n", + "\n", + "we connect to the\n", + "\n", + "amqp://hpfx.collab.science.gc.ca broker, on the *xpublic* exchange, and the we will be interested in all messages matching the v02.post.# topic specification... (which is all v02 messages available.)\n", + "\n", + "### subtopic\n", + "\n", + "The subtopic here ( __#__ ) is matches everything produced on the server. The wider the subtopic, the more messages have to be sent, and the more processing done. It is better to make it narrower. Taking the example above, if we are interested in swob, a subtopic like:\n", + "\n", + "* *.WXO-DD.observations.swob-ml.#\n", + "\n", + "would match all of the swobs similar to the one above, but avoid sending messages for non-swobs to you.\n", + "\n", + "\n", + "## queueName\n", + "\n", + "By convention in brokers administered by Sarracenia, users can only create queues that start with q_ followed by their user name. we connected as anonymous, and so q_anonymous must be used. After that, the rest can be whatever you want, but there are a few considerations:\n", + "\n", + "* If you want to start up multiple python processes to share a data feed, they all specify the same queue_name, and they will share the flow of messages. It scales well for a few dozen co-operating downloaders, but does not scale infinitely, do not expect more than 99 or so processes to be able to effectively share a load from a single queue. To scale beyond that with AMQP, multiple selections are better.\n", + "\n", + "* if you are going to ask for help from the data pump admins... you are going to need to supply them the name of the queue, and they may need to be able to pick it out of hundreds or thousands that are on the server.\n", + "\n", + "\n", + "## Messages\n", + "\n", + "Different messaging protocols have different storage structures and conventions. the MoTH class returns\n", + "messages as python dictionaries regardless of what protocol is used to obtain them or, if forwarding them, to transmit them. One can add fields for programmatic use to the messages just by adding elements to the dictionary.\n", + "If they are only for internal use, then add the name of the dictionary element to the special '\\_deleteOnPost' key, so that the dictionary element will be dropped when forwarding the message.\n", + "\n", + "\n", + "## Ack\n", + "\n", + "Messages are marked in transit by the broker, and if you do not acknowledge them, the data pump will hold onto them, and eventually re-dispatch them. keeping pending messages in memory will also slow down processing of all messages. One should acknowledge receipt of messages as soon as practicable, but not so soon that you will lose data if the the program is interrupted. In the example, we acknowledge after we have done our work of printing the message.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "little-louis", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 10:55:22,559 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_SomethingHelpfulToYou (as: amqps://anonymous@hpfx.collab.science.gc.ca) \n", + "2023-05-27 10:55:22,560 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_SomethingHelpfulToYou with v02.post.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message 0: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145518', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_34df392aeffc9c678011f3fd30193bb6:CMC:REGIONAL:GRIB2:BIN::20230527145518', 'source': 'WXO-DD', 'mtime': '20230527T145520.791', 'atime': '20230527T145520.791', 'pubTime': '20230527T145520.791', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_WIND_ISBL_30_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'U1vVZnatrCeK3bLrXshb2g=='}, 'size': 554100, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}\n", + "first 50 bytes of corresponding file: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x08tt\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1b\\x0c\\x00\\x00\\x00\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x0b\\xc1\\x88\\x00\\x00\\x00'\n", + "message 1: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_752eb4e8803503704990563d84030e67:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145520.292', 'atime': '20230527T145520.292', 'pubTime': '20230527T145520.292', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_HGT_ISBL_250_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'j6bh9dbE4QbJAXEOejw0Tw=='}, 'size': 377005, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}\n", + "first 50 bytes of corresponding file: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x05\\xc0\\xad\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1b\\x0c\\x00\\x00\\x00\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x0b\\xc1\\x88\\x00\\x00\\x00'\n", + "message 2: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_55f121bb28e822cffb6e61196cd924eb:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145521.260', 'atime': '20230527T145521.260', 'pubTime': '20230527T145521.260', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_RH_ISBL_700_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'V7goy/doL6Gle68s1zoVEA=='}, 'size': 808438, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}\n", + "first 50 bytes of corresponding file: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x0cU\\xf6\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1b\\x0c\\x00\\x00\\x00\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x0b\\xc1\\x88\\x00\\x00\\x00'\n", + "message 3: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145518', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_dac300cf33756ba816e030f99fc9dc22:CMC:REGIONAL:GRIB2:BIN::20230527145518', 'source': 'WXO-DD', 'mtime': '20230527T145519.586', 'atime': '20230527T145519.586', 'pubTime': '20230527T145519.586', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_UGRD_ISBL_225_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'MI8XzT1uam5OUf7QlDZ4FA=='}, 'size': 487411, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}\n", + "first 50 bytes of corresponding file: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x07o\\xf3\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1b\\x0c\\x00\\x00\\x00\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x0b\\xc1\\x88\\x00\\x00\\x00'\n", + "message 4: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'exchange', 'ack_id', 'local_offset', '_format'}, 'sundew_extension': 'CMC:REGIONAL:GRIB2:BIN::20230527145519', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_c5e84748169e0a6dce8f3b884ffdf059:CMC:REGIONAL:GRIB2:BIN::20230527145519', 'source': 'WXO-DD', 'mtime': '20230527T145520.651', 'atime': '20230527T145520.651', 'pubTime': '20230527T145520.651', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230527/WXO-DD/model_gem_regional/10km/grib2/12/037/CMC_reg_RH_ISBL_550_ps10km_2023052712_P037.grib2', 'subtopic': ['20230527', 'WXO-DD', 'model_gem_regional', '10km', 'grib2', '12', '037'], 'identity': {'method': 'md5', 'value': 'zukdtksA5I0C5oq/ieiXbQ=='}, 'size': 774394, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 10:55:24,022 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_SomethingHelpfulToYou\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "first 50 bytes of corresponding file: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x0b\\xd0\\xfa\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1b\\x0c\\x00\\x00\\x00\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x0b\\xc1\\x88\\x00\\x00\\x00'\n", + "obtained 5 product announcements\n" + ] + } + ], + "source": [ + "h = sarracenia.moth.Moth.subFactory(options)\n", + "\n", + "count=0\n", + "good=0\n", + "while count < 5:\n", + " m = h.getNewMessage() #get only one Message\n", + " if m is not None:\n", + " print(\"message %d: %s\" % (count,m) )\n", + " content = m.getContent() \n", + " print(\"first 50 bytes of corresponding file: %s\" % content[0:50])\n", + " good +=1 \n", + " h.ack(m)\n", + " time.sleep(0.1)\n", + " count += 1\n", + "\n", + "h.cleanup() # remove server-side queue defined by Factory.\n", + "h.close()\n", + "print( f\"obtained {good} product announcements\")" + ] + }, + { + "cell_type": "markdown", + "id": "other-woman", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "2nd example ... combine baseURL + relPath (talk about retPath) and retrieve data...\n", + "use newMessages() instead of getNewMessage to show alternate consumption ui.\n", + "talk about http, and how retrieval will vary depending on the protocol listed in the baseUrl, and can get\n", + "complicated.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "abroad-sense", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 11:00:20,655 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_SomethingHelpfulToYou (as: amqps://anonymous@hpfx.collab.science.gc.ca) \n", + "2023-05-27 11:00:20,656 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_SomethingHelpfulToYou with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "url 0: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CMTH/2023-05-27-1459-CMTH-AUTO-minute-swob.xml\n", + "station: THETFORD MINES RCS, tc_id: MTH, lat: 46.049134, long: -71.266448, air_temp: 17.3\n", + "url 1: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWPZ/2023-05-27-1459-CWPZ-AUTO-minute-swob.xml\n", + "station: BURNS LAKE DECKER LAKE, tc_id: WPZ, lat: 54.383092, long: -125.95879, air_temp: 7.2\n", + "url 2: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWRK/2023-05-27-1459-CWRK-AUTO-minute-swob.xml\n", + "station: BANCROFT AUTO, tc_id: WRK, lat: 45.071498, long: -77.879936, air_temp: 22.6\n", + "url 3: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWSV/2023-05-27-1459-CWSV-AUTO-minute-swob.xml\n", + "station: BLUE RIVER CS, tc_id: WSV, lat: 52.128917, long: -119.289848, air_temp: 14.7\n", + "url 4: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWSL/2023-05-27-1459-CWSL-AUTO-minute-swob.xml\n", + "station: SALMON ARM CS, tc_id: WSL, lat: 50.703, long: -119.290677, air_temp: 15.2\n", + "url 5: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CPRJ/2023-05-27-1459-CPRJ-AUTO-minute-swob.xml\n", + "station: YORKTON RCS, tc_id: PRJ, lat: 51.260003, long: -102.461318, air_temp: 14.1\n", + "url 6: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWFE/2023-05-27-1459-CWFE-AUTO-minute-swob.xml\n", + "station: ELK ISLAND NAT PARK, tc_id: WFE, lat: 53.682619, long: -112.868105, air_temp: 15.6\n", + "url 7: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWQC/2023-05-27-1459-CWQC-AUTO-minute-swob.xml\n", + "station: PORT ALBERNI AIRPORT (AUT), tc_id: WQC, lat: 49.316698, long: -124.926912, air_temp: 15.1\n", + "url 8: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CVBB/2023-05-27-1459-CVBB-AUTO-minute-swob.xml\n", + "station: DELTA BURNS BOG, tc_id: VBB, lat: 49.125992, long: -123.002436, air_temp: 14.9\n", + "url 9: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWXA/2023-05-27-1459-CWXA-AUTO-minute-swob.xml\n", + "station: BOW VALLEY, tc_id: WXA, lat: 51.0831, long: -115.066, air_temp: 13.3\n", + "url 10: https://hpfx.collab.science.gc.ca/20230527/WXO-DD/observations/swob-ml/20230527/CWJL/2023-05-27-1459-CWJL-AUTO-minute-swob.xml\n", + "station: FORT LIARD, tc_id: WJL, lat: 60.235775, long: -123.472672, air_temp: 14.1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 11:00:23,960 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_SomethingHelpfulToYou\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "obtained 10 product temperatures\n" + ] + } + ], + "source": [ + "import urllib.request\n", + "import xml.etree.ElementTree as ET\n", + "\n", + "\n", + "options['bindings'] = [('xpublic', [ 'v02', 'post'], \\\n", + " [ '*', 'WXO-DD', 'observations', 'swob-ml', '#'] )]\n", + "\n", + "h = sarracenia.moth.Moth.subFactory(options)\n", + "\n", + "count=0\n", + "\n", + "while count < 10:\n", + " messages = h.newMessages() #get all received Messages, upto options['batch'] of them at a time.\n", + " for m in messages:\n", + " dataUrl = m['baseUrl']\n", + " if 'retPath' in m:\n", + " dataUrl += m['retPath']\n", + " else:\n", + " dataUrl += m['relPath']\n", + "\n", + " print(\"url %d: %s\" % (count,dataUrl) )\n", + " with urllib.request.urlopen( dataUrl ) as f:\n", + " vxml = f.read().decode('utf-8')\n", + " xmlData = ET.fromstring(vxml)\n", + "\n", + " stn_name=''\n", + " tc_id=''\n", + " lat=''\n", + " lon=''\n", + " air_temp=''\n", + "\n", + " for i in xmlData.iter():\n", + " name = i.get('name')\n", + " if name == 'stn_nam' :\n", + " stn_name= i.get('value')\n", + " elif name == 'tc_id' :\n", + " tc_id = i.get('value')\n", + " elif name == 'lat' :\n", + " lat = i.get('value')\n", + " elif name == 'long' :\n", + " lon = i.get('value')\n", + " elif name == 'air_temp' :\n", + " air_temp = i.get('value')\n", + "\n", + " print( 'station: %s, tc_id: %s, lat: %s, long: %s, air_temp: %s' %\n", + " ( stn_name, tc_id, lat, lon, air_temp ))\n", + " h.ack(m)\n", + " count += 1\n", + " if count > 10:\n", + " break\n", + " time.sleep(1)\n", + "\n", + "h.cleanup() # remove server-side queue defined by Factory.\n", + "h.close()\n", + "print(\"obtained 10 product temperatures\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "neither-radius", + "metadata": {}, + "source": [ + "## Downloading Data with Python\n", + "\n", + "You can use the urllib python library to download data, and then parse it.\n", + "In this example, the data is an XML structure per message downloaded and read into memory.\n", + "Some station data is then printed.\n", + "\n", + "This works well with urllib for hyper-test transport protocol resources, but other resources may be announced using other protocols, such as sftp, or ftp. The python code will need to be expanded to deal\n", + "with other protocols, as well as error conditions, such as temporary failures.\n" + ] + }, + { + "cell_type": "markdown", + "id": "blank-emphasis", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "[Sarracenia.moth.amqp](../Reference/code.rst#module-sarracenia.moth) is the lightest-weight way to add consumption of Sarracenia messages to your existing python stack. You explicitly ask for new messages when ready to use them. \n", + "\n", + "Things this type of integration does not provide:\n", + "\n", + "* data retrieval: you need your own code to download the corresponding data, \n", + "\n", + "* error recovery: if there are transient errors, then you need to build error recovery code (for recovering partial downloads.)\n", + "\n", + "* async/event/data driven: a way to say \"do this every time you get a file\" ... define callbacks to be run when a particular event happens, rather than the sequential flow shown above.\n", + "\n", + "The sarracenia.flow class, provides downloads, error recovery, and an asynchronous API using the sarracenia.flowcb (flowCallback) class.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "senior-dressing", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Tutorials/5_api_moth_post_demo.html b/Tutorials/5_api_moth_post_demo.html new file mode 100644 index 000000000..8e0b0d436 --- /dev/null +++ b/Tutorials/5_api_moth_post_demo.html @@ -0,0 +1,496 @@ + + + + + + + Posting from Python Code — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Posting from Python Code

+

If you have some python code that is already creating files, and you have no wish to invoke a separate program to post the files, one can access message posting logic easily, given an existing file.

+

This example is for programmatically creating posts for files. It requires write access to a broker, with a user that is allowed to post to an exchange.

+

Need to establish a posting configuration, and then instantiate a posting_engine (an instance than can be used to post messages.)

+
+
[1]:
+
+
+
+import sarracenia
+import sarracenia.moth
+import sarracenia.credentials
+from sarracenia.config import default_config
+
+import os
+import time
+import socket
+
+cfg = default_config()
+cfg.logLevel = 'debug'
+cfg.broker = sarracenia.credentials.Credential('amqp://tfeed:password@localhost')
+cfg.exchange = 'xpublic'
+cfg.post_baseUrl = 'http://host'
+cfg.post_baseDir = '/tmp'
+
+# moth wants a dict as options, rather than sarracenia.config.Config instance.
+posting_engine = sarracenia.moth.Moth.pubFactory( cfg.dictify() )
+
+
+
+
+
+
+
+
+2023-05-27 11:02:29,889 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@fractal', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']
+2023-05-27 11:02:29,890 [DEBUG] amqp __init__ using channel_id: 1
+2023-05-27 11:02:29,890 [DEBUG] amqp _on_open_ok Channel open
+2023-05-27 11:02:29,891 [DEBUG] sarracenia.moth.amqp __putSetup putSetup ... 1. declaring xpublic
+2023-05-27 11:02:29,892 [INFO] sarracenia.moth.amqp __putSetup exchange declared: xpublic (as: amqp://tfeed@localhost)
+2023-05-27 11:02:29,892 [DEBUG] sarracenia.moth.amqp __putSetup putSetup ... Done!
+
+
+

next we create a text file…

+
+
[2]:
+
+
+
+sample_fileName = '/tmp/sample.txt'
+sample_file = open( sample_fileName , 'w')
+sample_file.write(
+"""
+CACN00 CWAO 161800
+PMN
+160,2021,228,1800,1065,100,-6999,20.49,43.63,16.87,16.64,323.5,9.32,27.31,1740,317.8,19.22,1.609,230.7,230.7,230.7,230.7,0,0,0,16.38,15.59,305.
+9,17.8,16.38,19.35,55.66,15.23,14.59,304,16.67,3.844,20.51,18.16,0,0,-6999,-6999,-6999,-6999,-6999,-6999,-6999,-6999,0,0,0,0,0,0,0,0,0,0,0,0,0,
+13.41,13.85,27.07,3473
+"""
+)
+sample_file.close()
+
+
+
+

you give the file name, the config initialized above, and the stat record for the file to the msg_init() function. It will return a message that is ready to feed to the posting_engine.

+
+
[3]:
+
+
+
+# you can supply msg_init with your files, it will build a message appropriate for it.
+m = sarracenia.Message.fromFileData(sample_fileName, cfg, os.stat(sample_fileName) )
+# here is the resulting message.
+print(m)
+
+# feed the message to the posting engine.
+posting_engine.putNewMessage(m)
+
+# when done, should close... cleaner...
+posting_engine.close()
+
+
+
+
+
+
+
+
+2023-05-27 11:02:41,866 [DEBUG] sarracenia __computeIdentity xattr sum too old
+2023-05-27 11:02:41,868 [DEBUG] sarracenia.moth.amqp putNewMessage published body: {"pubTime": "20230527T150241.865911961", "relPath": "sample.txt", "baseUrl": "http://host", "mode": "664", "size": 335, "mtime": "20230527T150237.927556038", "atime": "20230525T031938.0635721684", "identity": {"method": "sha512", "value": "w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw=="}} headers: {} to xpublic under: v03
+2023-05-27 11:02:41,868 [DEBUG] amqp collect Closed channel #1
+
+
+
+
+
+
+
+{'_format': 'v03', '_deleteOnPost': {'local_offset', 'new_baseUrl', 'new_dir', 'new_subtopic', 'new_file', 'subtopic', '_format', 'exchange', 'new_relPath', 'post_format'}, 'exchange': 'xpublic', 'local_offset': 0, 'pubTime': '20230527T150241.865911961', 'new_dir': '/tmp', 'new_file': 'sample.txt', 'post_format': 'v03', 'new_baseUrl': 'http://host', 'new_relPath': 'sample.txt', 'new_subtopic': [], 'relPath': 'sample.txt', 'subtopic': [], 'baseUrl': 'http://host', 'mode': '664', 'size': 335, 'mtime': '20230527T150237.927556038', 'atime': '20230525T031938.0635721684', 'identity': {'method': 'sha512', 'value': 'w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw=='}}
+
+
+

One can post as many messages as needed with putNewMessage, when finished with posting files, to shut down the connection with the broker cleanly, please close the posting_engine.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/5_api_moth_post_demo.ipynb b/Tutorials/5_api_moth_post_demo.ipynb new file mode 100644 index 000000000..bd5fc1dda --- /dev/null +++ b/Tutorials/5_api_moth_post_demo.ipynb @@ -0,0 +1,166 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "absent-economy", + "metadata": {}, + "source": [ + "# Posting from Python Code\n", + "\n", + "If you have some python code that is already creating files, and\n", + "you have no wish to invoke a separate program to post the files,\n", + "one can access message posting logic easily, given an existing file.\n", + "\n", + "This example is for programmatically creating posts for files.\n", + "It requires write access to a broker, with a user that is allowed\n", + "to post to an exchange. \n", + "\n", + "Need to establish a posting configuration, and then instantiate a posting_engine (an instance than can be used to post messages.)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "prescribed-mortgage", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 11:02:29,889 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@fractal', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'PLAIN', b'AMQPLAIN'], locales: ['en_US']\n", + "2023-05-27 11:02:29,890 [DEBUG] amqp __init__ using channel_id: 1\n", + "2023-05-27 11:02:29,890 [DEBUG] amqp _on_open_ok Channel open\n", + "2023-05-27 11:02:29,891 [DEBUG] sarracenia.moth.amqp __putSetup putSetup ... 1. declaring xpublic\n", + "2023-05-27 11:02:29,892 [INFO] sarracenia.moth.amqp __putSetup exchange declared: xpublic (as: amqp://tfeed@localhost)\n", + "2023-05-27 11:02:29,892 [DEBUG] sarracenia.moth.amqp __putSetup putSetup ... Done!\n" + ] + } + ], + "source": [ + "import sarracenia\n", + "import sarracenia.moth\n", + "import sarracenia.credentials\n", + "from sarracenia.config import default_config\n", + "\n", + "import os\n", + "import time\n", + "import socket\n", + "\n", + "cfg = default_config()\n", + "cfg.logLevel = 'debug'\n", + "cfg.broker = sarracenia.credentials.Credential('amqp://tfeed:password@localhost')\n", + "cfg.exchange = 'xpublic'\n", + "cfg.post_baseUrl = 'http://host'\n", + "cfg.post_baseDir = '/tmp'\n", + "\n", + "# moth wants a dict as options, rather than sarracenia.config.Config instance.\n", + "posting_engine = sarracenia.moth.Moth.pubFactory( cfg.dictify() )" + ] + }, + { + "cell_type": "markdown", + "id": "pharmaceutical-airport", + "metadata": {}, + "source": [ + "next we create a text file... " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "early-background", + "metadata": {}, + "outputs": [], + "source": [ + "sample_fileName = '/tmp/sample.txt'\n", + "sample_file = open( sample_fileName , 'w')\n", + "sample_file.write(\n", + "\"\"\"\n", + "CACN00 CWAO 161800\n", + "PMN\n", + "160,2021,228,1800,1065,100,-6999,20.49,43.63,16.87,16.64,323.5,9.32,27.31,1740,317.8,19.22,1.609,230.7,230.7,230.7,230.7,0,0,0,16.38,15.59,305.\n", + "9,17.8,16.38,19.35,55.66,15.23,14.59,304,16.67,3.844,20.51,18.16,0,0,-6999,-6999,-6999,-6999,-6999,-6999,-6999,-6999,0,0,0,0,0,0,0,0,0,0,0,0,0,\n", + "13.41,13.85,27.07,3473\n", + "\"\"\"\n", + ")\n", + "sample_file.close()" + ] + }, + { + "cell_type": "markdown", + "id": "affected-oxygen", + "metadata": {}, + "source": [ + "you give the file name, the config initialized above, and the stat record for the file to the msg_init() function. It will return a message that is ready to feed to the posting_engine." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "precise-delivery", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-27 11:02:41,866 [DEBUG] sarracenia __computeIdentity xattr sum too old\n", + "2023-05-27 11:02:41,868 [DEBUG] sarracenia.moth.amqp putNewMessage published body: {\"pubTime\": \"20230527T150241.865911961\", \"relPath\": \"sample.txt\", \"baseUrl\": \"http://host\", \"mode\": \"664\", \"size\": 335, \"mtime\": \"20230527T150237.927556038\", \"atime\": \"20230525T031938.0635721684\", \"identity\": {\"method\": \"sha512\", \"value\": \"w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw==\"}} headers: {} to xpublic under: v03 \n", + "2023-05-27 11:02:41,868 [DEBUG] amqp collect Closed channel #1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'_format': 'v03', '_deleteOnPost': {'local_offset', 'new_baseUrl', 'new_dir', 'new_subtopic', 'new_file', 'subtopic', '_format', 'exchange', 'new_relPath', 'post_format'}, 'exchange': 'xpublic', 'local_offset': 0, 'pubTime': '20230527T150241.865911961', 'new_dir': '/tmp', 'new_file': 'sample.txt', 'post_format': 'v03', 'new_baseUrl': 'http://host', 'new_relPath': 'sample.txt', 'new_subtopic': [], 'relPath': 'sample.txt', 'subtopic': [], 'baseUrl': 'http://host', 'mode': '664', 'size': 335, 'mtime': '20230527T150237.927556038', 'atime': '20230525T031938.0635721684', 'identity': {'method': 'sha512', 'value': 'w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw=='}}\n" + ] + } + ], + "source": [ + "# you can supply msg_init with your files, it will build a message appropriate for it.\n", + "m = sarracenia.Message.fromFileData(sample_fileName, cfg, os.stat(sample_fileName) )\n", + "# here is the resulting message.\n", + "print(m)\n", + "\n", + "# feed the message to the posting engine.\n", + "posting_engine.putNewMessage(m)\n", + "\n", + "# when done, should close... cleaner...\n", + "posting_engine.close() " + ] + }, + { + "cell_type": "markdown", + "id": "cleared-worst", + "metadata": {}, + "source": [ + "One can post as many messages as needed with putNewMessage, when finished with posting files, to shut down the connection with the broker cleanly, please close the posting_engine." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/Tutorials/Install.html b/Tutorials/Install.html new file mode 100644 index 000000000..f5d2268c9 --- /dev/null +++ b/Tutorials/Install.html @@ -0,0 +1,506 @@ + + + + + + + MetPX-Sarracenia Installation — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

MetPX-Sarracenia Installation

+
+

Revision Record

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+
+

Do you already have it?

+

If sarracenia is already installed you may invoke it like with:

+
fractal% sr3 status
+status:
+Component/Config                         State        Run  Miss   Exp Retry
+----------------                         -----        ---  ----   --- -----
+      total running configs:   0 ( processes: 0 missing: 0 stray: 0 )
+fractal%
+
+
+

Sarracenia can be installed system-wide or only for one user. For use by a single +user installation, the python #PIP method should work, +giving access to sr3 any all libraries needed for programmatic access.

+

For operational use, administrative access may be needed for package installation, +and integration with systemd. Regardless of how it is installed, some periodic +processing (on linux usually known as cron jobs) may also need to be configured.

+

Some environments provide sarracenia, but the installation can be incomplete, in +that, some environments do not provide for libraries that sarracenia depends on. +To know what features your installation has, use the features command:

+
fractal% sr3 features
+
+Status:    feature:   python imports:      Description:
+Installed  amqp       amqp                 can connect to rabbitmq brokers
+Installed  appdirs    appdirs              place configuration and state files appropriately for platform (windows/mac/linux)
+Installed  filetypes  magic                able to set content headers
+Installed  ftppoll    dateparser,pytz      able to poll with ftp
+Installed  humanize   humanize             humans numbers that are easier to read.
+Absent     mqtt       paho.mqtt.client     cannot connect to mqtt brokers
+Installed  redis      redis,redis_lock     can use redis implementations of retry and nodupe
+Installed  sftp       paramiko             can use sftp or ssh based services
+Installed  vip        netifaces            able to use the vip option for high availability clustering
+Installed  watch      watchdog             watch directories
+Installed  xattr      xattr                on linux, will store file metadata in extended attributes
+MISSING    clamd      pyclamd              cannot use clamd to av scan files transferred
+
+ state dir: /home/peter/.cache/sr3
+ config dir: /home/peter/.config/sr3
+
+fractal%
+
+
+

Each feature is explained, and the status is indicated. Note that plugins +can also declare the extra libraries they need. Each feature depends on the python imports +listed in the third column. Making those libraries available to the python environment +will activate the given feature.

+

For example, if “paramiko” is installed, support for SFTP polls and transfers will start +working.

+
+
+

Client Installation

+

The package is built for python version 3.6 or higher. On systems where +they are available, debian packages are recommended. These can be obtained from the +launchpad repository. If you cannot use debian packages, then consider pip packages +avialable from PyPI. In both cases, the other python packages (or dependencies) needed +will be installed by the package manager automatically.

+

Note that in some cases, the operating system does not provide all the required +functionality for all features, so one can complement with pip packages, which +can be installed system-wide, within a user’s environment or even within +venv. as long as sr3 features reports the feature as available, it will +be used.

+ +
+

Redhat/Suse Distros (rpm based)

+

Python distutils on redhat package manager based distributions does not handle dependencies +with the current packaging, so one needs to manually install them. +For example, on fedora 28 mandatories:

+
$ sudo dnf install python3-appdirs
+$ sudo dnf install python3-humanize
+$ sudo dnf install python3-psutil
+$ sudo dnf install python3-watchdog
+$ sudo dnf install python3-paramiko
+$ sudo dnf install python3-xattr
+
+
+

Optional ones:

+
$ sudo dnf install python3-amqp      # optionally support rabbitmq brokers
+$ sudo dnf install python3-file-magic      # optionally support content-type header in messages.
+$ sudo dnf install python3-netifaces # optionally support vip directive for HA.
+$ sudo dnf install python3-paho-mqtt # optionally support mqtt brokers
+
+$ sudo dnf install python3-setuptools # needed to build rpm package.
+
+
+

If packages are not available, the one can substitute by using python install package (pip/pip3)

+

Once the dependencies are in place, one can build an RPM file using setuptools:

+
$ git clone https://github.com/MetPX/sarracenia
+$ cd sarracenia
+
+$ python3 setup.py bdist_rpm
+$ sudo rpm -i dist/*.noarch.rpm
+
+
+

This procedure installs only the python application (not the C one.) +No man pages nor other documentation is installed either.

+

The RPM file does not include systemd integration, which must currently be taken +care of manually:

+
groupadd sarra
+useradd -g sarra sarra
+wget  https://github.com/MetPX/sarracenia/blob/development/debian/metpx-sr3.service
+cp metpx-sr3.service /lib/systemd/system
+cp metpx-sr3.service /etc/systemd/system
+
+
+

After this it can be enabled as per any other systemd unit.

+

(yes this is sad, more information here: https://github.com/MetPX/sarracenia/issues/863 )

+
+
+

PIP

+

On Windows or Linux distributions where system packages are not +available or other special cases, such as if using python in virtual env, where +it is more practical to install the package using pip (python install package) +from http://pypi.python.org/.

+

It is straightforward to do that just the essentials:

+
$ pip install metpx-sr3
+
+
+

one could also add the extras:

+
$ pip install metpx-sr3[amqp,mqtt,vip,ftppoll,filetype]
+
+
+

for all the extras, there is a shortcut:

+
$ pip install metpx-sr3[all]
+
+
+

and to upgrade after the initial installation:

+
$ pip install metpx-sr3
+
+
+
    +
  • To install server-wide on a linux server, prefix with sudo

  • +
+

NOTE:

+
* On many systems where both pythons 2 and 3 are installed, you may need to specify pip3 rather than pip.
+
+* on Windows, in order to get the filetype feature working, one will need to manually *pip install python-magic-bin*
+  see here for details: https://pypi.org/project/python-magic/
+
+
+
+
+

System Startup and Shutdown

+

If the intent is to implement a Data Pump, that is a server with a role in doing +large amounts of data transfers, then the convention is to create an sarra application +user, and arrange for it to be started on boot, and stopped on shutdown.

+

When Sarracenia is installed using a debian package:

+
    +
  • the SystemD unit file is installed in the right place.

  • +
  • the sarra user is created,

  • +
+

If installing using python3 (pip) methods, then this file should be installed:

+
+
+

in the correct location. It can be installed in:

+
/lib/systemd/system/metpx-sr3.service
+
+
+

once installed, it can be activated in the normal way. It expected a sarra user +to exist, which might be created like so:

+
groupadd sarra
+useradd --system --create-home sarra
+
+
+

Directories should be made read/write for sara. The preferences will go in +~sarra/.config, and the state files will be in ~sarra/.cache, and the +periodic processing (see next session) also be implemented.

+
+
+

Periodic Processing/Cron Jobs

+

Regardless of how it is installed, Additional periodic processing may be necessary:

+
+
    +
  • to run sr3 sanity to ensure that appropriate processes are running.

  • +
  • to clean up old directories and avoid filling file systems.

  • +
+
+

examples:

+
# kill off stray process, or restart ones that might have died.
+# avoiding the top of the hour or the bottom.
+7,14,21,28,35,42,49,56 * * * sr3 sanity
+# example directory cleaning jobs, script is included in examples/ subdirectory.
+17 5,11,17,23 * * *    IPALIAS='192.168.1.27';RESULT=`/sbin/ip addr show | grep $IPALIAS|wc|awk '{print $1}'`; if [ $RESULT -eq 1 ]; then tools/old_hour_dirs.py 6 /Projects/web_root ; fi
+
+
+
+
+

Windows

+

On Windows, there are 2 (other) possible options:

+
+
Without Python

Download Sarracenia installer file from here, +execute it and follow the instructions. +Don’t forget to add Sarracenia’s Python directory to your PATH.

+
+
With Anaconda

Create your environment with the file suggested by this repository. +Executing that command from the Anaconda Prompt should install everything:

+
$ conda env create -f sarracenia_env.yml
+
+
+
+
+

See Windows user manual for more information on how to run Sarracenia on Windows.

+
+
+

Packages

+

Debian packages and python wheels can be downloaded directly +from: launchpad

+
+
+
+

Source

+

Source code for each module is available https://github.com/MetPX:

+
$ git clone https://github.com/MetPX/sarracenia sarracenia
+$ cd sarracenia
+
+
+

Development happens on the master branch. One probably wants real release, +so run git tag, and checkout the last one (the latest stable release):

+
$ git tag
+  .
+  .
+  .
+  v3.00.48
+  v3.00.49
+$ git checkout v3.00.49
+$ python3 -m build --no-isolation
+$ pip3 install dist/metpx_sarracenia-2.18.5b4-py3-none-any.whl
+
+
+
+
+

Sarrac

+

The C client is available in prebuilt binaries in the launchpad repositories alongside the python packages:

+
$ sudo add-apt-repository ppa:ssc-hpc-chp-spc/metpx
+$ sudo apt-get update
+$ sudo apt-get install metpx-sr3c
+
+
+

For any recent ubuntu version. The librabbitmq-0.8.0 has been backported in the PPA. +sarrac’s dependency. For other architectures or distributions, one can build from source:

+
$ git clone https://github.com/MetPX/sarrac
+
+
+

on any linux system, as long as librabbitmq dependency is satisfied. Note that the package does +not build or run on non-linux systems.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/Setup_a_local_subscriber.html b/Tutorials/Setup_a_local_subscriber.html new file mode 100644 index 000000000..8f0643771 --- /dev/null +++ b/Tutorials/Setup_a_local_subscriber.html @@ -0,0 +1,247 @@ + + + + + + + Server Admin: A Local Subscriber — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Server Admin: A Local Subscriber

+

This example goes over how to build a local pump, with a local broker, +subscribe to the swob files from the Environment Canada Weather office, +and republish them locally.

+
$ sudo apt install rabbitmq-server
+$ sudo rabbitmqctl list_users
+  Listing users ...
+  user    tags
+  guest   [administrator]
+
+$ sudo rabbitmqctl add_user 'bob'
+  Adding user "bob" ...
+  Password: robert
+
+$ sudo rabbitmqctl list_vhosts
+  Listing vhosts ...
+  name
+  /
+
+
+

Set user permissions for vhost for bob’s configure:read:write:

+
$ sudo rabbitmqctl set_permissions -p "/" "bob" ".*" ".*" ".*"
+  Settting permissions for user "bob" in vhost "/" ...
+
+$ sudo rabbitmqctl set_user_tags bob management
+  Setting tags for user "bob" to [management] ...
+
+$ sudo rabbitmq-plugins enable rabbitmq_management
+$ systemctl restart rabbitmq-server
+
+
+

For more on the different kinds of user tags, see rabbitmq access and permissions. +Open http://localhost:15672/ in a web browser. +Log in with the username/password created above. +Click the Queues tab to monitor the progress from the broker’s perspective. +Back in terminal:

+
$ mkdir -p .config/sarra/subscribe
+$ vi .config/sarra/subscribe/test-subscribe.conf
+  broker amqp://bob:robert@localhost/
+  exchange xs_bob
+  directory /tmp/sarra/output
+  accept .*
+
+
+

Setup the bits that post changes to the exchange:

+
$ mkdir -p .config/sarra/watch
+$ vi $_/test-watch.conf
+  post_broker amqp://bob:robert@localhost/
+  post_exchange xs_bob
+  path /tmp/sarra/input/
+  events modify,create
+
+$ mkdir -p /tmp/sarra/{in,out}put
+$ sr3 start
+$ sr3 log watch/test-watch
+
+
+

–> All reporting normal.:

+
$ sr3 log subscribe/test-subscribe
+  .
+  .
+  2020-08-20 16:29:26,111 [ERROR] standard queue name based on:
+    prefix=q_bob
+    component=subscribe
+    exchangeSplit=False
+    no=1
+
+
+

–> Note the line with [ERROR], it was unable to find the queue. +this is because the queue needs to first be created by the watch and since we started the +subscriber and watch at the same time with ‘sr start’ we ran into a small race condition. +This was soon after resolved as the sr_subscribe has a 1 second retry time. +This can be confirmed with the ‘RabbitMQ Queues’ page showing a q_bob.subscribe.test-subscribe. ... queue in the list.:

+
$ touch /tmp/sarra/input/testfile1.txt
+$ ls /tmp/sarra/input/
+  testfile1.txt
+$ ls /tmp/sarra/output/
+    testfile1.txt
+$ sr3 log subscribe/test-subscribe
+  .
+  .
+  2020-08-20 16:29:26,078 [INFO] file_log downloaded to: /tmp/sarra/output/testfile1.txt
+
+$ sr3 log watch/test-watch
+  2020-08-20 16:29:20,612 [INFO] post_log notice=20200820212920.611807823 file:/ /tmp/sarra/input/testfile1.txt headers={'to_clusters':'localhost', 'mtime':'20200820212920.0259232521', 'atime': '20200820212920.0259232521', 'mode': '644', 'parts': '1,0,1,0,0', 'sum':'d,d41d8cd98f00b204e9800998ecf8427e'}
+
+$ touch /tmp/sarra/input/testfile{2..9}.txt
+$ for i in {001..015}; do echo "file #$i" > file$i.txt; done
+$ watch -n 1 ls /tmp/sarra/output/
+
+
+

Now you can watch the files trickle into the output folder, +also watch the ‘RabbitMQ Queues’ page receive and process AMQP messages. +When all is completed you can shut down both the subscriber and watcher with:

+
$ sr3 stop
+  ...
+$ sr3 cleanup subscribe/test-subscribe
+  ...
+
+
+

Now the queue has been deleted from RabbitMQ and all services have been stopped.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/Setup_a_remote_subscriber.html b/Tutorials/Setup_a_remote_subscriber.html new file mode 100644 index 000000000..5201253e4 --- /dev/null +++ b/Tutorials/Setup_a_remote_subscriber.html @@ -0,0 +1,274 @@ + + + + + + + How to setup a Remote Subscriber — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

How to setup a Remote Subscriber

+

This example goes over how to subscribe to the swob files from the Environment Canada Weather office.

+
+

Setup

+

Initialize the credentials storage in the ~/.config/sr3/credentials.conf file:

+
$ sr3 edit credentials.conf
+  amqps://anonymous:anonymous@dd.weather.gc.ca
+
+
+

The format is a complete url on each line (amqps://<user>:<password>@<target.url>). +This credentials.conf file should be private (linux octal permissions: 0600). +.conf files placed in the ~/.config/sr3/subscribe_directory will be automatically found by subscribe, rather than giving the full path.

+

The edit command starts the user’s configured editor on the file to be created, in the correct directory:

+
$ sr3 edit subscribe/swob.conf
+  broker amqps://anonymous@dd.weather.gc.ca
+  subtopic observations.swob-ml.#
+  topicPrefix v02.post
+  directory /tmp/swob_downloads
+  accept .*
+$ mkdir /tmp/swob_downloads
+$ sr3 status subscribe/swob
+  2017-12-14 06:54:54,010 [INFO] subscribe swob 01 is stopped
+
+
+
+

Error

+

Currrently edit is failing if there isn’t a file in the expected location +(it does in fact, not create a file) +See issue #251 for more info or to complain. +In the interim instead use:

+
$ mkdir -p .config/sr3/subscribe
+$ touch $_/swob.conf
+$ sr3 edit swob.conf
+
+
+
+

broker indicates where to connect to get the stream of notifications. +The term broker is taken from AMQP (http://www.amqp.org), the protocol used to transfer the notifications. +The notifications that will be received all have topics that correspond to their URL.

+
+

Note

+

Omitting directory from the config file will write the files in the present working directory. +Given how quickly they arrive, be prepared to clean up.

+
+
+
+

Startup

+

Now start up the newly created subscriber:

+
$ sr3 start swob
+  2015-12-03 06:53:35,268 [INFO] user_config = 0 ../swob.conf
+  2015-12-03 06:53:35,269 [INFO] instances 1
+  2015-12-03 06:53:35,270 [INFO] sr3 subscribe swob 0001 started
+
+
+

Activity can be monitored via log files in ~/.cache/sr3/log/ or with the log command:

+
$ sr3 log swob
+
+  2015-12-03 06:53:35,635 [INFO] Binding queue q_anonymous.subscribe.swob.21096474.62787751 with key v02.post.observations.swob-ml.# to exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+  2015-12-03 17:32:01,834 [INFO] user_config = 1 ../swob.conf
+  2015-12-03 17:32:01,835 [INFO] subscribe start
+  2015-12-03 17:32:01,835 [INFO] subscribe run
+  2015-12-03 17:32:01,835 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+  2015-12-03 17:32:01,835 [INFO] AMQP  input :    exchange(xpublic) topic(v02.post.observations.swob-ml.#)
+  2015-12-03 17:32:01,835 [INFO] AMQP  output:    exchange(xs_anonymous) topic(v02.report.#)
+
+  2015-12-03 17:32:08,191 [INFO] Binding queue q_anonymous.subscribe.swob.21096474.62787751 with key v02.post.observations.swob-ml.# to exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+
+
+

[Ctrl] + [C] to exit watching the logs. +The startup log appears normal, indicating the authentication information was accepted. +Subscribe will get the notification and download the file into the present working directory +(unless otherwise specified in the configuration file).

+
+

A normal download looks like this:

+
2015-12-03 17:32:15,031 [INFO] Received topic   v02.post.observations.swob-ml.20151203.CMED
+2015-12-03 17:32:15,031 [INFO] Received notice  20151203223214.699 http://dd2.weather.gc.ca/observations/swob-ml/20151203/CMED/2015-12-03-2200-CMED-AUTO-swob.xml
+2015-12-03 17:32:15,031 [INFO] Received headers {'filename': '2015-12-03-2200-CMED-AUTO-swob.xml', 'parts': '1,3738,1,0,0', 'sum': 'd,157a9e98406e38a8252eaadf68c0ed60', 'source': 'metpx', 'to_clusters': 'DD,DDI.CMC,DDI.ED M', 'from_cluster': 'DD'}
+2015-12-03 17:32:15,031 [INFO] downloading/copying into ./2015-12-03-2200-CMED-AUTO-swob.xml
+
+
+

Giving all the information contained in the notification. +Here is a failure:

+
2015-12-03 17:32:30,715 [INFO] Downloads: http://dd2.weather.gc.ca/observations/swob-ml/20151203/CXFB/2015-12-03-2200-CXFB-AUTO-swob.xml  into ./2015-12-03-2200-CXFB-AUTO-swob.xml 0-6791
+2015-12-03 17:32:30,786 [ERROR] Download failed http://dd2.weather.gc.ca/observations/swob-ml/20151203/CXFB/2015-12-03-2200-CXFB-AUTO-swob.xml
+2015-12-03 17:32:30,787 [ERROR] Server couldn't fulfill the request. Error code: 404, Not Found
+
+
+

This message is not always a failure as subscribe retries a few times before giving up. +After a few minutes, here is what the download directory looks like:

+
$ ls -al | tail
+  -rw-rw-rw-  1 peter peter   7875 Dec  3 17:36 2015-12-03-2236-CL3D-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7868 Dec  3 17:37 2015-12-03-2236-CL3G-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7022 Dec  3 17:37 2015-12-03-2236-CTRY-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   6876 Dec  3 17:37 2015-12-03-2236-CYPY-AUTO-swob.xml
+  -rw-rw-rw-  1 peter peter   6574 Dec  3 17:36 2015-12-03-2236-CYZP-AUTO-swob.xml
+  -rw-rw-rw-  1 peter peter   7871 Dec  3 17:37 2015-12-03-2237-CL3D-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7873 Dec  3 17:37 2015-12-03-2237-CL3G-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7037 Dec  3 17:37 2015-12-03-2237-CTBF-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7022 Dec  3 17:37 2015-12-03-2237-CTRY-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter 122140 Dec  3 17:38 subscribe_dd_swob_0001.log
+
+
+
+
+

Cleanup

+

To not download more files, stop the subscriber:

+
$ sr3 stop subscribe/swob
+  2015-12-03 17:32:22,219 [INFO] subscribe swob 01 stopped
+
+
+

This however leaves the queue that sr3 start subscribe/swob setup on the broker active, +as to allow a failed subscriber to attempt reconnecting without loosing progress. +That is until the broker times out the queue and removes it. +To tell the broker that we are finished with the queue, tell the subscriber to cleanup:

+
$ sr3 cleanup subscribe/swob
+2015-12-03 17:32:22,008 [INFO] subscribe swob cleanup
+2015-12-03 17:32:22,008 [INFO] AMQP broker(dd.weatheer.gc.ca) user(anonymous) vhost()
+2015-12-03 17:32:22,008 [INFO] Using amqp module (AMQP 0-9-1)
+2015-12-03 17:32:22,008 [INFO] deleting queue q_anonymous.subscribe.swob.21096474.62787751 (anonymous@dd.weather.gc.ca)
+
+
+

Best practice is to clear the queue when done as to lessen the load on the broker.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/Windows.html b/Tutorials/Windows.html new file mode 100644 index 000000000..d6ec1130f --- /dev/null +++ b/Tutorials/Windows.html @@ -0,0 +1,285 @@ + + + + + + + Windows user manual — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Windows user manual

+

This document teaches novice user with Python on Windows how they could easily run Sarracenia in various ways. +The screenshots were taken from Windows Server 2012 R2 Standard edition. Feel free to create issues if +you believe that this document could be enhanced with one (or more) important case(s).

+
+

Running Sarracenia with a Command Prompt

+
+

From the Start Menu:

+

Click on Sarracenia (it will execute sr3.exe restart):

+../_images/start-menu-1.png +

This will pop Sarracenia’s Command Prompt, start Sarracenia processes as instructed by your configurations and show logging information.

+../_images/01_prompt_cmd.png +

Keep this window alive until you are done with Sarracenia. Closing it or typing ctrl-c will kill all Sarracenia processes. You may also want to restart Sarracenia which will stop those processes cleanly.

+
+
+

From a Windows Powershell session:

+

If sr3 is not found at the command line, even after a pip install metpx-sr3, then it may be because +pip does not get the entry points added to the Power Shell Path and you may need to do that manually:

+
PS C:\Users\SilvaP2> sr3
+sr3 : The term 'sr3' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the
+spelling of the name, or if a path was included, verify that the path is correct and try again.
+At line:1 char:1
++ sr3
++ ~~~
+    + CategoryInfo          : ObjectNotFound: (sr3:String) [], CommandNotFoundException
+    + FullyQualifiedErrorId : CommandNotFoundException
+
+PS C:\Users\SilvaP2>
+
+
+

One can validate that the metpx-sr3 package is installed with pip list … it should be in the list. +then look where the entry point scripts are located:

+
PS C:\Users\SilvaP2> pip show -f metpx-sr3
+Name: metpx-sr3
+Version: 3.0.42
+Summary: Subscribe, Acquire, and Re-Advertise products.
+Home-page: https://github.com/MetPX/sarracenia
+Author: Shared Services Canada, Supercomputing, Data Interchange
+Author-email: Peter.Silva@canada.ca
+License: GPLv2
+Location: c:\users\silvap2\appdata\local\packages\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\localcache\local-packages\python310\site-packages
+Editable project location: C:\Users\SilvaP2\Sarracenia\sr3
+Requires: appdirs, humanfriendly, humanize, jsonpickle, paramiko, psutil, python-magic-bin, watchdog
+Required-by:
+Files:
+  ..\Scripts\sr3.exe
+  ..\Scripts\sr3_post.exe
+  ..\Scripts\sr3_tailf.exe
+  __editable__.metpx_sr3-3.0.42.pth
+  __editable___metpx_sr3_3_0_42_finder.py
+  __pycache__\__editable___metpx_sr3_3_0_42_finder.cpython-310.pyc
+  metpx_sr3-3.0.42.dist-info\AUTHORS.rst
+  metpx_sr3-3.0.42.dist-info\INSTALLER
+  metpx_sr3-3.0.42.dist-info\LICENSE.txt
+  metpx_sr3-3.0.42.dist-info\METADATA
+  metpx_sr3-3.0.42.dist-info\RECORD
+  metpx_sr3-3.0.42.dist-info\REQUESTED
+  metpx_sr3-3.0.42.dist-info\WHEEL
+  metpx_sr3-3.0.42.dist-info\direct_url.json
+  metpx_sr3-3.0.42.dist-info\entry_points.txt
+  metpx_sr3-3.0.42.dist-info\top_level.txt
+PS C:\Users\SilvaP2>
+
+
+

From this we learn that the program files are in:

+
c:\users\silvap2\appdata\local\packages\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\localcache\local-packages\python310\site-packages\Scripts
+
+
+

So can add that Scripts directory to the powershell Profile. (On my laptop, it is +in DocumentsWindowsPowerShellMicrosoft.PowerShell_profile.ps1

+
$env:Path += ';c:\users\silvap2\appdata\local\packages\pythonsoftwarefoundation.python.3.10_qbz5n2kfra8p0\localcache\local-packages\python310\Scripts'
+$env:EDITOR = 'code.cmd'
+
+
+

The first line sets the Path so that sr3 entry points will be found. The (optional) second line, sets +the EDITOR variable, so that the sr3 edit command will open the configuration files in VSCode. +One must launch a new Powershell for the settings to take effect.

+

Launch a Powershell powershell session and type this command at the prompt:

+
sr3 restart
+
+
+

This will start Sarracenia processes as instructed by your configurations and show logging information

+../_images/02_prompt_powershell.png +

Keep this Powershell session alive until you are done with Sarracenia. To stop Sarracenia you may type:

+
sr3 stop
+
+
+

This will stop all Sarracenia processes cleanly as would do a restart. Closing this window will also kill all processes.

+
+
+

From Anaconda Prompt:

+

Run this command:

+
activate sr3 && s3r restart
+
+
+
+
+
+

Running Sarracenia without a Command Prompt

+

Here is a case where someone (like a sysadmin) needs to run Sarracenia without a Command Prompt and ensure that the system starts at Windows startup. +The obvious way of doing it would be from the Task Scheduler.

+
+

From the Task Scheduler:

+

Open Task scheduler:

+../_images/03_task_scheduler.png +

Select Create Basic Task… from the action panel on the right:

+../_images/04_create_basic_task.png +

This will launch the Create Basic Task Wizard where you …

+
+

Fill the name:

+../_images/05_fill_the_name.png +

Choose the trigger:

+../_images/06_choose_trigger.png +

Choose the action:

+../_images/07_choose_action.png +

Define the action:

+../_images/08_define_action.png +

Review the task and choose Finish:

+../_images/09_finish.png +
+

Open the Properties dialog and choose Run whether user is logged on or not and Run with highest privileges:

+../_images/10_properties_dialog.png +

The task should now appear in your Task Scheduler Library with the status Ready.

+../_images/12_task_scheduler_ready.png +

Then, you may run it immediately with the run_action button.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/Tutorials/index.html b/Tutorials/index.html new file mode 100644 index 000000000..8ed3b0563 --- /dev/null +++ b/Tutorials/index.html @@ -0,0 +1,213 @@ + + + + + + + Tutorials — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Tutorials

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/_Footer.html b/_Footer.html new file mode 100644 index 000000000..b425f01ff --- /dev/null +++ b/_Footer.html @@ -0,0 +1,131 @@ + + + + + + + <no title> — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Help Us Improve the Docs <3

+

If you’d like to contribute an improvement to the wiki, its source is available on GitHub. +Simply fork the repository and submit a pull request. +Thank you!

+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_Sidebar.html b/_Sidebar.html new file mode 100644 index 000000000..d43060b80 --- /dev/null +++ b/_Sidebar.html @@ -0,0 +1,163 @@ + + + + + + + MetPX Sarracenia Wiki — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

MetPX Sarracenia Wiki

+ + + + + + + + +

Languages

English

Français

+
+

👨‍🎓️ Main

+ +
+
+

🧙 Reference

+ +
+
+

👷 Developers

+ +
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_images/01_prompt_cmd.png b/_images/01_prompt_cmd.png new file mode 100644 index 000000000..787f64eb7 Binary files /dev/null and b/_images/01_prompt_cmd.png differ diff --git a/_images/02_prompt_powershell.png b/_images/02_prompt_powershell.png new file mode 100644 index 000000000..bf64bdb7a Binary files /dev/null and b/_images/02_prompt_powershell.png differ diff --git a/_images/03_task_scheduler.png b/_images/03_task_scheduler.png new file mode 100644 index 000000000..96d901243 Binary files /dev/null and b/_images/03_task_scheduler.png differ diff --git a/_images/04_create_basic_task.png b/_images/04_create_basic_task.png new file mode 100644 index 000000000..e6f4f5cd5 Binary files /dev/null and b/_images/04_create_basic_task.png differ diff --git a/_images/05_fill_the_name.png b/_images/05_fill_the_name.png new file mode 100644 index 000000000..a9effba5a Binary files /dev/null and b/_images/05_fill_the_name.png differ diff --git a/_images/06_choose_trigger.png b/_images/06_choose_trigger.png new file mode 100644 index 000000000..d6b0497be Binary files /dev/null and b/_images/06_choose_trigger.png differ diff --git a/_images/07_choose_action.png b/_images/07_choose_action.png new file mode 100644 index 000000000..1e0af5ab5 Binary files /dev/null and b/_images/07_choose_action.png differ diff --git a/_images/08_define_action.png b/_images/08_define_action.png new file mode 100644 index 000000000..1498d6366 Binary files /dev/null and b/_images/08_define_action.png differ diff --git a/_images/09_finish.png b/_images/09_finish.png new file mode 100644 index 000000000..63f577ca4 Binary files /dev/null and b/_images/09_finish.png differ diff --git a/_images/10_properties_dialog.png b/_images/10_properties_dialog.png new file mode 100644 index 000000000..55fe06c42 Binary files /dev/null and b/_images/10_properties_dialog.png differ diff --git a/_images/12_task_scheduler_ready.png b/_images/12_task_scheduler_ready.png new file mode 100644 index 000000000..3880ed0a6 Binary files /dev/null and b/_images/12_task_scheduler_ready.png differ diff --git a/_images/A2B_message.png b/_images/A2B_message.png new file mode 100644 index 000000000..2c4c124a9 Binary files /dev/null and b/_images/A2B_message.png differ diff --git a/_images/A2B_oldtech.png b/_images/A2B_oldtech.png new file mode 100644 index 000000000..2c00df607 Binary files /dev/null and b/_images/A2B_oldtech.png differ diff --git a/_images/AMQP4Sarra.svg b/_images/AMQP4Sarra.svg new file mode 100644 index 000000000..75958f28a --- /dev/null +++ b/_images/AMQP4Sarra.svg @@ -0,0 +1,264 @@ + + + + + + + + post, + poll, + flow + watch + + repost: + sarra, shovel, + subscribe, winnow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + subscribe + sarra + shovel + + + shared queue + shares load + + + + + Consumer + + + + + + Consumer + + + + + + Consumer + + + + + + + Queue + + + + AMQP Topic is + topic.subtopic.subsub.... + mapped to dir/subdir/subsubdir... + used by exchange to pick queues + to put messages in. + + + - Queues named by Consumers + - Same Name == Shared Queue + - 'durable' queues remain + when consumer disconnects + + + + + + Topic + Exchange + + + + + + + Queue + + + + + + + + + + + + + + Publisher + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bindings + + + + + + + + + + + + + + + + + + + + + + + + + + + + AMQP + + + MetPx-Sarracenia + + + Usage by + + + + + + + + + + + Broker + + + (AMQP Server) + + + Pump + + + + + + + + post + report + + + + + + + + + + + + + + + + + + + + + + + + + + + messages + + + + + + + + + + + + + + + + + + diff --git a/_images/Contribution_Philosophy_AboutTime_12_0.png b/_images/Contribution_Philosophy_AboutTime_12_0.png new file mode 100644 index 000000000..4d4ae0113 Binary files /dev/null and b/_images/Contribution_Philosophy_AboutTime_12_0.png differ diff --git a/_images/Contribution_Philosophy_AboutTime_16_0.png b/_images/Contribution_Philosophy_AboutTime_16_0.png new file mode 100644 index 000000000..04ffa8f50 Binary files /dev/null and b/_images/Contribution_Philosophy_AboutTime_16_0.png differ diff --git a/_images/Contribution_Philosophy_Amdahl_Applied_3_0.png b/_images/Contribution_Philosophy_Amdahl_Applied_3_0.png new file mode 100644 index 000000000..5b6830a9e Binary files /dev/null and b/_images/Contribution_Philosophy_Amdahl_Applied_3_0.png differ diff --git a/_images/Contribution_Philosophy_Amdahl_Applied_5_0.png b/_images/Contribution_Philosophy_Amdahl_Applied_5_0.png new file mode 100644 index 000000000..791391d3a Binary files /dev/null and b/_images/Contribution_Philosophy_Amdahl_Applied_5_0.png differ diff --git a/_images/Contribution_Philosophy_Amdahl_Applied_7_0.png b/_images/Contribution_Philosophy_Amdahl_Applied_7_0.png new file mode 100644 index 000000000..284dae299 Binary files /dev/null and b/_images/Contribution_Philosophy_Amdahl_Applied_7_0.png differ diff --git a/_images/Contribution_Philosophy_CAP_Theorem_Applied_8_0.png b/_images/Contribution_Philosophy_CAP_Theorem_Applied_8_0.png new file mode 100644 index 000000000..f47954137 Binary files /dev/null and b/_images/Contribution_Philosophy_CAP_Theorem_Applied_8_0.png differ diff --git a/_images/Contribution_Philosophy_PDS_Algorithm_1_0.png b/_images/Contribution_Philosophy_PDS_Algorithm_1_0.png new file mode 100644 index 000000000..f01732b51 Binary files /dev/null and b/_images/Contribution_Philosophy_PDS_Algorithm_1_0.png differ diff --git a/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_11_0.png b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_11_0.png new file mode 100644 index 000000000..95d73f3f8 Binary files /dev/null and b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_11_0.png differ diff --git a/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_14_0.png b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_14_0.png new file mode 100644 index 000000000..0d7ffa51c Binary files /dev/null and b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_14_0.png differ diff --git a/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_5_0.png b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_5_0.png new file mode 100644 index 000000000..509303726 Binary files /dev/null and b/_images/Contribution_Philosophy_Sarracenia_Algoritmic_Designs_5_0.png differ diff --git a/_images/Contribution_Philosophy_SundewDesign_7_0.png b/_images/Contribution_Philosophy_SundewDesign_7_0.png new file mode 100644 index 000000000..ee006481c Binary files /dev/null and b/_images/Contribution_Philosophy_SundewDesign_7_0.png differ diff --git a/_images/E-services_data-volume_pas.png b/_images/E-services_data-volume_pas.png new file mode 100644 index 000000000..c366e2a80 Binary files /dev/null and b/_images/E-services_data-volume_pas.png differ diff --git a/_images/Flow_test.svg b/_images/Flow_test.svg new file mode 100644 index 000000000..2a8355dad --- /dev/null +++ b/_images/Flow_test.svg @@ -0,0 +1,513 @@ + + + + + + + + + + + + + + + + + + + dd.weather.gc.ca + + + + + + + + + + + shovel + + + t_dd1 + + + + + + t_dd2 + + + + + + + + + + + xwinnow00 + + + + + xpublic + + + + + + + + + + + + + + + xwinnow01 + + + t00 + + + + t01 + + + + + + winnow + + + + + + + + + + + + xsarra + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + + + + download + + + Sarra + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TESTDOCROOT + + + + + + + + + + + + + + xpublic + + + + + + + + + + + + + + t + + + subscribe + + + + + + + + + + + + + + /downloaded_by_sub_t + + + + + + + + + + exchange + + + + + + + + config + + + Component + + + Legend + + + + + + + http data flow + + + sftp data flow + + + amqp posting flow + + + + watch + + + + + + + tsource2send + + + + xs_tsource + + + sender + + + + sub + + + + + + + /sent_by_tsource2send + + + + + + xs_tsource_output + + + u + + + subscribe + + + + + + + + + /downloaded_by_sub_u + + + + + + + + + + + + + http + + + http + + + sftp + + + sftp + + + + + + + + + + + + + post + + + + + + + test2 + + + + + + + + r + + + subscribe + + + + + + /sent_by_srpost_test2 + + + + + + + + + + + + + file + + + + + + + + + + + xs_tsource_post + + + + + + + + + + + + + + + + + + + po11 + + + + test1 + + + + + + + xs_tsource_po11 + + + + + + + q + + + subscribe + + + /sent_by_srpoll_test1 + + + + + + + + + + + + + + + + + + + + + + + + + + fclean + + + f0x + + + f1x + + + f2x + + + f3x + + + f4x + + + + + + + f5x + + + f6x + + + f7x + + + f9x + + + (Pure Python) Message Flow Test + + + + + + diff --git a/_images/GTS_Routing.jpg b/_images/GTS_Routing.jpg new file mode 100644 index 000000000..8167e5be3 Binary files /dev/null and b/_images/GTS_Routing.jpg differ diff --git a/_images/HPC-XC_High_Availability.png b/_images/HPC-XC_High_Availability.png new file mode 100644 index 000000000..b0c366fa6 Binary files /dev/null and b/_images/HPC-XC_High_Availability.png differ diff --git a/_images/IntelHPCStack.png b/_images/IntelHPCStack.png new file mode 100644 index 000000000..97a6a4e7d Binary files /dev/null and b/_images/IntelHPCStack.png differ diff --git a/_images/OpenStackArch.png b/_images/OpenStackArch.png new file mode 100644 index 000000000..8f7d535b6 Binary files /dev/null and b/_images/OpenStackArch.png differ diff --git a/_images/RADAR_DI_LogicFlow_Current.gif b/_images/RADAR_DI_LogicFlow_Current.gif new file mode 100644 index 000000000..4a10a1a9c Binary files /dev/null and b/_images/RADAR_DI_LogicFlow_Current.gif differ diff --git a/_images/WMO_mesh.png b/_images/WMO_mesh.png new file mode 100644 index 000000000..511e8912a Binary files /dev/null and b/_images/WMO_mesh.png differ diff --git a/_images/amqp_flow_concept.svg b/_images/amqp_flow_concept.svg new file mode 100644 index 000000000..582b1eb24 --- /dev/null +++ b/_images/amqp_flow_concept.svg @@ -0,0 +1,337 @@ + + + + + + + + + + AMQP4Sarra + + + Background + + Sheet.2 + + + + Sheet.3 + post watch sara + + + + postwatchsara + + Sheet.4 + + Sheet.5 + + + + Sheet.6 + + + + + Sheet.7 + + Sheet.8 + + + + Sheet.9 + + + + + Sheet.10 + + Sheet.11 + + + + Sheet.12 + + + + + Sheet.13 + + Sheet.14 + + + + Sheet.15 + + + + + Sheet.16 + + Sheet.17 + + + + Sheet.18 + + + + + Sheet.19 + + Sheet.20 + + + + Sheet.21 + + + + + Sheet.22 + subscribe sarra report + + + + subscribesarrareport + + Sheet.23 + shared queue shares load + + + + shared queueshares load + + Sheet.24 + + Sheet.25 + + + + Sheet.26 + Consumer + + + + Consumer + + + Sheet.27 + + Sheet.28 + + + + Sheet.29 + Consumer + + + + Consumer + + + Sheet.30 + + Sheet.31 + + + + Sheet.32 + Consumer + + + + Consumer + + + Sheet.33 + + Sheet.34 + + + + Sheet.35 + Queue + + + + Queue + + + Sheet.36 + AMQP Topic is topic.subtopic.subsub.... mapped to dir/subdir/... + + + + AMQP Topic is topic.subtopic.subsub....mapped to dir/subdir/subsubdir…used by exchange to pick queuesto put messages in + + Sheet.37 + - Queues named by Consumers - Same Name == Shared Queue - 'du... + + + + - Queues named by Consumers- Same Name == Shared Queue- 'durable' queues remain when consumer disconnects + + Sheet.38 + + Sheet.39 + + + + Sheet.40 + Topic Exchange + + + + TopicExchange + + + Sheet.41 + + Sheet.42 + + + + Sheet.43 + Queue + + + + Queue + + + Sheet.44 + components can produce report messages + + + + componentscan producereport messages + + Sheet.45 + + + + Sheet.46 + Publisher + + + + Publisher + + Sheet.47 + bindings + + + + bindings + + Sheet.48 + + Sheet.49 + AMQP + + + + AMQP + + Sheet.50 + MetPx-Sarracenia + + + + MetPx-Sarracenia + + Sheet.51 + Usage by + + + + Usage by + + + Sheet.53 + + Sheet.54 + Broker + + + + Broker + + Sheet.55 + (AMQP Server) + + + + (AMQP Server) + + Sheet.56 + Pump + + + + Pump + + + Sheet.57 + + Sheet.58 + + + + + Sheet.59 + + + + + Sheet.60 + post report + + + + postreport + + Sheet.61 + messages + + + + messages + + + diff --git a/_images/amqp_notion_de_flux.svg b/_images/amqp_notion_de_flux.svg new file mode 100644 index 000000000..148c8c950 --- /dev/null +++ b/_images/amqp_notion_de_flux.svg @@ -0,0 +1,341 @@ + + + + + + + + + + AMQP4Sarra + + + Background + + Sheet.2 + + + + Sheet.3 + post watch sarra + + + + postwatchsarra + + Sheet.4 + + Sheet.5 + + + + Sheet.6 + + + + + Sheet.7 + + Sheet.8 + + + + Sheet.9 + + + + + Sheet.10 + + Sheet.11 + + + + Sheet.12 + + + + + Sheet.13 + + Sheet.14 + + + + Sheet.15 + + + + + Sheet.16 + + Sheet.17 + + + + Sheet.18 + + + + + Sheet.19 + + Sheet.20 + + + + Sheet.21 + + + + + Sheet.22 + subscribe sarra + + + + subscribesarra + + Sheet.23 + chargement des partages de fil d'attente partagée + + + + chargement des partages de fil d'attente partagée + + Sheet.24 + + Sheet.25 + + + + Sheet.26 + Consumer + + + + Consumer + + + Sheet.27 + + Sheet.28 + + + + Sheet.29 + Consumer + + + + Consumer + + + Sheet.30 + + Sheet.31 + + + + Sheet.32 + Consumer + + + + Consumer + + + Sheet.33 + + Sheet.34 + + + + Sheet.35 + Queue + + + + Queue + + + Sheet.36 + Le thème AMQP est topic.subtopic.subsub.... mappé au dir/subd... + + + + Le thème AMQP est topic.subtopic.subsub.... mappé au dir/subdir/subsubdir utilisé par l'échange pour choisir les fils d'attente dans lesquelles placer les messages + + Sheet.37 + - Fils d'attente nommées par les Consommateurs - Même Nom == ... + + + + - Fils d'attente nommées par les Consommateurs- Même Nom == Fil d'attente Partagée- des fils d'attente durablessubsistent lorsque le consommateur se déconnecte + + Sheet.38 + + Sheet.39 + + + + Sheet.40 + Topic Exchange + + + + TopicExchange + + + Sheet.41 + + Sheet.42 + + + + Sheet.43 + Queue + + + + Queue + + + Sheet.44 + les composants peuvent produire des messages de rapport + + + + les composants peuvent produire des messages de rapport + + Sheet.45 + + + + Sheet.46 + Publisher + + + + Publisher + + Sheet.47 + bindings + + + + bindings + + Sheet.48 + + Sheet.51 + Utilisation AMQP par MetPx-Sarracenia + + + + Utilisation AMQP par MetPx-Sarracenia + + + Sheet.53 + + Sheet.54 + Broker + + + + Broker + + Sheet.55 + (AMQP Serveur) + + + + (AMQP Serveur) + + Sheet.56 + Pump + + + + Pump + + + Sheet.57 + + Sheet.58 + + + + + Sheet.59 + + + + + Sheet.60 + post + + + + post + + Sheet.61 + messages + + + + messages + + + diff --git a/_images/cFlow_test.svg b/_images/cFlow_test.svg new file mode 100644 index 000000000..09dbd7ec9 --- /dev/null +++ b/_images/cFlow_test.svg @@ -0,0 +1,511 @@ + + + + + + + + + + + + + + + Flow Self-Test + + + + + dd.weather.gc.ca + + + + + + + + + + + pelle + + + dd1 + + + + + + dd2 + + + + + + + + + + + xcvan00 + + + + + xpublic + + + + + + + + + + + + + + + xcvan01 + + + t00 + + + + t01 + + + + + + van + + + + + + + + + + + + xcsarra + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + localhost + + + + cdnld + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TESTDOCROOT + + + + + + + + + + + xcpublic + + + + + + + + + + + + + + veille + + + subscribe + + + + + + /cfr + + + + + + + + + + exchange + + + + + + + + config + + + Component + + + Legend + + + + + + + http data flow + + + sftp data flow + + + amqp posting flow + + + cpost + + + + + + + /cfile + + + + + + + + + + + + http + + + file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cfile + + + + + + subscribe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + cfclean + + + + + + f0x + + + f1x + + + f2x + + + f3x + + + + + + + + + f4x + + + f9x + + + + + + + + + + + xcpublic + + + + cfile + + + + + + shim post + + + + + + + + + + + + + + + + + + + + + + + + + + + /posted_by_shim + + + f6x + + + (part of python flow_poster) + + + /posted_by_srpost_test2 + + + diff --git a/_images/dar.png b/_images/dar.png new file mode 100644 index 000000000..076599f27 Binary files /dev/null and b/_images/dar.png differ diff --git a/_images/e-ddsr-components.jpg b/_images/e-ddsr-components.jpg new file mode 100644 index 000000000..a96c19056 Binary files /dev/null and b/_images/e-ddsr-components.jpg differ diff --git a/_images/ftp_proxy_today.svg b/_images/ftp_proxy_today.svg new file mode 100644 index 000000000..72e135789 --- /dev/null +++ b/_images/ftp_proxy_today.svg @@ -0,0 +1,595 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + firewalls likely + with source NAT + therefore PAT. + + + + + + + + + + + + + + + + + + + + + + + + LB-A + + + FW-A + + + FW-B + + + LB-B + + + PXA-A + + + PXA-B + + + + + + + + + Load Balancers map VIP + to real servers... aka NAT + therefore PAT. + + + + diff --git a/_images/gtsstructureL.png b/_images/gtsstructureL.png new file mode 100644 index 000000000..f2cf979ac Binary files /dev/null and b/_images/gtsstructureL.png differ diff --git a/_images/powershell.png b/_images/powershell.png new file mode 100644 index 000000000..6892494b8 Binary files /dev/null and b/_images/powershell.png differ diff --git a/_images/run_action.png b/_images/run_action.png new file mode 100644 index 000000000..0d0680363 Binary files /dev/null and b/_images/run_action.png differ diff --git a/_images/shim_explanation_normal_close.svg b/_images/shim_explanation_normal_close.svg new file mode 100644 index 000000000..0e0db0091 --- /dev/null +++ b/_images/shim_explanation_normal_close.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + Application + + + + + + libssl.so + + + + libc.so + + + + libm.so + + + + + + + kernel api + + + + + + + + + + + + The real close(2)is here. + + + + + + + + + + + + + + + + + close(2) + + + + + + + + + + + + + + + diff --git a/_images/shim_explanation_shim_close.svg b/_images/shim_explanation_shim_close.svg new file mode 100644 index 000000000..2f0bb3ec4 --- /dev/null +++ b/_images/shim_explanation_shim_close.svg @@ -0,0 +1,99 @@ + + + + + + + + + + + + + Application + + + + + + libssl.so + + + + libc.so + + + + libm.so + + + + + + + kernel api + + + + + + + + + + + + The real close(2)is here. + + + + + + + + + + + + + + libsrshim.so + + + + + + LD_PRELOAD="libsrshim.so" + + + + + + + + + + + + + close(2) + + + + + + + "fake" close(2) + calls real one, then post + + + + + + + AMQP message + posted to + rabbitmq broker + + + diff --git a/_images/site-store.jpg b/_images/site-store.jpg new file mode 100644 index 000000000..59889c9f4 Binary files /dev/null and b/_images/site-store.jpg differ diff --git a/_images/sr3_exemple_de_flux.svg b/_images/sr3_exemple_de_flux.svg new file mode 100644 index 000000000..9dd5b4fb8 --- /dev/null +++ b/_images/sr3_exemple_de_flux.svg @@ -0,0 +1,2675 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Translucent + + + + + + + + + + + + + + + + Sheet.313 + + Sheet.314 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sheet.315 + Légende + + Sheet.316 + + + + + + + + + + + + + + + + + + + + + + Légende + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Application server + ddsr + + Sheet.2 + + + + Sheet.3 + + Sheet.4 + + + + + Sheet.5 + + + + + Sheet.6 + + + + Sheet.7 + + + + Sheet.8 + + + + Sheet.9 + + + + + + Sheet.10 + + Sheet.11 + + + + Sheet.12 + + + + Sheet.13 + + + + Sheet.14 + + + + Sheet.15 + + + + Sheet.16 + + + + + + Sheet.17 + + Sheet.18 + + + + + Sheet.19 + + + + + Sheet.20 + + + + Sheet.21 + + + + Sheet.22 + + + + Sheet.23 + + + + + + Sheet.24 + + Sheet.25 + + + + Sheet.26 + + + + Sheet.27 + + + + Sheet.28 + + + + Sheet.29 + + + + Sheet.30 + + + + + + Sheet.31 + + Sheet.32 + + + + + Sheet.33 + + + + + Sheet.34 + + + + Sheet.35 + + + + Sheet.36 + + + + Sheet.37 + + + + + + Sheet.38 + + Sheet.39 + + + + Sheet.40 + + + + Sheet.41 + + + + Sheet.42 + + + + Sheet.43 + + + + Sheet.44 + + + + + + Sheet.45 + + Sheet.46 + + + + + Sheet.47 + + + + + Sheet.48 + + + + Sheet.49 + + + + Sheet.50 + + + + Sheet.51 + + + + + + Sheet.52 + + Sheet.53 + + + + Sheet.54 + + + + Sheet.55 + + + + Sheet.56 + + + + Sheet.57 + + + + Sheet.58 + + + + + + + + ddsr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FTP server + ddi + + Sheet.80 + + + + + Sheet.81 + + + + + Sheet.82 + + + + + Sheet.83 + + + + Sheet.84 + + + + Sheet.85 + + + + Sheet.86 + + + + + + Sheet.87 + + Sheet.88 + + + + Sheet.89 + + + + Sheet.90 + + + + Sheet.91 + + + + Sheet.92 + + + + Sheet.93 + + + + + + + + Sheet.94 + + Sheet.95 + + + + Sheet.96 + + + + Sheet.97 + + + + Sheet.98 + + + + Sheet.99 + + + + Sheet.100 + + Sheet.101 + + + + + Sheet.102 + + + + + + + + ddi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server + urp + + Sheet.104 + + + + + Sheet.105 + + + + + Sheet.106 + + + + + Sheet.107 + + + + Sheet.108 + + + + Sheet.109 + + + + Sheet.110 + + + + + + Sheet.111 + + Sheet.112 + + + + Sheet.113 + + + + Sheet.114 + + + + Sheet.115 + + + + Sheet.116 + + + + Sheet.117 + + + + + + + + Sheet.118 + + Sheet.119 + + + + Sheet.120 + + + + Sheet.121 + + + + Sheet.122 + + + + Sheet.123 + + + + + + + urp + + + + + + + + + + + + + + + + + + + + + + + + + + Firewall + + Sheet.157 + + + + Sheet.158 + + + + Sheet.159 + + + + Sheet.160 + + + + Sheet.161 + + + + + + + + + + + + + + + + + Sheet.162 + + + + + + + + + + Border small.191 + + Sheet.192 + + + + Sheet.193 + Distributeur de données OZ (ddi) + + + + Distributeur de données OZ (ddi) + + + + + + + + Border small.183 + + Sheet.184 + + + + Sheet.188 + Pompe de données (ddsr) + + + + Pompe de données (ddsr) + + + + + + + + Border small.185 + + Sheet.186 + + + + Sheet.187 + Source de données (urp) + + + + Source de données (urp) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server.124 + goes + + Sheet.125 + + + + + Sheet.126 + + + + + Sheet.127 + + + + + Sheet.128 + + + + Sheet.129 + + + + Sheet.130 + + + + Sheet.131 + + + + + + Sheet.132 + + Sheet.133 + + + + Sheet.134 + + + + Sheet.135 + + + + Sheet.136 + + + + Sheet.137 + + + + Sheet.138 + + + + + + + + Sheet.139 + + Sheet.140 + + + + Sheet.141 + + + + Sheet.142 + + + + Sheet.143 + + + + Sheet.144 + + + + + + + goes + + + Process.3 + watch Annoncer les fichiers placés dans un répertoire surveil... + + + + + + + + + + + + watchAnnoncer les fichiers placés dans un répertoire surveillé. + + + + + + + Border small.165 + + Sheet.166 + + + + Sheet.167 + Source de données (goes) + + + + Source de données (goes) + + + Process.169 + sr3_post Annoncer les fichiers pour ingestion. + + + + + + + + + + + + sr3_postAnnoncer les fichiers pour ingestion. + + Process.155 + sarra S'abonner, valider, récupérer et ré-annoncer. + + + + + + + + + + + + sarraS'abonner, valider, récupérer et ré-annoncer. + + Process.170 + sender S'abonner, valider et envoyer à destination. + + + + + + + + + + + + senderS'abonner, valider et envoyer à destination. + + Process.182 + subscribe Obtenez les données de l'échange de données et envo... + + + + + + + + + + + + subscribeObtenez les données de l'échange de données et envoyez une confirmation à l'échange de rapports. + + Dynamic connector.229 + + + + Dynamic connector.230 + + + + Dynamic connector.231 + + + + Dynamic connector.171 + + + + + + + + + Border small.225 + + Sheet.232 + + + + Sheet.233 + Rapports de pompe + + + + Rapports de pompe + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server.234 + + Sheet.235 + + + + + Sheet.236 + + + + + Sheet.238 + + + + + Sheet.239 + + + + Sheet.240 + + + + Sheet.242 + + + + Sheet.243 + + + + + + Sheet.244 + + Sheet.245 + + + + Sheet.246 + + + + Sheet.247 + + + + Sheet.248 + + + + Sheet.249 + + + + Sheet.250 + + + + + + + + Sheet.251 + + Sheet.252 + + + + Sheet.253 + + + + Sheet.254 + + + + Sheet.255 + + + + Sheet.256 + + + + + + Dynamic connector.59 + + + + File + + + + + + + + + + + Sheet.61 + Ex: 201505291700_WMN_PRECIPET_RAIN.gif201505291800_XAM_PRECIP... + + + + Ex: 201505291700_WMN_PRECIPET_RAIN.gif201505291800_XAM_PRECIPET_RAIN.gif + + File.62 + + + + + + + + + + + Sheet.63 + Ex: /GOES-12/* /GOES-13/* + + + + Ex:/GOES-12/* /GOES-13/* + + Sheet.146 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.145 + + + + + + + + + + + Dynamic connector + + + + Sheet.172 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.197 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User.173 + Billy + + Sheet.174 + + + + + Sheet.175 + + + + + Sheet.176 + + + + Sheet.177 + + + + Sheet.178 + + + + Sheet.179 + + + + Sheet.180 + + + + + + Billy + + + Process.224 + shovel Lire le rapport de l'échange de rapports source pour s... + + + + + + + + + + + + shovelLire le rapport de l'échange de rapports source pour savoir quels clients ont obtenu les données. + + + + + + + + + + + + + + + + + + + + User.228 + partenaire + + Sheet.260 + + + + + Sheet.261 + + + + + Sheet.262 + + + + Sheet.263 + + + + Sheet.266 + + + + Sheet.267 + + + + Sheet.268 + + + + + + partenaire + + + Sheet.277 + Auteurs: P.Silva, M.Grenier, J.Hu, T.Kaufmann Actualisé: 2022... + + + + Auteurs: P.Silva, M.Grenier, J.Hu, T.KaufmannActualisé: 2022-10-18 + + Process.219 + shovel Annoncer les rapports. + + + + + + + + + + + + shovelAnnoncer les rapports. + + Process.199 + shovel Recevoir, valider et ingérer. + + + + + + + + + + + + shovelRecevoir, valider et ingérer. + + Process.218 + shovel Propager à travers des grappe. + + + + + + + + + + + + shovelPropager à travers des grappe. + + Dynamic connector.290 + + + + Dynamic connector.276 + + + + Process.264 + sarra S'abonner, valider, récupérer et ré-annoncer. + + + + + + + + + + + + sarraS'abonner, valider, récupérer et ré-annoncer. + + Dynamic connector.269 + + + + Sheet.215 + xr_goes + + + + xr_goes + + Sheet.190 + xs_urp + + + + xs_urp + + Sheet.220 + xpublic + + + + xpublic + + Sheet.221 + xs_goes + + + + xs_goes + + Dynamic connector.222 + + + + Sheet.223 + xpublic + + + + xpublic + + Sheet.237 + xreport + + + + xreport + + Sheet.257 + xr_billy + + + + xr_billy + + Sheet.181 + + + + Sheet.226 + + + + Dynamic connector.227 + + + + Dynamic connector.258 + + + + Dynamic connector.259 + + + + Dynamic connector.270 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FTP server.60 + dd + + Sheet.64 + + + + + Sheet.65 + + + + + Sheet.66 + + + + + Sheet.67 + + + + Sheet.68 + + + + Sheet.76 + + + + Sheet.77 + + + + + + Sheet.78 + + Sheet.147 + + + + Sheet.148 + + + + Sheet.150 + + + + Sheet.151 + + + + Sheet.152 + + + + Sheet.153 + + + + + + + + Sheet.154 + + Sheet.164 + + + + Sheet.168 + + + + Sheet.194 + + + + Sheet.195 + + + + Sheet.196 + + + + Sheet.198 + + Sheet.200 + + + + + Sheet.201 + + + + + + + + dd + + + + + + + + Border small.202 + + Sheet.203 + + + + Sheet.204 + Distributeur de données PAZ (dd) + + + + Distributeur de données PAZ (dd) + + + Process.205 + subscribe Obtenez les données de l'échange de données et envo... + + + + + + + + + + + + subscribeObtenez les données de l'échange de données et envoyez une confirmation à l'échange de rapports. + + Sheet.207 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.208 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User.209 + Bob + + Sheet.210 + + + + + Sheet.211 + + + + + Sheet.212 + + + + Sheet.213 + + + + Sheet.214 + + + + Sheet.216 + + + + Sheet.217 + + + + + + Bob + + + Process.241 + shovel Recevoir, valider et ingérer. + + + + + + + + + + + + shovelRecevoir, valider et ingérer. + + Process.271 + shovel Propager à travers des grappe. + + + + + + + + + + + + shovelPropager à travers des grappe. + + Sheet.273 + xpublic + + + + xpublic + + Sheet.274 + xreport + + + + xreport + + Sheet.275 + xr_bob + + + + xr_bob + + Dynamic connector.279 + + + + Dynamic connector.282 + + + + Dynamic connector.284 + + + + Sheet.189 + xreport + + + + xreport + + Dynamic connector.281 + + + Dynamic connector.283 + + + + + + + + + Border small.309 + + Sheet.310 + + + + Sheet.311 + Serveur / Courtier + + + + Serveur / Courtier + + + Process.317 + Sarracenia composant + + + + + + + + + + + + Sarracenia composant + + Dynamic connector.318 + + + + Dynamic connector.319 + + + + Dynamic connector.320 + + + + Dynamic connector.321 + + + + Sheet.322 + Échange + + + + Échange + + Dynamic connector.323 + + + + Dynamic connector.324 + + + + Dynamic connector.327 + sens de l'écoulement + + + + + sens de l'écoulement + + Sheet.328 + Sarracenia Exemple de Composants et de Flux + + + + Sarracenia Exemple de Composants et de Flux + + Dynamic connector.331 + + + + diff --git a/_images/sr3_flow_example.svg b/_images/sr3_flow_example.svg new file mode 100644 index 000000000..bef8bd7a8 --- /dev/null +++ b/_images/sr3_flow_example.svg @@ -0,0 +1,2650 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Page-1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Translucent + + + + + + + + + + + + + + + + Sheet.313 + + Sheet.314 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sheet.315 + Legend + + Sheet.316 + + + + + + + + + + + + + + + + + + + + + + Legend + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Application server + ddsr + + Sheet.2 + + + + Sheet.3 + + Sheet.4 + + + + + Sheet.5 + + + + + Sheet.6 + + + + Sheet.7 + + + + Sheet.8 + + + + Sheet.9 + + + + + + Sheet.10 + + Sheet.11 + + + + Sheet.12 + + + + Sheet.13 + + + + Sheet.14 + + + + Sheet.15 + + + + Sheet.16 + + + + + + Sheet.17 + + Sheet.18 + + + + + Sheet.19 + + + + + Sheet.20 + + + + Sheet.21 + + + + Sheet.22 + + + + Sheet.23 + + + + + + Sheet.24 + + Sheet.25 + + + + Sheet.26 + + + + Sheet.27 + + + + Sheet.28 + + + + Sheet.29 + + + + Sheet.30 + + + + + + Sheet.31 + + Sheet.32 + + + + + Sheet.33 + + + + + Sheet.34 + + + + Sheet.35 + + + + Sheet.36 + + + + Sheet.37 + + + + + + Sheet.38 + + Sheet.39 + + + + Sheet.40 + + + + Sheet.41 + + + + Sheet.42 + + + + Sheet.43 + + + + Sheet.44 + + + + + + Sheet.45 + + Sheet.46 + + + + + Sheet.47 + + + + + Sheet.48 + + + + Sheet.49 + + + + Sheet.50 + + + + Sheet.51 + + + + + + Sheet.52 + + Sheet.53 + + + + Sheet.54 + + + + Sheet.55 + + + + Sheet.56 + + + + Sheet.57 + + + + Sheet.58 + + + + + + + + ddsr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FTP server + ddi + + Sheet.80 + + + + + Sheet.81 + + + + + Sheet.82 + + + + + Sheet.83 + + + + Sheet.84 + + + + Sheet.85 + + + + Sheet.86 + + + + + + Sheet.87 + + Sheet.88 + + + + Sheet.89 + + + + Sheet.90 + + + + Sheet.91 + + + + Sheet.92 + + + + Sheet.93 + + + + + + + + Sheet.94 + + Sheet.95 + + + + Sheet.96 + + + + Sheet.97 + + + + Sheet.98 + + + + Sheet.99 + + + + Sheet.100 + + Sheet.101 + + + + + Sheet.102 + + + + + + + + ddi + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server + urp + + Sheet.104 + + + + + Sheet.105 + + + + + Sheet.106 + + + + + Sheet.107 + + + + Sheet.108 + + + + Sheet.109 + + + + Sheet.110 + + + + + + Sheet.111 + + Sheet.112 + + + + Sheet.113 + + + + Sheet.114 + + + + Sheet.115 + + + + Sheet.116 + + + + Sheet.117 + + + + + + + + Sheet.118 + + Sheet.119 + + + + Sheet.120 + + + + Sheet.121 + + + + Sheet.122 + + + + Sheet.123 + + + + + + + urp + + + + + + + + + + + + + + + + + + + + + + + + + + Firewall + + Sheet.157 + + + + Sheet.158 + + + + Sheet.159 + + + + Sheet.160 + + + + Sheet.161 + + + + + + + + + + + + + + + + + Sheet.162 + + + + + + + + + + Border small.191 + + Sheet.192 + + + + Sheet.193 + Data distributor OZ (ddi) + + + + Data distributor OZ (ddi) + + + + + + + + Border small.183 + + Sheet.184 + + + + Sheet.188 + Data pump (ddsr) + + + + Data pump (ddsr) + + + + + + + + Border small.185 + + Sheet.186 + + + + Sheet.187 + Data source (urp) + + + + Data source (urp) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server.124 + goes + + Sheet.125 + + + + + Sheet.126 + + + + + Sheet.127 + + + + + Sheet.128 + + + + Sheet.129 + + + + Sheet.130 + + + + Sheet.131 + + + + + + Sheet.132 + + Sheet.133 + + + + Sheet.134 + + + + Sheet.135 + + + + Sheet.136 + + + + Sheet.137 + + + + Sheet.138 + + + + + + + + Sheet.139 + + Sheet.140 + + + + Sheet.141 + + + + Sheet.142 + + + + Sheet.143 + + + + Sheet.144 + + + + + + + goes + + + Process.3 + watch Announce files placed in a watched directory. + + + + + + + + + + + + watchAnnounce files placed in a watched directory. + + + + + + + Border small.165 + + Sheet.166 + + + + Sheet.167 + Data source (goes) + + + + Data source (goes) + + + Process.169 + sr3_post Announce files for ingest. + + + + + + + + + + + + sr3_postAnnounce files for ingest. + + Process.155 + sarra Subscribe, validate, fetch and re-announce. + + + + + + + + + + + + sarraSubscribe, validate, fetch and re-announce. + + Process.170 + sender Subscribe, validate and send to destination. + + + + + + + + + + + + senderSubscribe, validate and send to destination. + + Process.182 + subscribe Get data from data exchange and send a confirmation... + + + + + + + + + + + + subscribeGet data from data exchange and send a confirmation toreport exchange. + + Dynamic connector.229 + + + + Dynamic connector.230 + + + + Dynamic connector.231 + + + + Dynamic connector.171 + + + + + + + + + Border small.225 + + Sheet.232 + + + + Sheet.233 + Pump reporting + + + + Pump reporting + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + File server.234 + + Sheet.235 + + + + + Sheet.236 + + + + + Sheet.238 + + + + + Sheet.239 + + + + Sheet.240 + + + + Sheet.242 + + + + Sheet.243 + + + + + + Sheet.244 + + Sheet.245 + + + + Sheet.246 + + + + Sheet.247 + + + + Sheet.248 + + + + Sheet.249 + + + + Sheet.250 + + + + + + + + Sheet.251 + + Sheet.252 + + + + Sheet.253 + + + + Sheet.254 + + + + Sheet.255 + + + + Sheet.256 + + + + + + Dynamic connector.59 + + + + File + + + + + + + + + + + Sheet.61 + Ex: 201505291700_WMN_PRECIPET_RAIN.gif201505291800_XAM_PRECIP... + + + + Ex: 201505291700_WMN_PRECIPET_RAIN.gif201505291800_XAM_PRECIPET_RAIN.gif + + File.62 + + + + + + + + + + + Sheet.63 + Ex: /GOES-12/* /GOES-13/* + + + + Ex:/GOES-12/* /GOES-13/* + + Sheet.146 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.145 + + + + + + + + + + + Dynamic connector + + + + Sheet.172 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.197 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User.173 + Billy + + Sheet.174 + + + + + Sheet.175 + + + + + Sheet.176 + + + + Sheet.177 + + + + Sheet.178 + + + + Sheet.179 + + + + Sheet.180 + + + + + + Billy + + + Process.224 + shovel Read report from source report exchange to know which ... + + + + + + + + + + + + shovelRead report from source report exchange to know which clients obtained the data. + + + + + + + + + + + + + + + + + + + + User.228 + partner + + Sheet.260 + + + + + Sheet.261 + + + + + Sheet.262 + + + + Sheet.263 + + + + Sheet.266 + + + + Sheet.267 + + + + Sheet.268 + + + + + + partner + + + Sheet.277 + Authors: P.Silva, M.Grenier, J.Hu, T.Kaufmann Updated: 2022-0... + + + + Authors: P.Silva, M.Grenier, J.Hu, T.KaufmannUpdated: 2022-03-30 + + Process.219 + shovel Announce the report. + + + + + + + + + + + + shovelAnnounce the report. + + Process.199 + shovel Receive, validate and ingest. + + + + + + + + + + + + shovelReceive, validate and ingest. + + Process.218 + shovel Disseminate through clusters. + + + + + + + + + + + + shovelDisseminate through clusters. + + Dynamic connector.290 + + + + Dynamic connector.276 + + + + Process.264 + sarra Subscribe, validate, fetch and re-announce. + + + + + + + + + + + + sarraSubscribe, validate, fetch and re-announce. + + Dynamic connector.269 + + + + Sheet.215 + xr_goes + + + + xr_goes + + Sheet.190 + xs_urp + + + + xs_urp + + Sheet.220 + xpublic + + + + xpublic + + Sheet.221 + xs_goes + + + + xs_goes + + Dynamic connector.222 + + + + Sheet.223 + xpublic + + + + xpublic + + Sheet.237 + xreport + + + + xreport + + Sheet.257 + xr_billy + + + + xr_billy + + Sheet.181 + + + + Sheet.226 + + + + Dynamic connector.227 + + + + Dynamic connector.258 + + + + Dynamic connector.259 + + + + Dynamic connector.270 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + FTP server.60 + dd + + Sheet.64 + + + + + Sheet.65 + + + + + Sheet.66 + + + + + Sheet.67 + + + + Sheet.68 + + + + Sheet.76 + + + + Sheet.77 + + + + + + Sheet.78 + + Sheet.147 + + + + Sheet.148 + + + + Sheet.150 + + + + Sheet.151 + + + + Sheet.152 + + + + Sheet.153 + + + + + + + + Sheet.154 + + Sheet.164 + + + + Sheet.168 + + + + Sheet.194 + + + + Sheet.195 + + + + Sheet.196 + + + + Sheet.198 + + Sheet.200 + + + + + Sheet.201 + + + + + + + + dd + + + + + + + + Border small.202 + + Sheet.203 + + + + Sheet.204 + Data distributor PAZ (dd) + + + + Data distributor PAZ (dd) + + + Process.205 + subscribe Get data from data exchange and send a confirmation... + + + + + + + + + + + + subscribeGet data from data exchange and send a confirmation toreport exchange. + + Sheet.207 + Ex: /20150529/goes/GOES-12/ /20150529/goes/GOES-13/ /20150529... + + + + Ex: /20150529/goes/GOES-12//20150529/goes/GOES-13//20150529/urp/WMN/201505291700_WMN_PRECIPET_RAIN.gif/20150529/urp/XAM/201505291800_XAM_PRECIPET_RAIN.gif + + File.208 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User.209 + Bob + + Sheet.210 + + + + + Sheet.211 + + + + + Sheet.212 + + + + Sheet.213 + + + + Sheet.214 + + + + Sheet.216 + + + + Sheet.217 + + + + + + Bob + + + Process.241 + shovel Receive, validate and ingest. + + + + + + + + + + + + shovelReceive, validate and ingest. + + Process.271 + shovel Disseminate through clusters. + + + + + + + + + + + + shovelDisseminate through clusters. + + Sheet.273 + xpublic + + + + xpublic + + Sheet.274 + xreport + + + + xreport + + Sheet.275 + xr_bob + + + + xr_bob + + Dynamic connector.279 + + + + Dynamic connector.282 + + + + Dynamic connector.284 + + + + Sheet.189 + xreport + + + + xreport + + Dynamic connector.281 + + + Dynamic connector.283 + + + + + + + + + Border small.309 + + Sheet.310 + + + + Sheet.311 + Server / Broker + + + + Server / Broker + + + Process.317 + Sarracenia component + + + + + + + + + + + + Sarracenia component + + Dynamic connector.318 + + + + Dynamic connector.319 + + + + Dynamic connector.320 + + + + Dynamic connector.321 + + + + Sheet.322 + Exchange + + + + Exchange + + Dynamic connector.323 + + + + Dynamic connector.324 + + + + Dynamic connector.327 + Flow direction + + + + + Flow direction + + Sheet.328 + Sarracenia Components and Flows Example + + + + Sarracenia Components and Flows Example + + diff --git a/_images/start-menu-1.png b/_images/start-menu-1.png new file mode 100644 index 000000000..9c16fa5ff Binary files /dev/null and b/_images/start-menu-1.png differ diff --git a/_modules/index.html b/_modules/index.html new file mode 100644 index 000000000..6115189e2 --- /dev/null +++ b/_modules/index.html @@ -0,0 +1,185 @@ + + + + + + Overview: module code — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Overview: module code
  • +
  • +
  • +
+
+
+
+
+ +

All modules for which code is available

+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia.html b/_modules/sarracenia.html new file mode 100644 index 000000000..510bcc99c --- /dev/null +++ b/_modules/sarracenia.html @@ -0,0 +1,1073 @@ + + + + + + sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia

+#
+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# Questions or bugs report: dps-client@ec.gc.ca
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+# __init__.py : contains version number of sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+from ._version import __version__
+
+
+from base64 import b64decode, b64encode
+import calendar
+import datetime
+import humanize
+import importlib.util
+import logging
+import os
+import os.path
+import random
+import re
+from sarracenia.featuredetection import features
+import stat as os_stat
+import sys
+import time
+import types
+import urllib
+import urllib.parse
+import urllib.request
+
+logger = logging.getLogger(__name__)
+
+def baseUrlParse( url ):
+    upr = urllib.parse.urlparse(url)
+    u = types.SimpleNamespace()
+    u.scheme = upr.scheme
+    u.netlog = upr.netloc
+    u.params = upr.params
+    u.query = upr.query
+    u.fragment = upr.fragment
+    u.path = upr.path
+    if u.scheme in [ 'sftp', 'file' ]:
+        while u.path.startswith('//'):
+            u.path = u.path[1:]
+    return u
+
+
+if features['filetypes']['present']:
+   import magic
+
+if features['mqtt']['present']:
+   import paho.mqtt.client
+   if not hasattr( paho.mqtt.client, 'MQTTv5' ):
+       # without v5 support, mqtt is not useful.
+       features['mqtt']['present'] = False
+
+# if humanize is not present, compensate...
+if features['humanize']['present']:
+    import humanize
+
+    def naturalSize( num ):
+        return humanize.naturalsize(num,binary=True)
+
+    def naturalTime( dur ):
+        return humanize.naturaltime(dur)
+
+else:
+  
+    def naturalSize( num ):
+       return "%g" % num
+
+    def naturalTime( dur ):
+       return "%g" % dur
+
+
+if features['appdirs']['present']:
+    import appdirs
+
+    def site_config_dir( app, author ):
+        return appdirs.site_config_dir( app, author )
+
+    def user_config_dir( app, author ):
+        return appdirs.user_config_dir( app, author )
+
+    def user_cache_dir( app, author ):
+        return appdirs.user_cache_dir( app, author )
+else:
+    # if appdirs is missing, pretend we're on Linux.
+    import pathlib
+
+    def site_config_dir( app, author ):
+        return '/etc/xdg/xdg-ubuntu-xorg/%s' % app
+
+    def user_config_dir( app, author ):
+        return str(pathlib.Path.home()) + '/.config/%s' % app
+ 
+    def user_cache_dir( app, author ):
+        return str(pathlib.Path.home()) + '/.cache/%s' % app
+
+"""
+ end of extra feature scan. 
+
+"""
+
+import sarracenia.filemetadata
+
+
+[docs] +class Sarracenia: + """ + Core utilities of Sarracenia. The main class here is sarracenia.Message. + a Sarracenia.Message is subclassed from a dict, so for most uses, it works like the + python built-in, but also we have a few major entry points some factoryies: + + + **Building a message from a file** + + m = sarracenia.Message.fromFileData( path, options, lstat ) + + builds a notification message from a given existing file, consulting *options*, a parsed + in memory version of the configuration settings that are applicable + + **Options** + + see the sarracenia.config.Config class for functions to parse configuration files + and create corresponding python option dictionaries. One can supply small + dictionaries for example:: + + options['topicPrefix'] = [ 'v02', 'post' ] + options['bindings'] = [ ('xpublic', [ 'v02', 'post'] , [ '#' ] )] + options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_SomethingHelpfulToYou' + + Above is an example of a minimal options dictionary taken from the tutorial + example called moth_api_consumer.py. often + + + **If you don't have a file** + + If you don't have a local file, then build your notification message with: + + m = sarracenia.Message.fromFileInfo( path, options, lstat ) + + where you can make up the lstat values to fill in some fields in the message. + You can make a fake lstat structure to provide these values using sarracenia.filemetadata + class which is either an alias for paramiko.SFTPAttributes + ( https://docs.paramiko.org/en/latest/api/sftp.html#paramiko.sftp_attr.SFTPAttributes ) + if paramiko is installed, or a simple emulation if not. + + + from sarracenia.filemetadata import FmdStat + + lstat = FmdStat() + lstat.st_mtime= utcinteger second count in UTC (numeric version of a Sarracenia timestamp.) + lstat.st_atime= + lstat.st_mode=0o644 + lstat.st_size= size_in_bytes + + optional fields that may be of interest: + lstat.filename= "nameOfTheFile" + lstat.longname= 'lrwxrwxrwx 1 peter peter 20 Oct 11 20:28 nameOfTheFile' + + that you can then provide as an *lstat* argument to the above *fromFileInfo()* + call. However the notification message returned will lack an identity checksum field. + once you get the file, you can add the Identity field with: + + m.computeIdentity(path, o): + + In terms of consuming notification messages, the fields in the dictionary provide metadata + for the announced resource. The anounced data could be embedded in the notification message itself, + or available by a URL. + + Messages are generally gathered from a source such as the Message Queueing Protocol wrapper + class: moth... sarracenia.moth. + + + data = m.getContent() + + will return the content of the announced resource as raw data. + + """ + pass
+ + + +
+[docs] +class TimeConversions: + """ + + Time conversion routines. + + * os.stat, and time.now() return floating point + + * The floating point representation is a count of seconds since the beginning of the epoch. + + * beginning of epoch is platform dependent, and conversion to actual date is fraught (leap seconds, etc...) + + * Entire SR_* formats are text, no floats are sent over the protocol + (avoids byte order issues, null byte / encoding issues, and enhances readability.) + + * str format: YYYYMMDDHHMMSS.msec goal of this representation is that a naive + conversion to floats yields comparable numbers. + + * but the number that results is not useful for anything else, so need these + special routines to get a proper epochal time. + + * also OK for year 2032 or whatever (rollover of time_t on 32 bits.) + + * string representation is forced to UTC timezone to avoid having to communicate timezone. + + timestr2flt() - accepts a string and returns a float. + + caveat + + FIXME: this encoding will break in the year 10000 (assumes four digit year) + and requires leading zeroes prior to 1000. One will have to add detection of + the decimal point, and change the offsets at that point. + + """ + pass
+ + + +
+[docs] +def stat( path ) -> sarracenia.filemetadata.FmdStat: + """ + os.stat call replacement which improves on it by returning + and SFTPAttributes structure, in place of the OS stat one, + featuring: + + * mtime and ctime with subsecond accuracy + * fields that can be overridden (not immutable.) + + """ + native_stat = os.stat( path ) + + sa = sarracenia.filemetadata.FmdStat() + sa.st_mode = native_stat.st_mode + sa.st_ino = native_stat.st_ino + sa.st_dev = native_stat.st_dev + # st_nlink does not exist in paramiko.SFTPAttributes() + # FmdStat comes from that type. + #sa.st_nlink = native_stat.st_nlink + sa.st_uid = native_stat.st_uid + sa.st_gid = native_stat.st_gid + sa.st_size = native_stat.st_size + + sa.st_mtime = os.path.getmtime(path) + sa.st_atime = os.path.getctime(path) + sa.st_ctime = native_stat.st_atime + return sa
+ + +def nowflt(): + return timestr2flt(nowstr()) + + +def nowstr(): + return timeflt2str(time.time()) + + +
+[docs] +def timeflt2str(f=None): + """ + timeflt2str - accepts a float and returns a string. + + flow is a floating point number such as returned by time.now() + (number of seconds since beginning of epoch.) + + the str is YYYYMMDDTHHMMSS.sssss + + 20210921T011331.0123 + + translates to: Sept. 21st, 2021 at 01:13 and 31.0123 seconds. + always UTC timezone. + """ + + nsec = "{:.9g}".format(f % 1)[1:] + return "{}{}".format(time.strftime("%Y%m%dT%H%M%S", time.gmtime(f)), nsec)
+ + + +def timeValidate(s) -> bool: + + if len(s) < 14: return False + if (len(s) > 14) and (s[8] != 'T') and (s[14] != '.'): return False + if (len(s) > 15) and (s[8] == 'T') and (s[15] != '.'): return False + if not s[0:8].isalnum(): return False + return True + + +def timestr2flt(s): + + if s[8] == "T": + s = s.replace('T', '') + dt_tuple = int(s[0:4]), int(s[4:6]), int(s[6:8]), int(s[8:10]), int( + s[10:12]), int(s[12:14]) + t = datetime.datetime(*dt_tuple, tzinfo=datetime.timezone.utc) + return calendar.timegm(t.timetuple()) + float('0' + s[14:]) + + +def timev2tov3str(s): + if s[8] == 'T': + return s + else: + return s[0:8] + 'T' + s[8:] + +
+[docs] +def durationToString(d) -> str: + """ + given a numbner of seconds, return a short, human readable string. + """ + return humanize.naturaldelta(d).replace("minutes","m").replace("seconds","s").replace("hours","h").replace("days","d").replace("an hour","1h").replace("a day","1d").replace("a minute","1m").replace(" ","")
+ + +
+[docs] +def durationToSeconds(str_value, default=None) -> float: + """ + this function converts duration to seconds. + str_value should be a number followed by a unit [s,m,h,d,w] ex. 1w, 4d, 12h + return 0.0 for invalid string. + """ + factor = 1 + + if type(str_value) in [list]: + str_value = str_value[0] + + if type(str_value) in [int, float]: + return str_value + + if type(str_value) is not str: + return 0 + + if str_value.lower() in [ 'none', 'off', 'false' ]: + return 0 + + if default and str_value.lower() in [ 'on', 'true' ]: + return float(default) + + if str_value[-1] in 'sS': factor *= 1 + elif str_value[-1] in 'mM': factor *= 60 + elif str_value[-1] in 'hH': factor *= 60 * 60 + elif str_value[-1] in 'dD': factor *= 60 * 60 * 24 + elif str_value[-1] in 'wW': factor *= 60 * 60 * 24 * 7 + if str_value[-1].isalpha(): str_value = str_value[:-1] + + try: + duration = float(str_value) * factor + except: + logger.error("conversion failed for: %s" % str_value) + duration = 0.0 + + return duration
+ + + +known_report_codes = { + 201: + "Download successful. (variations: Downloaded, Inserted, Published, Copied, or Linked)", + 203: "Non-Authoritative Information: transformed during download.", + 205: + "Reset Content: truncated. File is shorter than originally expected (changed length during transfer) This only arises during multi-part transfers.", + 205: "Reset Content: checksum recalculated on receipt.", + 304: + "Not modified (Checksum validated, unchanged, so no download resulted.)", + 307: "Insertion deferred (writing to temporary part file for the moment.)", + 417: "Expectation Failed: invalid notification message (corrupt headers)", + 499: "Failure: Not Copied. SFTP/FTP/HTTP download problem", + #FIXME : should not have 503 error code 3 times in a row + # 503: "Service unavailable. delete (File removal not currently supported.)", + 503: "Unable to process: Service unavailable", + # 503: "Unsupported transport protocol specified in posting." +} + + +
+[docs] +class Message(dict): + """ + A notification message in Sarracenia is stored as a python dictionary, with a few extra management functions. + + The internal representation is very close to the v03 format defined here: https://metpx.github.io/sarracenia/Reference/sr_post.7.html + + Unfortunately, sub-classing of dict means that to copy it from a dict will mean losing the type, + and hence the need for the copyDict member. + """ +
+[docs] + def __init__(self): + self['_format'] = 'v03' + self['_deleteOnPost'] = set(['_format'])
+ + + +
+[docs] + def computeIdentity(msg, path, o, offset=0): + """ + check extended attributes for a cached identity sum calculation. + if extended attributes are present, and + * the file mtime is not too new, and + * the cached sum us using the same method + then use the cached value. + + otherwise, calculate a checksum. + set the file's extended attributes for the new value. + the method of checksum calculation is from options.identity. + + sets the message 'identity' field if appropriate. + """ + xattr = sarracenia.filemetadata.FileMetadata(path) + + if not 'blocks' in msg: + if o.randomize: + methods = [ + 'random', 'md5', 'md5name', 'sha512', 'cod,md5', 'cod,sha512' + ] + calc_method = random.choice(methods) + elif 'identity' in xattr.x and 'mtime' in xattr.x: + if xattr.get('mtime') >= msg['mtime']: + logger.debug("mtime remembered by xattr") + fxainteg = xattr.get('identity') + if fxainteg['method'] == o.identity_method: + msg['identity'] = fxainteg + return + logger.debug("xattr different method than on disk") + calc_method = o.identity_method + else: + logger.debug("xattr sum too old") + calc_method = o.identity_method + else: + calc_method = o.identity_method + else: + calc_method = o.identity_method + + if calc_method == None: + return + + if 'mtime' in msg: + xattr.set('mtime', msg['mtime']) + + logger.debug("mtime persisted, calc_method: {calc_method}") + + if calc_method[:4] == 'cod,' and len(calc_method) > 2: + sumstr = calc_method + elif calc_method in [ 'md5name', 'invalid' ]: + xattr.persist() # persist the mtime, at least... + return # no checksum needed for md5name. + elif calc_method == 'arbitrary': + sumstr = { + 'method': 'arbitrary', + 'value': o.identity_arbitrary_value + } + else: # a "normal" calculation method, liks sha512, or md5 + sumalgo = sarracenia.identity.Identity.factory(calc_method) + sumalgo.set_path(path) + + # compute checksum + + if calc_method in ['md5', 'sha512']: + + fp = open(path, 'rb') + i = 0 + + #logger.info( f"offset: {offset} size: {msg['size']} max: {offset+msg['size']} " ) + if offset: + fp.seek( offset ) + + while i < offset+msg['size']: + buf = fp.read(o.bufsize) + if not buf: break + sumalgo.update(buf) + i += len(buf) + fp.close() + + # setting sumstr + checksum = sumalgo.value + sumstr = {'method': calc_method, 'value': checksum} + + msg['identity'] = sumstr + xattr.set('identity', sumstr) + xattr.persist()
+ + +
+[docs] + def copyDict(msg, d): + """ + copy dictionary into message. + """ + if d is None: return + + for h in d: + msg[h] = d[h]
+ + +
+[docs] + def dumps(msg) -> str: + """ + FIXME: used to be msg_dumps. + print a message in a compact but relatively compact way. + msg is a python dictionary. if there is a field longer than maximum_field_length, + truncate. + + """ + + maximum_field_length = 255 + + if msg is None: return "" + + if msg['_format'] == 'Wis': + s = '{ ' + if 'id' in msg: + s += f"{{ 'id': '{msg['id']}', 'type':'Feature', " + if 'geometry' in msg: + s += f"'geometry':{msg['geometry']} 'properties':{{ " + else: + s += "'geometry': None, 'properties':{ " + + else: + s = "{ " + + for k in sorted(msg.keys()): + + if msg['_format'] == 'v04' and k in [ 'id', 'type', 'geometry' ]: + continue + + if type(msg[k]) is dict: + if k != 'properties': + v = "{ " + for kk in sorted(msg[k].keys()): + v += " '%s':'%s'," % (kk, msg[k][kk]) + v = v[:-1] + if k != 'properties': + v += " }" + else: + try: + v = "%s" % msg[k] + except: + v = "unprintable" + + if len(v) > maximum_field_length: + v = v[0:maximum_field_length - 4] + '...' + if v[0] == '{': + v += '}' + + s += f" '{k}':'{v}'," + + if msg['_format'] == 'Wis': + s += ' } ' + + s = s[:-1] + " }" + return s
+ + +
+[docs] + @staticmethod + def fromFileData(path, o, lstat=None): + """ + create a message based on a given file, calculating the checksum. + returns a well-formed message, or none. + """ + m = sarracenia.Message.fromFileInfo(path, o, lstat) + if lstat : + if os_stat.S_ISREG(lstat.st_mode): + m.computeIdentity(path, o) + if features['filetypes']['present']: + try: + t = magic.from_file(path,mime=True) + m['contentType'] = t + except Exception as ex: + logging.info("trying to determine mime-type. Exception details:", exc_info=True) + #else: + # m['contentType'] = 'application/octet-stream' # https://www.rfc-editor.org/rfc/rfc2046.txt (default when clueless) + # I think setting a bad value is worse than none, so just omitting. + elif os_stat.S_ISDIR(lstat.st_mode): + m['contentType'] = 'text/directory' # source: https://www.w3.org/2002/12/cal/rfc2425.html + elif os_stat.S_ISLNK(lstat.st_mode): + m['contentType'] = 'text/link' # I invented this one, could not find any reference + return m
+ + +
+[docs] + @staticmethod + def fromFileInfo(path, o, lstat=None): + """ + based on the fiven information about the file (it's name and a stat record if available) + and a configuration options object (sarracenia.config.Config) + return an sarracenia.Message suitable for placement on a worklist. + + A message is a specialized python dictionary with a certain set of fields in it. + The message returned will have the necessary fields for processing and posting. + + The message is built for a file is based on the given path, options (o), and lstat (output of os.stat) + + The lstat record is used to build 'atime', 'mtime' and 'mode' fields if + timeCopy and permCopy options are set. + + if no lstat record is supplied, then those fields will not be set. + """ + + msg = Message() + + #FIXME no variable substitution... o.variableExpansion ? + if hasattr(o,'post_format') : + msg['_format'] = o.post_format + elif hasattr(o,'post_topicPrefix') and o.post_topicPrefix[0] in [ 'v02', 'v03' ]: + msg['_format'] = o.post_topicPrefix[0] + else: + msg['_format'] = 'v03' + + if hasattr(o, 'post_exchange'): + msg['exchange'] = o.post_exchange + elif hasattr(o, 'exchange'): + msg['exchange'] = o.exchange + + if hasattr(o, 'blocksize') and (o.blocksize > 1) and lstat and \ + (os_stat.S_IFMT(lstat.st_mode) == os_stat.S_IFREG) and \ + (lstat.st_size > o.blocksize): + msg['blocks'] = { 'method': 'inplace', 'number':-1, 'size': o.blocksize, 'manifest': {} } + + msg['local_offset'] = 0 + msg['_deleteOnPost'] = set(['exchange', 'local_offset', 'subtopic', '_format']) + + # notice + msg['pubTime'] = timeflt2str(time.time()) + + # set new_dir, new_file, new_subtopic, etc... + msg.updatePaths(o, os.path.dirname(path), os.path.basename(path)) + + # rename + if 'new_relPath' in msg: + post_relPath = msg['new_relPath'] + elif 'relPath' in msg: + post_relPath = msg['relPath'] + else: + post_relPath = None + + newname = post_relPath + + # rename path given with no filename + + if o.rename: + msg['retrievePath'] = msg['new_retrievePath'] + newname = o.variableExpansion(o.rename) + if o.rename[-1] == '/': + newname += os.path.basename(path) + + # strip 'N' heading directories + if o.strip > 0: + strip = o.strip + if path[0] == '/': strip = strip + 1 + # if we strip too much... keep the filename + token = path.split('/') + try: + token = token[strip:] + except: + token = [os.path.basename(path)] + newname = '/' + '/'.join(token) + + if newname != post_relPath: msg['rename'] = newname + + if hasattr(o, 'to_clusters') and (o.to_clusters is not None): + msg['to_clusters'] = o.to_clusters + if hasattr(o, 'cluster') and (o.cluster is not None): + msg['from_cluster'] = o.cluster + + if hasattr(o, 'source') and (o.source is not None): + msg['source'] = o.source + + + if o.identity_method: + if o.identity_method.startswith('cod,'): + msg['identity'] = { + 'method': 'cod', + 'value': o.identity_method[4:] + } + elif o.identity_method in ['random']: + algo = sarracenia.identity.Identity.factory(o.identity_method) + algo.set_path(post_relPath) + msg['identity'] = { + 'method': o.identity_method, + 'value': algo.value + } + else: + if 'identity' in msg: + del msg['identity'] + + # for md5name/aka None aka omit identity... should just fall through. + + if lstat is None: return msg + + if (lstat.st_mode is not None) : + msg['mode'] = "%o" % (lstat.st_mode & 0o7777) + if not o.permCopy: + msg['_deleteOnPost'] |= set(['mode']) + + if os_stat.S_ISDIR(lstat.st_mode): + msg['fileOp'] = { 'directory': '' } + return msg + + if lstat.st_size is not None: + msg['size'] = lstat.st_size + + if lstat.st_mtime is not None: + msg['mtime'] = timeflt2str(lstat.st_mtime) + if lstat.st_atime is not None: + msg['atime'] = timeflt2str(lstat.st_atime) + + if not o.timeCopy: + msg['_deleteOnPost'] |= set([ 'atime', 'mtime' ]) + + return msg
+ + +
+[docs] + @staticmethod + def fromStream(path, o, data=None): + """ + Create a file and message for the given path. + The file will be created or overwritten with the provided data. + then invoke fromFileData() for the resulting file. + """ + + with open(path, 'wb') as fh: + fh.write(data) + + if hasattr(o, 'chmod') and o.chmod: + os.chmod(path, o.chmod) + + return sarracenia.Message.fromFileData(path, o, stat(path))
+ + +
+[docs] + def setReport(msg, code, text=None): + """ + FIXME: used to be msg_set_report + set message fields to indicate result of action so reports can be generated. + + set is supposed to indicate final message dispositions, so in the case + of putting a message on worklist.failed... no report is generated, since + it will be retried later. FIXME: should we publish an interim failure report? + + """ + + if code in known_report_codes: + if text is None: + text = known_report_codes[code] + + else: + logger.warning('unknown report code supplied: %d:%s' % + (code, text)) + if text is None: + text = 'unknown disposition' + + if 'report' in msg: + logger.warning('overriding initial report: %d: %s' % + (msg['report']['code'], msg['report']['message'])) + + msg['report'] = {'code': code, 'timeCompleted': nowstr(), 'message': text} + msg['_deleteOnPost'] |= set(['report'])
+ + +
+[docs] + def updatePaths(msg, options, new_dir=None, new_file=None): + """ + set the new_* fields in the message based on changed file placement. + if new_* options are ommitted updaste the rest of the fields in + the message based on their current values. + + If you change file placement in a flow callback, for example. + One would change new_dir and new_file in the message. + This routines updates other fields in the message (e.g. relPath, + baseUrl, topic ) to match new_dir/new_file. + + msg['post_baseUrl'] defaults to msg['baseUrl'] + + """ + + # the headers option is an override. + if hasattr(options, 'fixed_headers'): + for k in options.fixed_headers: + msg[k] = options.fixed_headers[k] + + msg['_deleteOnPost'] |= set([ + 'new_dir', 'new_file', 'new_relPath', 'new_baseUrl', 'new_subtopic', 'subtopic', 'post_format' + ]) + if new_dir: + msg['new_dir'] = new_dir + elif 'new_dir' in msg: + new_dir = msg['new_dir'] + else: + new_dir = '' + + if new_file or new_file == '': + msg['new_file'] = new_file + elif 'new_file' in msg: + new_file = msg['new_file'] + elif 'new_relPath' in msg: + new_file = os.path.basename(msg['rel_relPath']) + elif 'relPath' in msg: + new_file = os.path.basename(msg['relPath']) + else: + new_file = 'ErrorInSarraceniaMessageUpdatePaths.txt' + + newFullPath = new_dir + '/' + new_file + + # post_baseUrl option set in msg overrides other possible options + if 'post_baseUrl' in msg: + baseUrl_str = msg['post_baseUrl'] + elif options.post_baseUrl: + baseUrl_str = options.variableExpansion(options.post_baseUrl, msg) + else: + if 'baseUrl' in msg: + baseUrl_str = msg['baseUrl'] + else: + logger.error('missing post_baseUrl setting') + return + + if options.post_format: + msg['post_format'] = options.post_format + elif options.post_topicPrefix: + msg['post_format'] = options.post_topicPrefix[0] + elif options.topicPrefix != msg['_format']: + logger.warning( f"received message in {msg['_format']} format, expected {options.post_topicPrefix} " ) + msg['post_format'] = options.topicPrefix[0] + else: + msg['post_format'] = msg['_format'] + + if hasattr(options, 'post_baseDir') and ( type(options.post_baseDir) is str ) \ + and ( len(options.post_baseDir) > 1): + pbd_str = options.variableExpansion(options.post_baseDir, msg) + parsed_baseUrl = sarracenia.baseUrlParse(baseUrl_str) + + if newFullPath.startswith(pbd_str): + newFullPath = new_dir.replace(pbd_str, '', 1) + '/' + new_file + + if (len(parsed_baseUrl.path) > 1) and newFullPath.startswith( + parsed_baseUrl.path): + newFullPath = newFullPath.replace(parsed_baseUrl.path, '', 1) + + if ('new_dir' not in msg) and options.post_baseDir: + msg['new_dir'] = options.post_baseDir + + msg['new_baseUrl'] = baseUrl_str + + if len(newFullPath) > 0 and newFullPath[0] == '/': + newFullPath = newFullPath[1:] + + msg['new_relPath'] = newFullPath + msg['new_subtopic'] = newFullPath.split('/')[0:-1] + + for i in ['relPath', 'subtopic', 'baseUrl']: + if not i in msg: + msg[i] = msg['new_%s' % i] + + if sys.platform == 'win32': + if 'new_dir' not in msg: + msg['new_dir'] = msg['new_dir'].replace('\\', '/') + msg['new_relPath'] = msg['new_relPath'].replace('\\', '/') + if re.match('[A-Z]:', str(options.currentDir), + flags=re.IGNORECASE): + msg['new_dir'] = msg['new_dir'].lstrip('/') + msg['new_relPath'] = msg['new_relPath'].lstrip('/')
+ + +
+[docs] + def validate(msg): + """ + FIXME: used to be msg_validate + return True if message format seems ok, return True, else return False, log some reasons. + """ + if not type(msg) is sarracenia.Message: + logger.error( f"not a message") + return False + + res = True + for required_key in ['pubTime', 'baseUrl', 'relPath']: + if not required_key in msg: + logger.error( f'missing key: {required_key}' ) + res = False + + if not timeValidate(msg['pubTime']): + logger.error( f"malformed pubTime: {msg['pubTime']}") + res = False + + return res
+ + +
+[docs] + def getContent(msg,options=None): + """ + Retrieve the data referred to by a message. The data may be embedded + in the messate, or this routine may resolve a link to an external server + and download the data. + + does not handle authentication. + This routine is meant to be used with small files. using it to download + large files may be very inefficient. Untested in that use-case. + + Return value is the data. + + often on server where one is publishing data, the file is available as + a local file, and one can avoid the network usage by using a options.baseDir setting. + this behaviour can be disabled by not providing the options or not setting baseDir. + """ + + # inlined/embedded case. + if 'content' in msg: + if msg['content']['encoding'] == 'base64': + return b64decode(msg['content']['value']) + else: + return msg['content']['value'].encode('utf-8') + + path='' + if msg['baseUrl'].startswith('file:'): + pu = urllib.parse.urlparse(msg['baseUrl']) + path=pu.path + msg['relPath'] + logger.info( f"path: {path}") + elif options and hasattr(options,'baseDir') and options.baseDir: + # local file shortcut + path=options.baseDir + os.sep + msg['relPath'] + + if os.path.exists(path): + logger.info( f"reading local file path: {path} exists?: {os.path.exists(path)}" ) + with open(path,'rb') as f: + return f.read() + + # case requiring resolution. + if 'retrievePath' in msg: + retUrl = msg['baseUrl'] + '/' + msg['retrievePath'] + else: + retUrl = msg['baseUrl'] + '/' + msg['relPath'] + + logger.info( f"retrieving from: {retUrl}" ) + with urllib.request.urlopen(retUrl) as response: + return response.read()
+
+ + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/config.html b/_modules/sarracenia/config.html new file mode 100644 index 000000000..eb18c0dce --- /dev/null +++ b/_modules/sarracenia/config.html @@ -0,0 +1,3049 @@ + + + + + + sarracenia.config — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.config

+# This file is part of Sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Shared Services Canada, 2019
+#
+r"""
+
+Second version configuration parser
+
+FIXME: pas 2023/02/05...  missing options from v2: max_queue_size, outlet, pipe
+
+"""
+
+import argparse
+import copy
+import datetime
+import humanfriendly
+import inspect
+import logging
+
+import os
+import pathlib
+import pprint
+import re
+import shutil
+import socket
+import sys
+import time
+import urllib, urllib.parse
+
+from random import randint
+
+if sys.version_info[0] >= 3 and sys.version_info[1] < 8:
+    """
+        'extend' action not included in argparse prior to python 3.8
+        https://stackoverflow.com/questions/41152799/argparse-flatten-the-result-of-action-append
+    """
+ 
+    class ExtendAction(argparse.Action):
+
+        def __call__(self, parser, namespace, values, option_string=None):
+            items = getattr(namespace, self.dest) or []
+            items.extend(values)
+            setattr(namespace, self.dest, items)
+
+
+
+
+import sarracenia
+from sarracenia import durationToSeconds, site_config_dir, user_config_dir, user_cache_dir
+from sarracenia.featuredetection import features
+import sarracenia.credentials
+import sarracenia.flow
+import sarracenia.flowcb
+
+from sarracenia.flow.sarra import default_options as sarradefopts
+
+import sarracenia.identity.arbitrary
+
+import sarracenia.moth
+import sarracenia.identity
+import sarracenia.instance
+
+
+
+[docs] +class octal_number(int): + +
+[docs] + def __new__(cls, value): + if type(value) is str: + self = int(value,base=8) + elif type(value) is int: + self = value + return self
+ + +
+[docs] + def __str__(self) -> str: + return f"0o{self:o}"
+ + +
+[docs] + def __repr__(self) -> str: + return f"0o{self:o}"
+
+ + + +default_options = { + 'acceptSizeWrong': False, + 'acceptUnmatched': True, + 'amqp_consumer': False, + 'attempts': 3, + 'batch' : 100, + 'baseDir': None, + 'baseUrl_relPath': False, + 'block_reassemble': True, + 'delete': False, + 'documentRoot': None, + 'download': False, + 'dry_run': False, + 'filename': None, + 'flowMain': None, + 'inflight': None, + 'inline': False, + 'inlineOnly': False, + 'identity_method': 'sha512', + 'logMetrics': False, + 'logStdout': False, + 'nodupe_driver': 'disk', + 'nodupe_ttl': 0, + 'overwrite': True, + 'path': [], + 'permDefault' : octal_number(0), + 'permDirDefault' : octal_number(0o775), + 'permLog': octal_number(0o600), + 'post_documentRoot': None, + 'post_baseDir': None, + 'post_baseUrl': None, + 'post_format': 'v03', + 'realpathPost': False, + 'recursive' : True, + 'report': False, + 'retryEmptyBeforeExit': False, + 'retry_refilter': False, + 'sanity_log_dead': 9999, + 'sourceFromExchange': False, + 'sourceFromMessage': False, + 'sundew_compat_regex_first_match_is_zero': False, + 'sourceFromExchange': False, + 'sourceFromMessage': False, + 'topicCopy': False, + 'v2compatRenameDoublePost': False, + 'varTimeOffset': 0, + 'wololo': False +} + +count_options = [ + 'batch', 'count', 'exchangeSplit', 'instances', 'logRotateCount', 'no', + 'post_exchangeSplit', 'prefetch', 'messageCountMax', 'messageRateMax', + 'messageRateMin' +] + + +# all the boolean settings. +flag_options = [ 'acceptSizeWrong', 'acceptUnmatched', 'amqp_consumer', 'baseUrl_relPath', 'block_reassemble', 'debug', \ + 'delete', 'discard', 'download', 'dry_run', 'durable', 'exchangeDeclare', 'exchangeSplit', 'logReject', 'realpathFilter', \ + 'follow_symlinks', 'force_polling', 'inline', 'inlineOnly', 'inplace', 'logMetrics', 'logStdout', 'logReject', 'restore', \ + 'messageDebugDump', 'mirror', 'timeCopy', 'notify_only', 'overwrite', 'post_on_start', \ + 'permCopy', 'persistent', 'queueBind', 'queueDeclare', 'randomize', 'recursive', 'realpathPost', \ + 'reconnect', 'report', 'reset', 'retry_refilter', 'retryEmptyBeforeExit', 'save', + 'sundew_compat_regex_first_match_is_zero', 'sourceFromExchange', 'sourceFromMessage', 'topicCopy', + 'statehost', 'users', 'v2compatRenameDoublePost', 'wololo' + ] + +float_options = [ ] + +duration_options = [ + 'expire', 'housekeeping', 'logRotateInterval', 'message_ttl', 'fileAgeMax', 'fileAgeMin', \ + 'retry_ttl', 'sanity_log_dead', 'sleep', 'timeout', 'varTimeOffset' +] + +list_options = [ 'path', 'vip' ] + +# set, valid values of the set. +set_options = [ 'logEvents', 'fileEvents' ] + +set_choices = { + 'logEvents' : set(sarracenia.flowcb.entry_points + [ 'reject' ]), + 'fileEvents' : set( [ 'create', 'delete', 'link', 'mkdir', 'modify', 'rmdir' ] ) + } +# FIXME: doesn't work... wonder why? +# 'fileEvents': sarracenia.flow.allFileEvents + +perm_options = [ 'permDefault', 'permDirDefault','permLog'] + +size_options = ['accelThreshold', 'blocksize', 'bufsize', 'byteRateMax', 'inlineByteMax'] + +str_options = [ + 'action', 'admin', 'baseDir', 'broker', 'cluster', 'directory', 'exchange', + 'exchange_suffix', 'feeder', 'filename', 'flatten', 'flowMain', 'header', + 'hostname', 'identity', 'inlineEncoding', 'logLevel', + 'pollUrl', 'post_baseUrl', 'post_baseDir', 'post_broker', 'post_exchange', + 'post_exchangeSuffix', 'post_format', 'post_topic', 'queueName', 'sendTo', 'rename', + 'report_exchange', 'source', 'strip', 'timezone', 'nodupe_ttl', 'nodupe_driver', + 'nodupe_basis', 'tlsRigour', 'topic' +] + +r""" + for backward compatibility, + + convert some old plugins that are hard to get working with + v2wrapper, into v3 plugin. + + the fdelay ones makes in depth use of sr_replay function, and + that has changed in v3 too much. + + accelerators and rate limiting are now built-in, no plugin required. +""" + +convert_to_v3 = { + 'cache_stat' : ['continue'], + 'cluster_aliases' : [ 'continue' ], + 'discard' : [ 'delete_destination', 'on' ], + 'from_cluster' : [ 'continue' ], + 'to_clusters' : [ 'continue' ], + 'identity' : { + 'n' : [ 'identity', 'none' ], + 's' : [ 'identity', 'sha512' ], + 'd' : [ 'identity', 'md5' ], + 'a' : [ 'identity', 'arbitrary' ], + 'r' : [ 'identity', 'random' ], + 'z,d' : [ 'identity', 'cod,md5' ], + 'z,s' : [ 'identity', 'cod,sha512' ], + 'z,n' : [ 'identity', 'none' ] + }, + 'ls_file_index' : [ 'continue' ], + 'plugin': { + 'msg_fdelay': ['callback', 'filter.fdelay'], + 'msg_pclean_f90': + ['callback', 'filter.pclean_f90.PClean_F90'], + 'msg_pclean_f92': + ['callback', 'filter.pclean_f92.PClean_F92'], + 'accel_wget': ['continue'], + 'accel_scp': ['continue'], + 'accel_cp': ['continue'], + 'msg_total_save': ['continue'], + 'post_total_save': ['continue'], + 'post_total_interval': ['continue'] + }, + 'destfn_script': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_get': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_poll': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_put': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_download': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_put': { 'manual_conversion_required' : [ 'continue' ] }, + 'do_send': { + 'file_email' : [ 'callback', 'send.email' ], + }, + 'do_task': { 'manual_conversion_required' : [ 'continue' ] }, + 'no_download': [ 'download', 'False' ], + 'notify_only': [ 'download', 'False' ], + 'do_data': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_file': { + 'file_age' : [ 'callback', 'work.age' ], + }, + 'on_heartbeat': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_html_page': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_part': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_line': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_message': { + 'msg_by_source': ['continue'], + 'msg_by_user': ['continue'], + 'msg_delete': [ 'callback', 'filter.deleteflowfiles.DeleteFlowFiles'], + 'msg_download': ['continue'], + 'msg_dump': ['continue'], + 'msg_log': ['logEvents', '+after_accept'], + 'msg_print_lag': [ 'callback', 'accept.printlag.PrintLag'], + 'msg_rawlog': ['logEvents', '+after_accept'], + 'msg_total': ['continue'], + 'msg_replace_new_dir': [ 'callback', 'accept.pathreplace' ], + 'msg_skip_old': [ 'callback', 'accept.skipold.SkipOld'], + 'msg_test_retry': [ 'callback', 'accept.testretry.TestRetry'], + 'msg_to_clusters': [ 'callback', 'accept.toclusters.ToClusters'], + 'msg_save': [ 'callback', 'accept.save'], + 'msg_2localfile': [ 'callback', 'accept.tolocalfile.ToLocalFile'], + 'msg_rename_whatfn': [ 'callback', 'accept.renamewhatfn.RenameWhatFn'], + 'msg_rename_dmf': [ 'callback', 'accept.renamedmf.RenameDMF'], + 'msg_hour_tree': [ 'callback', 'accept.hourtree.HourTree'], + 'msg_renamer': [ 'callback', 'accept.renamer.Renamer'], + 'msg_2http': [ 'callback', 'accept.tohttp.ToHttp'], + 'msg_2local': [ 'callback', 'accept.tolocal.ToLocal'], + 'msg_http_to_https': [ 'callback', 'accept.httptohttps.HttpToHttps'], + 'msg_speedo': [ 'callback', 'accept.speedo.Speedo'], + 'msg_WMO_type_suffix': [ 'callback', 'accept.wmotypesuffix.WmoTypeSuffix'], + 'msg_sundew_pxroute': [ 'callback', 'accept.sundewpxroute.SundewPxRoute'], + 'msg_rename4jicc': [ 'flow_callback', 'accept.rename4jicc.Rename4Jicc'], + 'msg_delay': [ 'callback', 'accept.messagedelay.MessageDelay'], + 'msg_download_baseurl': [ 'callback', 'accept.downloadbaseurl.DownloadBaseUrl'], + 'msg_from_cluster': ['continue'], + 'msg_stdfiles': ['continue'], + 'msg_fdelay': ['callback', 'filter.fdelay'], + 'msg_stopper': ['continue'], + 'msg_overwrite_sum': ['continue'], + 'msg_gts2wistopic': ['continue'], + }, + 'on_report': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_stop': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_start': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_watch': { 'manual_conversion_required' : [ 'continue' ] }, + 'on_post': { + 'post_log': ['logEvents', '+after_work'], + 'post_total': ['continue'], + 'wmo2msc': [ 'callback', 'filter.wmo2msc.Wmo2Msc'], + 'post_hour_tree': [ 'callback', 'accept.posthourtree.PostHourTree'], + 'post_long_flow': [ 'callback', 'accept.longflow.LongFLow'], + 'post_override': [ 'callback', 'accept.postoverride.PostOverride'], + 'post_rate_limit': ['continue'], + 'to': ['continue'] + }, + 'parts' : [ 'continue' ], + 'poll_without_vip': [ 'manual_conversion_required' ], + 'pump' : [ 'continue' ], + 'pump_flag' : [ 'continue' ], + 'reconnect': ['continue'], + 'report_daemons': ['continue'], + 'restore' : [ 'continue' ], + 'retry_mode' : ['continue'], + 'save' : [ 'continue' ], + 'set_passwords': ['continue'], + 'windows_run': [ 'continue' ], + 'xattr_disable': [ 'continue' ] +} + +# question: why don't these have matching closing braces? +# answer: there might be an offset (-1h, -5m, etc...) and covering those cases is hard with simple substitution. + +convert_patterns_to_v3 = { + '${YYYYMMDD' : '${%Y%m%d', + '${YYYY': '${%Y', + '${JJJ': '${%j', + '${HH': '${%H', + '${DD': '${%d', + '${MM': '${%m', + '${SS': '${%S', + +} + +logger = logging.getLogger(__name__) + +r""" + FIXME: respect appdir stuff using an environment variable. + for not just hard coded as a class variable appdir_stuff + +""" + + +def isTrue(S): + if type(S) is list: + S = S[-1] + return S.lower() in ['true', 'yes', 'on', '1'] + +def parse_count(cstr): + if cstr[0] == '-': + offset=1 + else: + offset=0 + count=humanfriendly.parse_size(cstr[offset:], binary=cstr[-1].lower() in ['i','b'] ) + return -count if offset else count + +def get_package_lib_dir(): + return os.path.dirname(inspect.getfile(Config)) + + +def get_site_config_dir(): + return sarracenia.site_config_dir(Config.appdir_stuff['appname'], + Config.appdir_stuff['appauthor']) + + +
+[docs] +def get_user_cache_dir(hostdir): + """ + hostdir = None if statehost is false, + """ + ucd = sarracenia.user_cache_dir(Config.appdir_stuff['appname'], + Config.appdir_stuff['appauthor']) + if hostdir: + ucd = os.path.join(ucd, hostdir) + return ucd
+ + + +def get_user_config_dir(): + return sarracenia.user_config_dir(Config.appdir_stuff['appname'], + Config.appdir_stuff['appauthor']) + + +
+[docs] +def get_pid_filename(hostdir, component, configuration, no): + """ + return the file name for the pid file for the specified instance. + """ + piddir = get_user_cache_dir(hostdir) + piddir += os.sep + component + os.sep + + if configuration[-5:] == '.conf': + configuration = configuration[:-5] + + piddir += configuration + os.sep + + return piddir + os.sep + component + '_' + configuration + '_%02d' % no + '.pid'
+ + + +
+[docs] +def get_log_filename(hostdir, component, configuration, no): + """ + return the name of a single logfile for a single instance. + """ + logdir = get_user_cache_dir(hostdir) + os.sep + 'log' + + if configuration is None: + configuration = '' + else: + configuration = '_' + configuration + + if configuration[-5:] == '.conf': + configuration = configuration[:-5] + + return logdir + os.sep + component + configuration + '_%02d' % no + '.log'
+ + +
+[docs] +def get_metrics_filename(hostdir, component, configuration, no): + """ + return the name of a single logfile for a single instance. + """ + metricsdir = get_user_cache_dir(hostdir) + os.sep + 'metrics' + + if configuration is None: + configuration = '' + else: + configuration = '_' + configuration + + if configuration[-5:] == '.conf': + configuration = configuration[:-5] + + return metricsdir + os.sep + component + configuration + '_%02d' % no + '.json'
+ + +def wget_config(urlstr, path, remote_config_url=False): + logger.debug("wget_config %s %s" % (urlstr, path)) + + try: + req = urllib.request.Request(urlstr) + resp = urllib.request.urlopen(req) + if os.path.isfile(path): + try: + info = resp.info() + ts = time.strptime(info.get('Last-Modified'), + "%a, %d %b %Y %H:%M:%S %Z") + last_mod_remote = time.mktime(ts) + last_mod_local = os.stat(path).st_mtime + if last_mod_remote <= last_mod_local: + logger.info("file %s is up to date (%s)" % (path, urlstr)) + return True + except: + logger.error( + "could not compare modification dates... downloading") + logger.debug('Exception details: ', exc_info=True) + + fp = open(path + '.downloading', 'wb') + + # top program config only needs to keep the url + # we set option remote_config_url with the urlstr + # at the first line of the config... + # includes/plugins etc... may be left as url in the config... + # as the urlstr is kept in the config this option would be useless + # (and damagable for plugins) + + if remote_config_url: + fp.write(bytes("remote_config_url %s\n" % urlstr, 'utf-8')) + while True: + chunk = resp.read(8192) + if not chunk: break + fp.write(chunk) + fp.close() + + try: + os.unlink(path) + except: + pass + os.rename(path + '.downloading', path) + + logger.info("file %s downloaded (%s)" % (path, urlstr)) + + return True + + except urllib.error.HTTPError as e: + if os.path.isfile(path): + logger.warning('file %s could not be processed1 (%s)' % + (path, urlstr)) + logger.warning('resume with the one on the server') + else: + logger.error('Download failed 0: %s' % urlstr) + logger.error('Server couldn\'t fulfill the request') + logger.error('Error code: %s, %s' % (e.code, e.reason)) + + except urllib.error.URLError as e: + if os.path.isfile(path): + logger.warning('file %s could not be processed2 (%s)' % + (path, urlstr)) + logger.warning('resume with the one on the server') + else: + logger.error('Download failed 1: %s' % urlstr) + logger.error('Failed to reach server. Reason: %s' % e.reason) + + except Exception as e: + if os.path.isfile(path): + logger.warning('file %s could not be processed3 (%s) %s' % + (path, urlstr, e.reason)) + logger.warning('resume with the one on the server') + else: + logger.error('Download failed 2: %s %s' % (urlstr, e.reason)) + logger.debug('Exception details: ', exc_info=True) + + try: + os.unlink(path + '.downloading') + except: + pass + + if os.path.isfile(path): + logger.warning("continue using existing %s" % path) + + return False + + +
+[docs] +def config_path(subdir, config, mandatory=True, ctype='conf'): + """ + Given a subdir/config look for file in configish places. + + return Tuple: Found (True/False), path_of_file_found|config_that_was_not_found + """ + logger.debug("config_path = %s %s" % (subdir, config)) + + if config == None: return False, None + + # remote config + + if config.startswith('http:'): + urlstr = config + name = os.path.basename(config) + if not name.endswith(ctype): name += '.' + ctype + path = get_user_config_dir() + os.sep + subdir + os.sep + name + config = name + + logger.debug("http url %s path %s name %s" % (urlstr, path, name)) + + # do not allow plugin (Peter's mandatory decision) + # because plugins may need system or python packages + # that may not be installed on the current server. + if subdir == 'plugins': + logger.error("it is not allowed to download plugins") + else: + ok = Config.wget_config(urlstr, path) + + # priority 1 : config given is a valid path + + logger.debug("config_path %s " % config) + if os.path.isfile(config): + return True, config + config_file = os.path.basename(config) + config_name = re.sub(r'(\.inc|\.conf|\.py)', '', config_file) + ext = config_file.replace(config_name, '') + if ext == '': ext = '.' + ctype + config_path = config_name + ext + + # priority 1.5: config file given without extenion... + if os.path.isfile(config_path): + return True, config_path + + # priority 2 : config given is a user one + + config_path = os.path.join(get_user_config_dir(), subdir, + config_name + ext) + logger.debug("config_path %s " % config_path) + + if os.path.isfile(config_path): + return True, config_path + + # priority 3 : config given to site config + + config_path = os.path.join(get_site_config_dir(), subdir, + config_name + ext) + logger.debug("config_path %s " % config_path) + + if os.path.isfile(config_path): + return True, config_path + + # priority 4 : plugins + + if subdir == 'plugins': + config_path = get_package_lib_dir( + ) + os.sep + 'plugins' + os.sep + config_name + ext + logger.debug("config_path %s " % config_path) + if os.path.isfile(config_path): + return True, config_path + + # return bad file ... + if mandatory: + if subdir == 'plugins': logger.error("script not found %s" % config) + elif config_name != 'plugins': + logger.error("file not found %s" % config) + + return False, config
+ + + +
+[docs] +class Config: + r""" + The option parser to produce a single configuration. + + it can be instantiated with one of: + + * one_config(component, config, action, isPost=False) -- read the options for + a given component an configuration, (all in one call.) + + On the other hand, a configu can be built up from the following constructors: + + * default_config() -- returns an empty configuration, given a config file tree. + * no_file_config() -- returns an empty config without any config file tree. + + Then just add settings manually:: + + cfg = no_file_config() + + cfg.broker = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca') + cfg.topicPrefix = [ 'v02', 'post'] + cfg.component = 'subscribe' + cfg.config = 'flow_demo' + cfg.action = 'start' + cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ] + cfg.queueName='q_anonymous.subscriber_test2' + cfg.download=True + cfg.batch=1 + cfg.messageCountMax=5 + + # set the instance number for the flow class. + cfg.no=0 + + # and at the end call finalize + + cfg.finalize() + + """ + + port_required = [ 'on_line', 'on_html_page' ] + + v2entry_points = [ + 'do_download', 'do_get', 'do_poll', 'do_put', 'do_send', 'on_message', + 'on_file', 'on_heartbeat', 'on_housekeeping', + 'on_html_page', 'on_part', 'on_post', 'on_report', + 'on_start', 'on_stop', 'on_watch', 'plugin' + ] + components = [ + 'cpost', 'cpump', 'flow', 'poll', 'post', 'sarra', 'sender', 'shovel', + 'subscribe', 'watch', 'winnow' + ] + + actions = [ + 'add', 'cleanup', 'convert', 'devsnap', 'declare', 'disable', 'dump', 'edit', + 'enable', 'features', 'foreground', 'log', 'list', 'remove', 'restart', 'run', 'sanity', + 'setup', 'show', 'start', 'stop', 'status', 'overview' + ] + + # lookup in dictionary, respond with canonical version. + appdir_stuff = {'appauthor': 'MetPX', 'appname': 'sr3'} + + # Correct name on the right, old name on the left. + synonyms = { + 'a': 'action', + 'accel_cp_threshold': 'accelThreshold', + 'accel_scp_threshold': 'accelThreshold', + 'accel_wget_threshold': 'accelThreshold', + 'accept_unmatch': 'acceptUnmatched', + 'accept_unmatched': 'acceptUnmatched', + 'at': 'attempts', + 'b': 'broker', + 'bd': 'baseDir', + 'basedir': 'baseDir', + 'base_dir': 'baseDir', + 'baseurl': 'baseUrl', + 'bind_queue': 'queueBind', + 'cache': 'nodupe_ttl', + 'c': 'include', + 'cb': 'nodupe_basis', + 'cache_basis': 'nodupe_basis', + 'caching': 'nodupe_ttl', + 'chmod': 'permDefault', + 'chmod_dir': 'permDirDefault', + 'chmod_log': 'permLog', + 'content' : 'inline', + 'content_encoding': 'inlineEncoding', + 'content_max': 'inlineByteMax', + 'd': 'discard', + 'declare_exchange': 'exchangeDeclare', + 'declare_queue': 'queueDeclare', + 'default_mode': 'permDefault', + 'default_dir_mode': 'permDirDefault', + 'default_log_mode': 'permLog', + 'destination_timezone': 'timezone', + 'document_root': 'documentRoot', + 'download-and-discard': 'discard', + 'e' : 'fileEvents', + 'events' : 'fileEvents', + 'ex': 'exchange', + 'exchange_split': 'exchangeSplit', + 'exchange_suffix': 'exchangeSuffix', + 'expiry': 'expire', + 'file_time_limit' : 'fileAgeMax', + 'nodupe_fileAgeMax' : 'fileAgeMax', + 'nodupe_fileAgeMin' : 'fileAgeMin', + 'fp' : 'force_polling', + 'fs' : 'follow_symlinks', + 'h' : 'help', + 'heartbeat': 'housekeeping', + 'hb_memory_baseline_file' : 'MemoryBaseLineFile', + 'hb_memory_max' : 'MemoryMax', + 'hb_memory_multiplier' : 'MemoryMultiplier', + 'imx': 'inlineByteMax', + 'inl' : 'inline', + 'inline_encoding': 'inlineEncoding', + 'inline_max': 'inlineByteMax', + 'instance': 'instances', + 'lock': 'inflight', + 'log_format': 'logFormat', + 'll': 'logLevel', + 'loglevel': 'logLevel', + 'log_reject': 'logReject', + 'logdays': 'logRotateCount', + 'log_rotate': 'logRotateCount', + 'logRotate': 'logRotateCount', + 'logRotate': 'logRotateCount', + 'logRotate_interval': 'logRotateInterval', + 'message-ttl': 'message_ttl', + 'msg_replace_new_dir' : 'pathReplace', + 'msg_filter_wmo2msc_replace_dir': 'filter_wmo2msc_replace_dir', + 'msg_filter_wmo2msc_uniquify': 'filter_wmo2msc_uniquify', + 'msg_filter_wmo2msc_tree': 'filter_wmo2msc_treeify', + 'msg_filter_wmo2msc_convert': 'filter_wmo2msc_convert', + 'msg_fdelay' : 'fdelay', + 'n': 'no_download', + 'nd': 'nodupe_ttl', + 'no_duplicates': 'nodupe_ttl', + 'o' : 'overwrite', + 'on_msg': 'on_message', + 'p' : 'path', + 'pm' : 'permCopy', + 'post_base_dir': 'post_baseDir', + 'post_basedir': 'post_baseDir', + 'post_base_url': 'post_baseUrl', + 'post_baseurl': 'post_baseUrl', + 'post_document_root': 'post_documentRoot', + 'post_exchange_split': 'post_exchangeSplit', + 'post_exchange_suffix': 'post_exchangeSuffix', + 'post_rate_limit': 'messageRateMax', + 'post_topic_prefix' : 'post_topicPrefix', + 'preserve_mode' : 'permCopy', + 'preserve_time' : 'timeCopy', + 'pt' : 'timeCopy', + 'qn': 'queueName', + 'queue' : 'queueName', + 'queue_name' : 'queueName', + 'realpath' : 'realpathPost', + 'realpath_filter' : 'realpathFilter', + 'realpath_post' : 'realpathPost', + 'remoteUrl' : 'sendTo', + 'report_back': 'report', + 'sd' : 'nodupe_ttl', + 'sdb' : 'nodupe_basis', + 'simulate': 'dry_run', + 'simulation': 'dry_run', + 'source_from_exchange': 'sourceFromExchange', + 'sum' : 'identity', + 'suppress_duplicates' : 'nodupe_ttl', + 'suppress_duplicates_basis' : 'nodupe_basis', + 'tls_rigour' : 'tlsRigour', + 'topic_prefix' : 'topicPrefix' + } + credentials = None + +
+[docs] + def __init__(self, parent=None) -> 'Config': + """ + instantiate an empty Configuration + """ + self.bindings = [] + self.__admin = None + self.__broker = None + self.__post_broker = None + + if Config.credentials is None: + Config.credentials = sarracenia.credentials.CredentialDB() + Config.credentials.read(get_user_config_dir() + os.sep + + "credentials.conf") + self.directory = None + + self.env = copy.deepcopy(os.environ) + + egdir = os.path.dirname(inspect.getfile(sarracenia.config.Config)) + os.sep + 'examples' + + self.config_search_path = [ "." , get_user_config_dir(), egdir, egdir + os.sep + 'flow' ] + + + for k in default_options: + setattr(self, k, default_options[k]) + + if parent is not None: + for i in parent: + setattr(self, i, parent[i]) + + self.bufsize = 1024 * 1024 + self.byteRateMax = 0 + + self.fileAgeMax = 0 # disabled. + self.fileAgeMin = 0 # disabled. + self.timezone = 'UTC' + self.debug = False + self.declared_exchanges = [] + self.discard = False + self.displayFull = False + self.dry_run = False + self.env_declared = [] # list of variable that are "declared env"'d + self.files = [] + self.lineno = 0 + self.v2plugins = {} + self.v2plugin_options = [] + self.imports = [] + self.logEvents = set(['after_accept', 'after_post', 'after_work', 'on_housekeeping' ]) + self.destfn_scripts = [] + self.plugins_late = [] + self.plugins_early = [] + self.exchange = None + self.filename = None + self.fixed_headers = {} + self.flatten = '/' + self.hostname = socket.getfqdn() + self.hostdir = socket.getfqdn().split('.')[0] + self.log_flowcb_needed = False + self.sleep = 0.1 + self.housekeeping = 300 + self.inline = False + self.inlineByteMax = 4096 + self.inlineEncoding = 'guess' + self.identity_arbitrary_value = None + self.logReject = False + self.logRotateCount = 5 + self.logRotateInterval = 60*60*24 + self.masks = [] + self.instances = 1 + self.mirror = False + self.messageAgeMax = 0 + self.post_exchanges = [] + #self.post_topicPrefix = None + self.pstrip = False + self.queueName = None + self.randomize = False + self.rename = None + self.randid = "%04x" % randint(0, 65536) + self.statehost = False + self.settings = {} + self.strip = 0 + self.timeout = 300 + self.tlsRigour = 'normal' + self.topicPrefix = [ 'v03', 'post' ] + self.undeclared = [] + self.declared_users = {} + self.users = False + self.vip = []
+ + +
+[docs] + def __deepcopy__(self, memo) -> 'Configuration': + r""" + code for this from here: https://stackoverflow.com/questions/1500718/how-to-override-the-copy-deepcopy-operations-for-a-python-object + Needed for python < 3.7ish? (ubuntu 18) found this bug: https://bugs.python.org/issue10076 + deepcopy fails for objects with re's in them? + ok on ubuntu 20.04 + """ + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k == 'masks': + v2=[] + for m in v: + v2.append(tuple(list(copy.deepcopy(m[0:3]))+ [m[3]] + list(copy.deepcopy(m[4:])))) + + setattr(result, k, v2) + else: + setattr(result, k, copy.deepcopy(v, memo)) + return result
+ + +
+[docs] + def _validate_urlstr(self, urlstr) -> tuple : + """ + returns a tuple ( bool, expanded_url ) + the bool is whether the expansion worked, and the expanded_url is one with + the added necessary authentication details from sarracenia.Credentials. + + """ + # check url and add credentials if needed from credential file + ok, cred_details = Config.credentials.get(urlstr) + if cred_details is None: + logging.critical("bad credential %s" % urlstr) + # Callers expect that a Credential object will be returned + cred_details = sarracenia.credentials.Credential() + cred_details.url = urllib.parse.urlparse(urlstr) + return False, cred_details + return True, cred_details
+ + +
+[docs] + def applyComponentDefaults( self, component ): + """ + overlay defaults options for the given component to the given configuration. + """ + if component in ['post']: + self.override(sarracenia.flow.post.default_options) + elif component in ['poll']: + self.override(sarracenia.flow.poll.default_options) + elif component in ['sarra']: + self.override(sarradefopts) + elif component in ['sender']: + self.override(sarracenia.flow.sender.default_options) + elif component in ['subscribe']: + self.override(sarracenia.flow.subscribe.default_options) + elif component in ['watch']: + self.override(sarracenia.flow.watch.default_options)
+ + + @property + def admin(self): + return self.__admin + + @admin.setter + def admin(self, v): + if type(v) is str: + ok, cred_details = self._validate_urlstr(v) + if ok: + self.__admin = cred_details + else: + self.__admin = v + + @property + def broker(self): + return self.__broker + + @broker.setter + def broker(self, v): + if type(v) is str: + ok, cred_details = self._validate_urlstr(v) + if ok: + self.__broker = cred_details + else: + self.__broker = v + + @property + def post_broker(self): + return self.__post_broker + + @post_broker.setter + def post_broker(self, v): + if type(v) is str: + ok, cred_details = self._validate_urlstr(v) + if ok: + self.__post_broker = cred_details + else: + self.__post_broker = v + +
+[docs] + def _varsub(self, word): + """ substitute variable values from options + """ + + if word is None: + return word + elif type(word) in [bool, int, float, octal_number]: + return word + elif not '$' in word: + return word + + result = word + if (('${BROKER_USER}' in word) and hasattr(self, 'broker') and self.broker is not None and + self.broker.url is not None and hasattr(self.broker.url, 'username')): + result = result.replace('${BROKER_USER}', self.broker.url.username) + # FIXME: would this work also automagically if BROKER.USERNAME ? + + if (('${POST_BROKER_USER}' in word) and hasattr(self, 'post_broker') and self.post_broker is not None and + self.post_broker.url is not None and hasattr(self.post_broker.url, 'username')): + result = result.replace('${POST_BROKER_USER}', self.post_broker.url.username) + + if not '$' in result: + return result + + elst = [] + plst = result.split('}') + for parts in plst: + try: + if '{' in parts: elst.append((parts.split('{'))[1]) + except: + pass + for E in elst: + if E in ['PROGRAM']: + e = 'component' + else: + e = E.lower() + + if hasattr(self, e): + repval = getattr(self, e) + if type(repval) is list: + repval = repval[0] + result = result.replace('${' + E + '}', repval) + continue + + if E in self.env.keys(): + result = result.replace('${' + E + '}', self.env[E]) + if sys.platform == 'win32': + result = result.replace('\\', '/') + return result
+ + +
+[docs] + def _build_mask(self, option, arguments): + """ return new entry to be appended to list of masks + """ + try: + regex = re.compile(arguments[0]) + except: + logger.critical( f"{','.join(self.files)}{self.lineno} invalid regular expression: {arguments[0]}, ignored." ) + return None + + if len(arguments) > 1: + fn = arguments[1] + else: + fn = self.filename + if fn and re.compile('DESTFNSCRIPT=.*').match(fn): + script=fn[13:] + self.destfn_scripts.append(script) + + if self.directory: + d = os.path.expanduser(self.directory) + else: + d = self.directory + return (arguments[0], d, fn, regex, + option.lower() in ['accept' ], self.mirror, self.strip, + self.pstrip, self.flatten)
+ + +
+[docs] + def mask_ppstr(self, mask): + """ + return a pretty print string version of the given mask, easier for humans to read. + """ + pattern, maskDir, maskFileOption, mask_regexp, accepting, mirror, strip, pstrip, flatten = mask + + s = 'accept' if accepting else 'reject' + if pstrip : strip=pstrip + strip = '' if strip == 0 else f' strip:{strip}' + fn = '' if (maskFileOption == 'WHATFN') else f' filename:{maskFileOption}' + flatten = '' if flatten == '/' else f' flatten:{flatten}' + w = 'with ' if fn or flatten or strip else '' + return f'{s} {pattern} into {maskDir} {w}mirror:{mirror}{strip}{flatten}{fn}'
+ + +
+[docs] + def _parse_set_string( self, v:str, old_value: set ) -> set: + """ + given a set string, return a python set. + """ + sv=set() + if type(v) is list: + sv=set(v) + elif type(v) is set: + sv=v + elif type(v) is str: + v=v.replace('|',',') + if v == 'None': + sv=set([]) + else: + if v[0] in [ '+', '-']: + op=v[0] + v=v[1:] + else: + op='r' + + if ',' in v: + sv=set(v.split(',')) + else: + sv=set([v]) + + if op == '+': + sv= old_value | sv + elif op == '-' : + sv= old_value - sv + return sv
+ + +
+[docs] + def add_option(self, option, kind='list', default_value=None, all_values=None ): + r""" + options can be declared in any plugin. There are various *kind* of options, where the declared type modifies the parsing. + + * 'count' integer count type. + + * 'octal' base-8 (octal) integer type. + * 'duration' a floating point number indicating a quantity of seconds (0.001 is 1 milisecond) + modified by a unit suffix ( m-minute, h-hour, w-week ) + + * 'flag' boolean (True/False) option. + + * 'float' a simple floating point number. + + * 'list' a list of string values, each succeeding occurrence catenates to the total. + all v2 plugin options are declared of type list. + + * 'set' a set of string values, each succeeding occurrence is unioned to the total. + if all_values is provided, then constrain set to that. + + * 'size' integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers. + + * 'str' an arbitrary string value, as will all of the above types, each + succeeding occurrence overrides the previous one. + + If a value is set to None, that could mean that it has not been set. + """ + #Blindly add the option to the list if it doesn't already exist + if not hasattr(self, option): + setattr(self, option, default_value) + + # Retreive the 'new' option & enforce the correct type. + v = getattr(self, option) + + if kind not in [ 'list', 'set' ] and type(v) == list: + v=v[-1] + logger.warning( f"{','.join(self.files)}{self.lineno} multiple declarations of {kind} {option}={getattr(self,option)} choosing last one: {v}" ) + + + if kind == 'count': + count_options.append(option) + if type(v) is not int: + setattr(self, option, parse_count(v)) + elif kind == 'duration': + duration_options.append(option) + if type(v) is not float: + setattr(self, option, durationToSeconds(v,default_value)) + elif kind == 'flag' or kind == bool: + flag_options.append(option) + if type(v) is not bool: + setattr(self, option, isTrue(v)) + elif kind == 'float' or kind == float : + float_options.append(option) + if type(v) is not float: + setattr(self, option, float(v)) + elif kind == 'list' or kind == list: + list_options.append( option ) + if type(v) is not list: + #subtlety... None means: has not been set, + # where an empty list to be an explicit setting. + if v is None: + setattr(self, option, None) + else: + setattr(self, option, [v]) + elif kind == 'octal': + perm_options.append(option) + if type(v) is not octal_number: + setattr(self, option, octal_number(int(v,base=8))) + elif kind == 'set': + set_options.append(option) + sv = self._parse_set_string(v,set()) + setattr(self, option, sv) + if all_values: + set_choices[option] = all_values + + elif kind == 'size' or kind == int: + size_options.append(option) + if type(v) is not int: + setattr(self, option, parse_count(v)) + + elif kind == 'str' or kind == str: + str_options.append(option) + if v is None: + setattr(self, option, None) + elif type(v) is not str: + setattr(self, option, str(v)) + else: + logger.error( f"{','.join(self.files)}{self.lineno} invalid kind: {kind} for option: {option} ignored" ) + return + + logger.debug( f"{','.join(self.files)}{self.lineno} {option} declared as type:{type(getattr(self,option))} value:{v}" )
+ + +
+[docs] + def dump(self): + """ print out what the configuration looks like. + """ + + term = shutil.get_terminal_size((80, 20)) + + # for python > 3.7 + #c = copy.deepcopy(self.dictify()) + # but older python needs: + c = self.dictify() + d={} + for k in c: + if k == 'masks': + i=0 + d['masks'] = [] + while i < len(c['masks']): + d['masks'].append( self.mask_ppstr(c['masks'][i]) ) + i+=1 + else: + d[k] = copy.deepcopy(c[k]) + + for omit in [ 'env' ] : + del d[omit] + + for k in d: + if type(d[k]) is sarracenia.credentials.Credential : + d[k] = str(d[k]) + + pprint.pprint( d, width=term.columns, compact=True ) + return
+ + +
+[docs] + def dictify(self): + """ + return a dict version of the cfg... + """ + cd = self.__dict__ + + if hasattr(self, 'admin'): + cd['admin'] = self.admin + + if hasattr(self, 'broker'): + cd['broker'] = self.broker + + if hasattr(self, 'post_broker'): + cd['post_broker'] = self.post_broker + + return cd
+ + + + def get_source_from_exchange(self,exchange): + #self.logger.debug("%s get_source_from_exchange %s" % (self.program_name,exchange)) + + source = None + if len(exchange) < 4 or not exchange.startswith('xs_') : return source + + # check if source is a valid declared source user + + len_u = 0 + try: + # look for user with role source + for u in self.declared_users : + if self.declared_users[u] != 'source' : continue + if exchange[3:].startswith(u) and len(u) > len_u : + source = u + len_u = len(u) + except: pass + + return source + + + + def _merge_field(self, key, value): + if key == 'masks': + self.masks += value + else: + if value is not None: + setattr(self, key, value) + +
+[docs] + def merge(self, oth): + """ + merge to lists of options. + + merge two lists of options if one is cumulative then merge, + otherwise if not None, then take value from oth + """ + + if type(oth) == dict: + for k in oth.keys(): + self._merge_field(k, self._varsub(oth[k])) + else: + for k in oth.__dict__.keys(): + self._merge_field(k, self._varsub(getattr(oth, k)))
+ + + def _override_field(self, key, value): + if key == 'masks': + self.masks += value + else: + setattr(self, key, value) + +
+[docs] + def override(self, oth): + """ + override a value in a set of options. + + why override() method and not just assign values to the dictionary? + in the configuration file, there are various ways to have variable substituion. + override invokes those, so that they are properly interpreted. Otherwise, + you just end up with a literal value. + """ + + if type(oth) == dict: + for k in oth.keys(): + self._override_field(k, self._varsub(oth[k])) + else: + for k in oth.__dict__.keys(): + self._override_field(k, self._varsub(getattr(oth, k)))
+ + +
+[docs] + def _resolve_exchange(self): + """ + based on the given configuration, fill in with defaults or guesses. + sets self.exchange. + """ + if not hasattr(self, 'exchange') or self.exchange is None: + #if hasattr(self, 'post_broker') and self.post_broker is not None and self.post_broker.url is not None: + # self.exchange = 'xs_%s' % self.post_broker.url.username + #else: + if not hasattr(self.broker.url,'username') or ( self.broker.url.username == 'anonymous' ): + self.exchange = 'xpublic' + else: + self.exchange = 'xs_%s' % self.broker.url.username + + if hasattr(self, 'exchangeSuffix'): + self.exchange += '_%s' % self.exchangeSuffix + + if hasattr(self, 'exchangeSplit') and hasattr( + self, 'no') and (self.no > 0): + self.exchange += "%02d" % self.no
+ + +
+[docs] + def _parse_binding(self, subtopic_string): + """ + FIXME: see original parse, with substitions for url encoding. + also should sqwawk about error if no exchange or topicPrefix defined. + also None to reset to empty, not done. + """ + if hasattr(self, 'broker') and self.broker is not None and self.broker.url is not None: + self._resolve_exchange() + + if type(subtopic_string) is str: + if not hasattr(self, 'broker') or self.broker is None or self.broker.url is None: + logger.error( f"{','.join(self.files)}:{self.lineno} broker needed before subtopic" ) + return + + if self.broker.url.scheme == 'amq' : + subtopic = subtopic_string.split('.') + else: + subtopic = subtopic_string.split('/') + + if hasattr(self, 'exchange') and hasattr(self, 'topicPrefix'): + self.bindings.append((self.exchange, self.topicPrefix, subtopic))
+ + +
+[docs] + def _parse_v2plugin(self, entryPoint, value): + """ + config file parsing for a v2 plugin. + + """ + if not entryPoint in Config.v2entry_points: + logging.error( + "undefined entry point: {} skipped".format(entryPoint)) + return + + if not entryPoint in self.v2plugins: + self.v2plugins[entryPoint] = [value] + else: + self.v2plugins[entryPoint].append(value)
+ + + def _parse_declare(self, words): + + if words[0] in ['env', 'envvar', 'var', 'value']: + name, value = words[1].split('=') + self.env[name] = value + self.env_declared.append(name) + elif words[0] in ['option', 'o']: + self._parse_option(words[1], words[2:]) + elif words[0] in ['source', 'subscriber', 'subscribe']: + self.declared_users[words[1]] = words[0] + elif words[0] in ['exchange']: + self.declared_exchanges.append(words[1]) + +
+[docs] + def _parse_setting(self, opt, value): + """ + v3 plugin accept options for specific modules. + + parsed from: + set sarracenia.flowcb.log.filter.Log.level debug + + example: + opt= sarracenia.flowcb.log.filter.Log.level value = debug + + results in: + self.settings[ sarracenia.flowcb.log.filter.Log ][level] = debug + + options should be fed to plugin class on instantiation. + stripped of class... + * options = { 'level' : 'debug' } + + + """ + opt_class = '.'.join(opt.split('.')[:-1]) + opt_var = opt.split('.')[-1] + if opt_class not in self.settings: + self.settings[opt_class] = {} + + self.settings[opt_class][opt_var] = ' '.join(value)
+ + + def _parse_sum(self, value): + #logger.error('FIXME! input value: %s' % value) + + if not value: + if not self.identity_method: + return + value = self.identity_method + + if (value in sarracenia.identity.known_methods) or ( + value[0:4] == 'cod,'): + self.identity_method = value + #logger.error('returning 1: %s' % value) + return + + #logger.error( f'1 value: {value} self.identity_method={self.identity_method}' ) + if (value[0:2] == 'z,'): + value = value[2:] + self.identity_method = 'cod,' + elif (value[0:2] == 'a,'): + self.identity_method = 'arbitrary' + self.identity_arbitrary_value = value[2:] + else: + self.identity_method = value + #logger.error( f'2 value: {value} self.identity_method={self.identity_method}' ) + + if value.lower() in [ 'n', 'none' ]: + self.identity_method = None + #logger.error('returning 1.1: %s' % 'none') + return + #logger.error( f'3 value: {value} self.identity_method={self.identity_method}' ) + + for sc in sarracenia.identity.Identity.__subclasses__(): + #logger.error('against 1.8: %s' % sc.__name__.lower() ) + if value == sc.__name__.lower(): + #logger.error('returning 2: %s' % value ) + if self.identity_method == 'cod,': + self.identity_method += value + else: + self.identity_method = value + return + if hasattr(sc, 'registered_as'): + #logger.error('against 3: %s' % sc.registered_as() ) + + if (sc.registered_as() == value): + if self.identity_method == 'cod,': + self.identity_method += sc.__name__.lower() + else: + self.identity_method = sc.__name__.lower() + #logger.error('returning 3: %s' % self.identity_method) + return + # FIXME this is an error return case, how to designate an invalid checksum? + self.identity_method = 'invalid' + #logger.error('returning 4: invalid' ) + + + + +
+[docs] + def parse_file(self, cfg, component=None): + """ add settings from a given config file to self + """ + if component: + cfname = f'{component}/{cfg}' + else: + cfname = cfg + + logger.debug( f'looking for {cfg} (in {os.getcwd()}') + + cfg=os.path.expanduser(cfg) + + if cfg[0] == os.sep: + cfgfilepath=cfg + else: + cfgfilepath=None + for d in self.config_search_path: + cfgfilepath=d + os.sep + cfg + if os.path.isfile( cfgfilepath ): + break + + if not cfgfilepath: + logger.error( f'failed to find {cfg}' ) + return + logger.debug( f'found {cfgfilepath}') + + lineno=0 + saved_lineno=0 + self.files.append(cfgfilepath) + + for l in open(cfgfilepath, "r").readlines(): + lineno+=1 + if self.lineno > 0: + saved_lineno = self.lineno + self.parse_line( component, cfg, cfname, lineno, l.strip() ) + + self.files.pop() + self.lineno = saved_lineno
+ + + + def parse_line(self, component, cfg, cfname, lineno, l ): + self.lineno = lineno + line = l.split() + + #print('FIXME parsing %s:%d %s' % (cfg, lineno, line )) + + if (len(line) < 1) or (line[0].startswith('#')): + return + + k = line[0] + if k in Config.synonyms: + k = Config.synonyms[k] + elif k == 'destination': + if component == 'poll': + k = 'pollUrl' + else: + k = 'sendTo' + elif k == 'broker' and component == 'poll' : + k = 'post_broker' + + if (k in convert_to_v3): + self.log_flowcb_needed |= '_log' in k + + if (len(line) > 1): + v = line[1].replace('.py', '', 1) + if (v in convert_to_v3[k]): + line = convert_to_v3[k][v] + k = line[0] + if 'continue' in line: + logger.debug( f'{cfname}:{lineno} obsolete v2: \"{l}\" ignored' ) + else: + logger.debug( f'{cfname}:{lineno} obsolete v2:\"{l}\" converted to sr3:\"{" ".join(line)}\"' ) + else: + line = convert_to_v3[k] + k=line[0] + v=line[1] + + if k == 'continue': + return + + #FIXME: note for Clea, line conversion to v3 complete here. + + line = list(map(lambda x: self._varsub(x), line)) + + if len(line) == 1: + v = True + else: + v = line[1] + + # FIXME... I think synonym check should happen here, but no time to check right now. + + if k in flag_options: + if len(line) == 1: + setattr(self, k, True) + else: + setattr(self, k, isTrue(v)) + if k in ['logReject'] and self.logReject: + self.logEvents = self.logEvents | set(['reject']) + return + + if len(line) < 2: + logger.error( f"{','.join(self.files)}:{lineno} {k} missing argument(s)" ) + return + if k in ['accept', 'reject' ]: + self.masks.append(self._build_mask(k, line[1:])) + elif k in [ 'callback', 'cb' ]: + #vv = v.split('.') + #v = 'sarracenia.flowcb.' + v + '.' + vv[-1].capitalize() + if v not in self.plugins_late: + self.plugins_late.append(v) + elif k in [ 'callback_prepend', 'cbp' ]: + #vv = v.split('.') + #v = 'sarracenia.flowcb.' + v + '.' + vv[-1].capitalize() + if v not in self.plugins_early: + self.plugins_early.insert(0,v) + elif k in ['declare']: + self._parse_declare(line[1:]) + elif k in ['feeder', 'manager']: + self.feeder = urllib.parse.urlparse(line[1]) + self.declared_users[self.feeder.username] = 'feeder' + elif k in ['header', 'h']: + (kk, vv) = line[1].split('=') + self.fixed_headers[kk] = vv + elif k in ['include', 'config']: + try: + self.parse_file(v) + except Exception as ex: + logger.error( f"{','.join(self.files)}:{self.lineno} file {v} failed to parse: {ex}" ) + logger.debug('Exception details: ', exc_info=True) + elif k in ['subtopic']: + self._parse_binding(v) + elif k in ['topicPrefix']: + if '/' in v : + self.topicPrefix = v.split('/') + else: + self.topicPrefix = v.split('.') + elif k in ['post_topicPrefix']: + #if (not self.post_broker.url) or self.post_broker.url.scheme[0:3] == 'amq': + if '/' in v : + self.post_topicPrefix = v.split('/') + else: + self.post_topicPrefix = v.split('.') + elif k in ['import']: + self.imports.append(v) + elif k in ['flow_callback', 'flowcb', 'fcb', 'flowCallback' ]: + if v not in self.plugins_late: + self.plugins_late.append(v) + elif k in ['flow_callback_prepend', 'flowcb_prepend', 'fcbp', 'flowCallbackPrepend' ]: + if v not in self.plugins_early: + self.plugins_early.insert(0, v) + elif k in ['set', 'setting', 's']: + self._parse_setting(line[1], line[2:]) + elif k in ['identity', 'integrity']: + self._parse_sum(v) + elif k in Config.port_required: + logger.error( f' {cfname}:{lineno} {k} {v} not supported in v3, consult porting guide. Option ignored.' ) + logger.error( f' porting guide: https://github.com/MetPX/sarracenia/blob/v03_wip/docs/How2Guides/v2ToSr3.rst ' ) + return + elif k in Config.v2entry_points: + #if k in self.plugins: + # self.plugins.remove(v) + self._parse_v2plugin(k, v) + elif k in ['no-import']: + self._parse_v3unplugin(v) + elif k in ['inflight', 'lock']: + if v[:-1].isnumeric(): + vv = durationToSeconds(v) + setattr(self, k, vv) + self.fileAgeMin = vv + else: + if line[1].lower() in ['none', 'off', 'false']: + setattr(self, k, None) + else: + setattr(self, k, v) + elif k in ['strip']: + """ + 2020/08/26 - PAS + strip in config file gets translated into two separate attributes: strip and pstrip. + strip is the numeric variety (0-n) and if the supplied option in a regex pattern, + then instead pstrip is set, and strip is set to 0. + + I don't know why it is done this way... just documenting/conforming to existing state. + """ + if v.isdigit(): + self.strip = int(v) + self.pstrip = None + else: + if v[0] == '/': + self.pstrip = v[1:] + else: + self.pstrip = v + self.strip = 0 + elif k in duration_options: + if len(line) == 1: + logger.error( + '%s:%d %s is a duration option requiring a decimal number of seconds value' + % ( cfname, lineno, line[0]) ) + return + setattr(self, k, durationToSeconds(v)) + elif k in float_options: + try: + setattr(self, k, float(v)) + except (ValueError, TypeError) as e: + logger.error(f"{','.join(self.files)}:{self.lineno} Ignored '{i}': {e}") + elif k in perm_options: + if v.isdigit(): + setattr(self, k, octal_number(int(v, base=8))) + else: + logger.error( f'{",".join(self.files)}:{lineno} {k} setting to {v} ignored: only numberic modes supported' ) + elif k in size_options: + setattr(self, k, parse_count(v)) + elif k in count_options: + setattr(self, k, parse_count(v)) + elif k in list_options: + if not hasattr(self, k) or not getattr(self,k): + setattr(self, k, [' '.join(line[1:])]) + else: + l = getattr(self, k) + l.append(' '.join(line[1:])) + elif k in set_options: + if v.lower() == 'none': + setattr(self, k, set([])) + return + if v.lower() in [ 'all' , '+all' ]: + if k in set_choices: + setattr(self,k,set_choices[k]) + return + v=v.replace('|',',') + vs = self._parse_set_string(v,getattr(self,k)) + setattr(self, k, vs ) + + if k in set_choices : + for i in getattr(self,k): + if i not in set_choices[k]: + logger.error( f'{",".join(self.files)}:{lineno} invalid entry {i} in {k}. Must be one of: {set_choices[k]}' ) + + elif k in str_options: + if ( k == 'directory' ) and not self.download: + logger.info( f"{','.join(self.files)}:{lineno} if download is false, directory has no effect" ) + + v = ' '.join(line[1:]) + if v == 'None': + v=None + setattr(self, k, v) + else: + #FIXME: with _options lists for all types and addition of declare, this is probably now dead code. + if k not in self.undeclared: + logger.debug( f'{",".join(self.files)}:{self.lineno} possibly undeclared option: {line}' ) + v = ' '.join(line[1:]) + if hasattr(self, k): + if type(getattr(self, k)) is float: + setattr(self, k, float(v)) + elif type(getattr(self, k)) is int: + # the only integers that have units are durations. + # integers without units will come out unchanged. + setattr(self, k, durationToSeconds(v)) + elif type(getattr(self, k)) is str: + setattr(self, k, [getattr(self, k), v]) + elif type(getattr(self, k)) is list: + newv=getattr(self,k) + newv.append(v) + setattr(self, k, newv) + else: + # FIXME: + setattr(self, k, v) + self.undeclared.append( (cfname, lineno, k) ) + + def _resolveQueueName(self,component,cfg): + + queuefile = sarracenia.user_cache_dir( + Config.appdir_stuff['appname'], + Config.appdir_stuff['appauthor']) + + if self.statehost: + queuefile += os.sep + self.hostdir + + queuefile += os.sep + component + os.sep + cfg + queuefile += os.sep + component + '.' + cfg + '.' + self.broker.url.username + + if hasattr(self, 'exchangeSplit') and hasattr( + self, 'no') and (self.no > 0): + queuefile += "%02d" % self.no + queuefile += '.qname' + + self.queue_filename = queuefile + + #while (not hasattr(self, 'queueName')) or (self.queueName is None): + """ + + normal: + if not the lead instance, wait a bit for the queuefile to be written. + look for a queuefile in the state directory, if it is there, read it. + if you can't read the file + + if you are instance 1, or 0 (foreground) and the queuefile is missing, then need + to write it. if queueName is set, use that, if not + + if you set the queuename, it might have variable values that when evaluated repeatedly (such as randomized settings) + will come out differently every time. So even in the case of a fixed queue name, need to write + + """ + + if hasattr(self,'no') and self.no > 1: + # worker instances need give lead instance time to write the queuefile + time.sleep(randint(4,14)) + + queue_file_read=False + config_read_try=0 + while not queue_file_read: + if os.path.isfile(queuefile): + f = open(queuefile, 'r') + self.queueName = f.read() + f.close() + else: + self.queueName = '' + + config_read_try += 1 + logger.debug( f'instance read try {config_read_try} queueName {self.queueName} from queue state file {queuefile}' ) + if len(self.queueName) < 1: + nap=randint(1,4) + logger.debug( f'queue name corrupt take a short {nap} second nap, then try again' ) + time.sleep(nap) + if config_read_try > 5: + logger.critical( f'failed to read queue name from {queuefile}') + sys.exit(2) + else: + queue_file_read=True + + else: + # only lead instance (0-foreground, 1-start, or none in the case of 'declare') + # should write the state file. + + + # lead instance shou + if os.path.isfile(queuefile): + f = open(queuefile, 'r') + self.queueName = f.read() + f.close() + + #if the queuefile is corrupt, then will need to guess anyways. + if ( self.queueName is None ) or ( self.queueName == '' ): + queueName = 'q_' + self.broker.url.username + '_' + component + '.' + cfg + if hasattr(self, 'queue_suffix'): + queueName += '.' + self.queue_suffix + queueName += '.' + str(randint(0, 100000000)).zfill(8) + queueName += '.' + str(randint(0, 100000000)).zfill(8) + self.queueName = queueName + logger.debug( f'default guessed queueName {self.queueName} ' ) + + if self.action not in [ 'start', 'foreground', 'declare' ]: + return + + # first make sure directory exists. + if not os.path.isdir(os.path.dirname(queuefile)): + pathlib.Path(os.path.dirname(queuefile)).mkdir(parents=True, exist_ok=True) + + if not os.path.isfile(queuefile) and (self.queueName is not None): + tmpQfile=queuefile+'.tmp' + if not os.path.isfile(tmpQfile): + f = open(tmpQfile, 'w') + f.write(self.queueName) + f.close() + os.rename( tmpQfile, queuefile ) + else: + logger.info( f'Queue name {self.queueName} being persisted to {queuefile} by some other process, so ignoring it.' ) + return + + logger.debug( f'queue name {self.queueName} persisted to {queuefile}' ) + + + + + +
+[docs] + def finalize(self, component=None, config=None): + """ + Before final use, take the existing settings, and infer any missing needed defaults from what is provided. + Should be called prior to using a configuration. + + There are default options that apply only if they are not overridden... + """ + + self._parse_sum(None) + + if not component and self.component: + component = self.component + + if not config and self.config: + config = self.config + + if self.action not in self.actions: + logger.error( f"invalid action: {self.action} must be one of: {','.join(self.actions)}" ) + + if hasattr(self, 'nodupe_ttl'): + if (type(self.nodupe_ttl) is str): + if isTrue(self.nodupe_ttl): + self.nodupe_ttl = 300 + else: + self.nodupe_ttl = durationToSeconds( + self.nodupe_ttl, default=300) + else: + self.nodupe_ttl = 0 + + if self.debug: + self.logLevel = 'debug' + + if self.directory: + self.directory = os.path.expanduser(self.directory) + + # double check to ensure duration options are properly parsed + for d in duration_options: + if hasattr(self, d) and (type(getattr(self, d)) is str): + setattr(self, d, durationToSeconds(getattr(self, d))) + + if hasattr(self, 'kbytes_ps'): + bytes_ps = parse_count(self.kbytes_ps) + if not self.kbytes_ps[-1].isalpha(): + bytes_ps *= 1024 + setattr(self, 'byteRateMax', bytes_ps) + + for d in count_options: + if hasattr(self, d) and (type(getattr(self, d)) is str): + setattr(self, d, parse_count(getattr(self, d))) + + for d in size_options: + if hasattr(self, d) and (type(getattr(self, d)) is str): + setattr(self, d, chunksize_from_str(getattr(self, d))) + + for f in flag_options: + if hasattr(self, f) and (type(getattr(self, f)) is str): + setattr(self, f, isTrue(getattr(self, f))) + + for f in float_options: + if hasattr(self, f) and (type(getattr(self, f)) is str): + setattr(self, f, float(getattr(self, f))) + + if ( (len(self.logEvents) > 0 ) or self.log_flowcb_needed) : + if ('sarracenia.flowcb.log.Log' not in self.plugins_late) and \ + ('log' not in self.plugins_late) : + self.plugins_late.append( 'log' ) + + # patch, as there is no 'none' level in python logging module... + # mapping so as not to break v2 configs. + if hasattr(self, 'logLevel'): + if self.logLevel == 'none': + self.logLevel = 'critical' + + if hasattr(self, 'nodupe_basis'): + if self.nodupe_basis == 'data': + self.plugins_early.append( 'nodupe.data' ) + delattr( self, 'nodupe_basis' ) + elif self.nodupe_basis == 'name': + self.plugins_early.append( 'nodupe.name' ) + delattr( self, 'nodupe_basis' ) + + if config[-5:] == '.conf': + cfg = config[:-5] + else: + cfg = config + + if self.sanity_log_dead == 9999 : + self.sanity_log_dead = 1.5*self.housekeeping + if not hasattr(self, 'post_topicPrefix'): + self.post_topicPrefix = self.topicPrefix + + if not hasattr(self, 'retry_ttl' ): + self.retry_ttl = self.expire + + if self.retry_ttl == 0: + self.retry_ttl = None + + # FIXME: note that v2 *user_cache_dir* is, v3 called: cfg_run_dir + if not hasattr(self, 'cfg_run_dir'): + if self.statehost: + hostdir = self.hostdir + else: + hostdir = None + self.cfg_run_dir = os.path.join(get_user_cache_dir(hostdir), + component, cfg) + + if self.post_broker is not None and self.post_broker.url is not None: + if not hasattr(self, + 'post_exchange') or self.post_exchange is None: + self.post_exchange = 'xs_%s' % self.post_broker.url.username + + if hasattr(self, 'post_exchangeSuffix'): + self.post_exchange += '_%s' % self.post_exchangeSuffix + + if hasattr(self,'post_exchange') and (type(self.post_exchange) is list ): + pass + elif hasattr(self, 'post_exchangeSplit') and self.post_exchangeSplit > 1: + l = [] + for i in range(0, int(self.post_exchangeSplit)): + y = self.post_exchange + '%02d' % i + l.append(y) + self.post_exchange = l + else: + self.post_exchange = [self.post_exchange] + + if (component in ['poll' ]) and (hasattr(self,'vip') and self.vip): + if (not hasattr(self,'exchange') or not self.exchange): + if type(self.post_exchange) is list: + self.exchange = self.post_exchange[0] + else: + self.exchange = self.post_exchange + if (not hasattr(self,'broker') or not self.broker): + self.broker = self.post_broker + + if not ( hasattr(self, 'source') or self.sourceFromExchange): + if hasattr(self, 'post_broker') and hasattr(self.post_broker,'url') and self.post_broker.url.username: + self.source = self.post_broker.url.username + elif hasattr(self, 'broker') and hasattr(self.broker,'url') and self.broker.url.username: + self.source = self.broker.url.username + + if self.broker and self.broker.url and self.broker.url.username: + self._resolve_exchange() + self._resolveQueueName(component,cfg) + + valid_inlineEncodings = [ 'guess', 'text', 'binary' ] + if hasattr(self, 'inlineEncoding') and self.inlineEncoding not in valid_inlineEncodings: + logger.error( f"invalid inlineEncoding: {self.inlineEncoding} must be one of: {','.join(valid_inlineEncodings)}" ) + + if hasattr(self, 'no'): + if self.statehost: + hostdir = self.hostdir + else: + hostdir = None + self.metricsFilename = get_metrics_filename(hostdir, component, cfg, self.no) + self.pid_filename = get_pid_filename(hostdir, component, cfg, self.no) + self.retry_path = self.pid_filename.replace('.pid', '.retry') + self.novipFilename = self.pid_filename.replace('.pid', '.noVip') + + + if (self.bindings == [] and hasattr(self, 'exchange')): + self.bindings = [(self.exchange, self.topicPrefix, [ '#' ])] + + if hasattr(self, 'documentRoot') and (self.documentRoot is not None): + path = os.path.expanduser(os.path.abspath(self.documentRoot)) + if self.realpathPost: + path = os.path.realpath(path) + + if sys.platform == 'win32' and words0.find('\\'): + logger.warning("%s %s" % (words0, words1)) + logger.warning( + "use of backslash ( \\ ) is an escape character. For a path separator use forward slash ( / )." + ) + + if sys.platform == 'win32': + self.documentRoot = path.replace('\\', '/') + else: + self.documentRoot = path + n = 2 + + if hasattr(self, 'pollUrl'): + if not hasattr(self,'post_baseUrl') or not self.post_baseUrl : + logger.debug( f"defaulting post_baseUrl to match pollURl, since it isn't specified." ) + self.post_baseUrl = self.pollUrl + + # verify post_baseDir + + if self.post_baseDir is None: + if self.post_documentRoot is not None: + self.post_baseDir = os.path.expanduser(self.post_documentRoot) + logger.warning("use post_baseDir instead of post_documentRoot") + elif self.documentRoot is not None: + self.post_baseDir = os.path.expanduser(self.documentRoot) + logger.warning("use post_baseDir instead of documentRoot") + elif self.post_baseUrl and ( self.post_baseUrl[0:5] in [ 'file:' ] ): + self.post_baseDir = self.post_baseUrl[5:] + elif self.post_baseUrl and ( self.post_baseUrl[0:5] in [ 'sftp:' ] ): + u = sarracenia.baseUrlParse(self.post_baseUrl) + self.post_baseDir = u.path + elif self.baseDir is not None: + self.post_baseDir = os.path.expanduser(self.baseDir) + logger.debug("defaulting post_baseDir to same as baseDir") + + + if self.messageCountMax > 0: + if self.batch > self.messageCountMax: + self.batch = self.messageCountMax + logger.info( f'overriding batch for consistency with messageCountMax: {self.batch}' ) + + if (component not in ['poll' ]): + self.path = list(map( os.path.expanduser, self.path )) + else: + if not (hasattr(self,'scheduled_interval') or hasattr(self,'scheduled_hour') or hasattr(self,'scheduled_minute')): + if self.sleep > 1: + self.scheduled_interval = self.sleep + self.sleep=1 + + + if self.vip and not features['vip']['present']: + logger.critical( f"vip feature requested, but missing library: {' '.join(features['vip']['modules_needed'])} " ) + sys.exit(1)
+ + + def check_undeclared_options(self): + + alloptions = str_options + flag_options + float_options + list_options + set_options + count_options + size_options + duration_options + # FIXME: confused about this... commenting out for now... + for f,l,u in self.undeclared: + if u not in alloptions: + logger.error( f"{f}:{l} undeclared option: {u}") + elif u in flag_options: + if type( getattr(self,u) ) is not bool: + setattr(self,u,isTrue(getattr(self,u))) + elif u in float_options: + if type( getattr(self,u) ) is not float: + setattr(self,u,float(getattr(self,u))) + elif u in set_options: + if type( getattr(self,u) ) is not set: + setattr(self,u,self._parse_set_string(getattr(self,u),set())) + elif u in str_options: + if type( getattr(self,u) ) is not str: + setattr(self,u,str(getattr(self,u))) + elif u in count_options: + if type( getattr(self,u) ) not in [ int, float ]: + setattr(self,u,parse_count(getattr(self,u))) + elif u in size_options: + if type( getattr(self,u) ) not in [ int, float ]: + setattr(self,u,parse_count(getattr(self,u))) + elif u in duration_options: + if type( getattr(self,u) ) not in [ int, float ]: + setattr(self,u,durationToSeconds(getattr(self,u))) + # list options are the default, so no need to regularize + + no_defaults=set() + for u in alloptions: + if not hasattr(self,u): + no_defaults.add( u ) + + logger.debug("missing defaults: %s" % no_defaults) + + """ + 2020/05/26 FIXME here begins sheer terror. + following routines are taken verbatim from v2. + trying not to touch it... it is painful. + setting new_ values for downloading etc... + sundew_* ... + """ + +
+[docs] + def _sundew_basename_parts(self, pattern, basename): + """ + modified from metpx SenderFTP + """ + + if pattern == None: return [] + parts = re.findall(pattern, basename) + if len(parts) == 2 and parts[1] == '': parts.pop(1) + if len(parts) != 1: return None + + lst = [] + if isinstance(parts[0], tuple): + lst = list(parts[0]) + else: + lst.append(parts[0]) + + return lst
+ + + # from metpx SenderFTP +
+[docs] + def sundew_dirPattern(self, pattern, urlstr, basename, destDir): + """ + does substitutions for patterns in directories. + + """ + BN = basename.split(":") + EN = BN[0].split("_") + + BP = self._sundew_basename_parts(pattern, urlstr) + + ndestDir = "" + DD = destDir.split("/") + for ddword in DD: + if ddword == "": continue + + nddword = "" + DW = ddword.split("$") + for dwword in DW: + nddword += self.sundew_matchPattern(BN, EN, BP, dwword, dwword) + + ndestDir += "/" + nddword + + # This code might add an unwanted '/' in front of ndestDir + # if destDir does not start with a substitution $ and + # if destDir does not start with a / ... it does not need one + + if (len(destDir) > 0) and (destDir[0] != '$') and (destDir[0] != '/'): + if ndestDir[0] == '/': ndestDir = ndestDir[1:] + + return ndestDir
+ + + # modified from metpx SenderFTP + def sundew_matchPattern(self, BN, EN, BP, keywd, defval): + + BN6 = time.strftime("%Y%m%d%H%M%S", time.gmtime()) + if len(BN) >= 7: BN6 = BN[6] + + if keywd[:4] == "{T1}": return (EN[0])[0:1] + keywd[4:] + elif keywd[:4] == "{T2}": return (EN[0])[1:2] + keywd[4:] + elif keywd[:4] == "{A1}": return (EN[0])[2:3] + keywd[4:] + elif keywd[:4] == "{A2}": return (EN[0])[3:4] + keywd[4:] + elif keywd[:4] == "{ii}": return (EN[0])[4:6] + keywd[4:] + elif keywd[:6] == "{CCCC}": return EN[1] + keywd[6:] + elif keywd[:4] == "{YY}": return (EN[2])[0:2] + keywd[4:] + elif keywd[:4] == "{GG}": return (EN[2])[2:4] + keywd[4:] + elif keywd[:4] == "{Gg}": return (EN[2])[4:6] + keywd[4:] + elif keywd[:5] == "{BBB}": + return (EN[3])[0:3] + keywd[5:] + # from pds'datetime suffix... not sure + elif keywd[:7] == "{RYYYY}": + return BN6[0:4] + keywd[7:] + elif keywd[:5] == "{RMM}": + return BN6[4:6] + keywd[5:] + elif keywd[:5] == "{RDD}": + return BN6[6:8] + keywd[5:] + elif keywd[:5] == "{RHH}": + return BN6[8:10] + keywd[5:] + elif keywd[:5] == "{RMN}": + return BN6[10:12] + keywd[5:] + elif keywd[:5] == "{RSS}": + return BN6[12:14] + keywd[5:] + + # Matching with basename parts if given + + if BP != None: + for i, v in enumerate(BP): + kw = '{' + str(i) + '}' + lkw = len(kw) + if keywd[:lkw] == kw: return v + keywd[lkw:] + + return defval + +
+[docs] + def variableExpansion(self, cdir, message=None ) -> str: + """ + replace substitution patterns, variable substitutions as described in + https://metpx.github.io/sarracenia/Reference/sr3_options.7.html#variables + + returns: the given string with the substiturions done. + + examples: ${YYYYMMDD-70m} becomes 20221107 assuming that was the current date 70 minutes ago. + environment variables, and built-in settings are replaced also. + + timeoffset -70m + + + """ + + if not '$' in cdir: + return cdir + + new_dir = cdir + + while '${BD}' in new_dir and self.baseDir != None: + new_dir = new_dir.replace('${BD}', self.baseDir, 1) + + while ( '${BUP}' in new_dir ) and ( 'baseUrl' in message ): + u = sarracenia.baseUrlParse( message['baseUrl'] ) + new_dir = new_dir.replace('${BUP}', u.path, 1 ) + + while ( '${baseUrlPath}' in new_dir ) and ( 'baseUrl' in message ): + u = sarracenia.baseUrlParse( message['baseUrl'] ) + new_dir = new_dir.replace('${baseUrlPath}', u.path, 1) + + while ( '${BUPL}' in new_dir ) and ( 'baseUrl' in message ): + u = sarracenia.baseUrlParse( message['baseUrl'] ) + new_dir = new_dir.replace('${BUPL}', os.path.basename(u.path), 1 ) + + while ( '${baseUrlPathLast}' in new_dir ) and ( 'baseUrl' in message ): + u = sarracenia.baseUrlParse( message['baseUrl'] ) + new_dir = new_dir.replace('${baseUrlPathLast}', os.path.basename(u.path), 1 ) + + while '${PBD}' in new_dir and self.post_baseDir != None: + new_dir = new_dir.replace('${PBD}', self.post_baseDir, 1) + + while '${DR}' in new_dir and self.documentRoot != None: + logger.warning( + "DR = documentRoot should be replaced by BD for base_dir") + new_dir = new_dir.replace('${DR}', self.documentRoot, 1) + + while '${PDR}' in new_dir and self.post_baseDir != None: + logger.warning( + "PDR = post_documentRoot should be replaced by PBD for post_baseDir" + ) + new_dir = new_dir.replace('${PDR}', self.post_baseDir, 1) + + #whenStamp = time.gmtime( time.time()+self.varTimeOffset ) + + whenStamp = datetime.datetime.fromtimestamp( time.time()+self.varTimeOffset ) + + while '${YYYYMMDD}' in new_dir: + YYYYMMDD = whenStamp.strftime("%Y%m%d") + new_dir = new_dir.replace('${YYYYMMDD}', YYYYMMDD) + + while '${SOURCE}' in new_dir: + new_dir = new_dir.replace('${SOURCE}', message['source']) + + while '${DD}' in new_dir: + DD = whenStamp.strftime("%d") + new_dir = new_dir.replace('${DD}', DD) + + while '${HH}' in new_dir: + HH = whenStamp.strftime("%H") + new_dir = new_dir.replace('${HH}', HH) + + while '${YYYY}' in new_dir: + YYYY = whenStamp.strftime("%Y") + new_dir = new_dir.replace('${YYYY}', YYYY) + + while '${MM}' in new_dir: + MM = whenStamp.strftime("%m") + new_dir = new_dir.replace('${MM}', MM) + + while '${JJJ}' in new_dir: + JJJ = whenStamp.strftime("%j") + new_dir = new_dir.replace('${JJJ}', JJJ) + + + # strftime compatible patterns. + fragments = new_dir.split( '${%' ) + if len(fragments) > 1: + fragment_list=[fragments[0]] + for fragment in fragments[1:]: + close_brace = fragment.find('}') + frag_start=0 + seconds=self.varTimeOffset + + # only support %o time offsets at the beginning of the string. + if fragment[0] in [ '+', '-', 'o' ]: + end_of_offset=fragment.find('%') + if fragment[0] == 'o': + s= 2 if fragment[1] in [ '-','+' ] else 1 + else: + s= 1 if fragment[0] in [ '-','+' ] else 0 + seconds = durationToSeconds(fragment[s:end_of_offset]) + frag_start=end_of_offset+1 + if '-' in fragment[0:2]: + seconds = -1 * seconds + + whenStamp = datetime.datetime.fromtimestamp( time.time()+seconds ) + + if close_brace > 0: + time_str=whenStamp.strftime( "%"+fragment[frag_start:close_brace] ) + fragment_list.append(time_str) + fragment_list.append(fragment[close_brace+1:]) + else: + fragment_list.append(fragment) + new_dir=''.join(fragment_list) + + # Parsing cdir to subtract time from it in the following formats + # time unit can be: sec/mins/hours/days/weeks + + # ${YYYY-[number][time_unit]} + offset_check = re.search( r'\$\{YYYY-(\d+)(\D)\}', cdir) + if offset_check: + logger.info( f"offset 0: {offset_check.group(1,2)}" ) + seconds = durationToSeconds(''.join(offset_check.group(1, 2)), + 's') + + epoch = time.mktime(time.gmtime()) - seconds + YYYY1D = time.strftime("%Y", time.localtime(epoch)) + new_dir = re.sub( r'\$\{YYYY-\d+\D\}', YYYY1D, new_dir) + + # ${MM-[number][time_unit]} + offset_check = re.search( r'\$\{MM-(\d+)(\D)\}', cdir) + if offset_check: + logger.info( f"offset 1: {offset_check.group(1,2)}" ) + seconds = durationToSeconds(''.join(offset_check.group(1, 2)), + 's') + + epoch = time.mktime(time.gmtime()) - seconds + MM1D = time.strftime("%m", time.localtime(epoch)) + new_dir = re.sub( r'\$\{MM-\d+\D\}', MM1D, new_dir) + + # ${JJJ-[number][time_unit]} + offset_check = re.search(r'\$\{JJJ-(\d+)(\D)\}', cdir) + if offset_check: + logger.info( f"offset 2: {offset_check.group(1,2)}" ) + seconds = durationToSeconds(''.join(offset_check.group(1, 2)), + 's') + + epoch = time.mktime(time.gmtime()) - seconds + JJJ1D = time.strftime("%j", time.localtime(epoch)) + new_dir = re.sub( r'\$\{JJJ-\d+\D\}', JJJ1D, new_dir) + + # ${YYYYMMDD-[number][time_unit]} + offset_check = re.search(r'\$\{YYYYMMDD-(\d+)(\D)\}', cdir) + if offset_check: + logger.info( f"offset 3: {offset_check.group(1,2)}" ) + seconds = durationToSeconds(''.join(offset_check.group(1, 2)), + 's') + epoch = time.mktime(time.gmtime()) - seconds + YYYYMMDD = time.strftime("%Y%m%d", time.localtime(epoch)) + logger.info( f"seconds: {seconds} YYYYMMDD {YYYYMMDD}" ) + new_dir = re.sub( r'\$\{YYYYMMDD-\d+\D\}', YYYYMMDD, new_dir) + + new_dir = self._varsub(new_dir) + + # substitute positional fields from the regex accept (0,1,2,3...) + if message and '_matches' in message and len(new_dir.split( '${' )) > 1: + fragment_list=[] + for fragment in new_dir.split( '${' ): + close_brace = fragment.find('}') + frag_start=0 + if close_brace < 0 : + fragment_list.append(fragment) + continue + + match_field=fragment[0:close_brace] + matches= re.search( r'^[0-9]+$', match_field) + # non-numeric thing... variable or something. + if not matches: + fragment_list.append('${' + fragment) + continue + field=int(match_field) + if self.sundew_compat_regex_first_match_is_zero: + field +=1 + if len(message['_matches'].groups()) >= field: + fragment_list.append(message['_matches'].group(field)) + fragment_list.append(fragment[close_brace+1:]) + else: + logger.error( f"only {len(message['_matches'].groups())} groups in regex, group number too high: ${{{fragment}" ) + fragment_list.append('${' +fragment) + + new_dir=''.join(fragment_list) + + #del message['_matches'] + #message['_deleteOnPost'] -= set(['_matches']) + return new_dir
+ + + + + """ + 2020/05/26 PAS... FIXME: end of sheer terror. + + the parts below used be part of the sheer terror... but have been + tamed a bit. + """ + +
+[docs] + class addBinding(argparse.Action): + """ + called by argparse to deal with queue bindings. + """ +
+[docs] + def __call__(self, parser, namespace, values, option_string): + + if values == 'None': + namespace.bindings = [] + + namespace._resolve_exchange() + + if not hasattr(namespace, 'broker'): + raise Exception('broker needed before subtopic') + return + + if not hasattr(namespace, 'exchange'): + raise Exception('exchange needed before subtopic') + return + + if not hasattr(namespace, 'topicPrefix'): + raise Exception('topicPrefix needed before subtopic') + return + + if type(namespace.topicPrefix) is str: + if namespace.broker.scheme[0:3] == 'amq': + topicPrefix = namespace.topicPrefix.split('.') + else: + topicPrefix = namespace.topicPrefix.split('/') + + namespace.bindings.append( + (namespace.exchange, topicPrefix, values))
+
+ + +
+[docs] + def parse_args(self, isPost=False): + """ + user information: + accept a configuration, apply argParse library to augment the given configuration + with command line settings. + + the post component has a different calling convention than others, so use that flag + if called from post. + + development notes: + Use argparse.parser to modify defaults. + FIXME, many FIXME notes below. this is a currently unusable placeholder. + have not figured this out yet. many issues. + + FIXME #1: + parseArgs often sets the value of the variable, regardless of it's presence (normally a good thing.) + ( if you have 'store_true' then default needed, for broker, just a string, it ignores if not present.) + This has the effect of overriding settings in the file parsed before the arguments. + Therefore: often supply defaults... but... sigh... + + but there is another consideration stopping me from supplying defaults, wish I remembered what it was. + I think it is: + FIXME #2: + arguments are parsed twice: once to get basic stuff (loglevel, component, action) + and if the parsing fails there, the usage will print the wrong defaults... + + """ + + parser=argparse.ArgumentParser( \ + description='version: %s\nSarracenia flexible tree copy ( https://MetPX.github.io/sarracenia ) ' % sarracenia.__version__ ,\ + formatter_class=argparse.ArgumentDefaultsHelpFormatter ) + + if sys.version_info[0] >= 3 and sys.version_info[1] < 8: + parser.register('action', 'extend', ExtendAction) + + parser.add_argument('--acceptUnmatched', + default=self.acceptUnmatched, + type=bool, + nargs='?', + help='default selection, if nothing matches') + parser.add_argument( + '--action', + '-a', + nargs='?', + choices=Config.actions, + help='action to take on the specified configurations') + parser.add_argument('--admin', + help='amqp://user@host of peer to manage') + parser.add_argument( + '--attempts', + type=int, + nargs='?', + help='how many times to try before queuing for retry') + parser.add_argument( + '--base_dir', + '-bd', + nargs='?', + help="path to root of tree for relPaths in messages.") + parser.add_argument('--batch', + type=int, + nargs='?', + help='how many transfers per each connection') + parser.add_argument( + '--blocksize', + type=int, + nargs='?', + help= + 'size to partition files. 0-guess, 1-never, any other number: that size' + ) + """ + FIXME: Most of this is gobblygook place holder stuff, by copying from wmo-mesh example. + Don't really need this to work right now, so just leaving it around as-is. Challenges: + + -- sizing units, K, M, G, (should have humanfriendly based parsing.) + -- time units s,h,m,d + -- what to do with verbs. + -- accept/reject whole mess requires extension deriving a class from argparse.Action. + + """ + parser.add_argument('--broker', + nargs='?', + help='amqp://user:pw@host of peer to subscribe to') + parser.add_argument('--config', + '-c', + nargs='?', + help=' specifical configuration to select ') + parser.add_argument('--dangerWillRobinson', + type=int, + default=0, + help='Confirm you want to do something dangerous') + parser.add_argument('--debug', + action='store_true', + default=self.debug, + help='print debugging output (very verbose)') + parser.add_argument('--wololo', + action='store_true', + default=self.wololo, + help='force overwrite of converted configs') + parser.add_argument('--dry_run', '--simulate', '--simulation', + action='store_true', + default=self.dry_run, + help='simulation mode (perform no file transfers, just print what would happen)') + parser.add_argument('--exchange', + nargs='?', + default=self.exchange, + help='root of the topic tree to subscribe to') + + parser.add_argument('--full', + action='store_true', + default=self.displayFull, + help='fuller, more verbose display') + """ + FIXME: header option not implemented in argparsing: should add to the fixed_header dictionary. + + """ + """ + FIXME: in previous parser, exchange is a modifier for bindings, can have several different values for different subtopic bindings. + as currently coded, just a single value that over-writes previous setting, so only binding to a single exchange is possible. + """ + + parser.add_argument('--inline', + dest='inline', + default=self.inline, + action='store_true', + help='include file data in the message') + parser.add_argument( + '--inlineEncoding', + choices=['text', 'binary', 'guess'], + default=self.inlineEncoding, + help='encode payload in base64 (for binary) or text (utf-8)') + parser.add_argument('--inlineByteMax', + type=int, + default=self.inlineByteMax, + help='maximum message size to inline') + parser.add_argument( + '--instances', + type=int, + help='number of processes to run per configuration') + + parser.add_argument('--identity_method', '--identity', '-s', '--sum', + nargs='?', + default=self.identity_method, + help='choose a different checksumming method for the files posted') + if hasattr(self, 'bindings'): + parser.set_defaults(bindings=self.bindings) + + + parser.add_argument( + '--logLevel', + choices=[ + 'notset', 'debug', 'info', 'warning', 'error', 'critical' + ], + help='encode payload in base64 (for binary) or text (utf-8)') + parser.add_argument('--logReject', + action='store_true', + default=self.logReject, + help='print a log message explaining why each file is rejected') + parser.add_argument('--logStdout', + action='store_true', + default=False, + help='disable logging, everything to standard output/error') + parser.add_argument('--no', + type=int, + help='instance number of this process') + parser.add_argument('--queueName', + nargs='?', + help='name of AMQP consumer queue to create') + parser.add_argument('--post_broker', + nargs='?', + help='broker to post downloaded files to') + #parser.add_argument('--post_baseUrl', help='base url of the files announced') + parser.add_argument('--post_exchange', + nargs='?', + help='root of the topic tree to announce') + parser.add_argument( + '--post_exchangeSplit', + type=int, + nargs='?', + help='split output into different exchanges 00,01,...') + parser.add_argument( + '--post_topicPrefix', + nargs='?', + help= + 'allows simultaneous use of multiple versions and types of messages' + ) + parser.add_argument('--retry_refilter', + action='store_true', + default=self.retry_refilter, + help='repeat message processing when retrying transfers (default just resends as previous attempt.)') + #FIXME: select/accept/reject in parser not implemented. + parser.add_argument( + '--select', + nargs=1, + action='append', + help='client-side filtering: accept/reject <regexp>') + parser.add_argument( + '--subtopic', + nargs=1, + action=Config.addBinding, + help= + 'server-side filtering: MQTT subtopic, wilcards # to match rest, + to match one topic' + ) + parser.add_argument( + '--topicPrefix', + nargs='?', + default=self.topicPrefix, + help= + 'allows simultaneous use of multiple versions and types of messages' + ) + parser.add_argument('--users', + default=False, + action='store_true', + help='only for declare... declare users?') + parser.add_argument( + '--version', + '-v', + action='version', + version='%s' % sarracenia.__version__, + help= + 'server-side filtering: MQTT subtopic, wilcards # to match rest, + to match one topic' + ) + + if isPost: + parser.add_argument('--path', + '-p', + action='append', + nargs='?', + help='path to post or watch') + parser.add_argument('path', + nargs='*', + action='extend', + help='files to post') + else: + parser.add_argument( + 'action', + nargs='?', + choices=Config.actions, + help='action to take on the specified configurations') + parser.add_argument('configurations', + nargs='*', + help='configurations to operate on') + + args = parser.parse_args() + + if hasattr(args, 'help'): + args.print_usage() + + if hasattr(args, 'config') and (args.config is not None): + args.configurations = [args.config] + + if hasattr(args,'full'): + self.displayFull = args.full + delattr(args,'full') + + self.merge(args)
+
+ + + +def default_config(): + + cfg = Config() + cfg.currentDir = None + cfg.override(default_options) + cfg.override(sarracenia.moth.default_options) + if features['amqp']['present']: + cfg.override(sarracenia.moth.amqp.default_options) + cfg.override(sarracenia.flow.default_options) + + for g in ["admin.conf", "default.conf"]: + if os.path.exists(get_user_config_dir() + os.sep + g): + cfg.parse_file(get_user_config_dir() + os.sep + g) + + return cfg + +
+[docs] +def no_file_config(): + """ + initialize a config that will not use Sarracenia configuration files at all. + meant for use by people writing independent programs to start up instances + with python API calls. + + """ + cfg = Config() + cfg.currentDir = None + cfg.override(default_options) + cfg.override(sarracenia.moth.default_options) + if features['amqp']['present']: + cfg.override(sarracenia.moth.amqp.default_options) + cfg.override(sarracenia.flow.default_options) + cfg.cfg_run_dir = '.' + cfg.retry_path = '.' + return cfg
+ + + +
+[docs] +def one_config(component, config, action, isPost=False): + """ + single call return a fully parsed single configuration for a single component to run. + + read in admin.conf and default.conf + + apply component default overrides ( maps to: component/check ?) + read in component/config.conf + parse arguments from command line. + return config instance item. + + + appdir_stuff can be to override file locations for testing during development. + + """ + default_cfg = default_config() + #default_cfg.override( { 'component':component, 'directory': os.getcwd(), 'acceptUnmatched':True, 'no':0 } ) + default_cfg.override({ + 'component': component, + 'config': config, + 'acceptUnmatched': True, + 'no': 0 + }) + + cfg = copy.deepcopy(default_cfg) + + cfg.applyComponentDefaults( component ) + + store_pwd = os.getcwd() + + os.chdir(get_user_config_dir()) + os.chdir(component) + + if config[-5:] != '.conf': + fname = os.path.expanduser(config + '.conf') + else: + fname = os.path.expanduser(config) + + if os.path.exists(fname): + cfg.parse_file(fname,component) + else: + logger.error('config %s not found' % fname ) + return None + + os.chdir(store_pwd) + + cfg.parse_args(isPost) + + #logger.error( 'after args' ) + #print( 'after args' ) + #cfg.dump() + if component in ['poll' ]: + if not hasattr(cfg,'broker') or (cfg.broker is None): + cfg.broker = cfg.post_broker + + cfg.action=action + + cfg.finalize(component, config) + + if component in ['post', 'watch']: + cfg.postpath = list( map( os.path.expanduser, cfg.configurations[1:])) + if hasattr(cfg, 'path') and (cfg is not None): + if type(cfg.path) is list: + cfg.postpath.extend(cfg.path) + else: + cfg.postpath.append(cfg.path) + logger.debug('path is : %s' % cfg.path) + logger.debug('postpath is : %s' % cfg.postpath) + + #pp = pprint.PrettyPrinter(depth=6) + #pp.pprint(cfg) + + return cfg
+ + +def cfglogs(cfg_preparse, component, config, logLevel, child_inst): + + + if cfg_preparse.logRotateInterval < 24*24*60: + logRotateInterval=int(cfg_preparse.logRotateInterval) + lr_when='s' + else: + logRotateInterval = int(cfg_preparse.logRotateInterval/(24*24*60)) + lr_when='midnight' + + # init logs here. need to know instance number and configuration and component before here. + + if cfg_preparse.action == 'start' and not cfg_preparse.logStdout: + if cfg_preparse.statehost: + hostdir = cfg_preparse.hostdir + else: + hostdir = None + + metricsfilename = get_metrics_filename( hostdir, component, config, child_inst) + + dir_not_there = not os.path.exists(os.path.dirname(metricsfilename)) + while dir_not_there: + try: + os.makedirs(os.path.dirname(metricsfilename), exist_ok=True) + dir_not_there = False + except FileExistsError: + dir_not_there = False + except Exception as ex: + logging.error( "makedirs {} failed err={}".format(os.path.dirname(metricsfilename),ex)) + logging.debug("Exception details:", exc_info=True) + time.sleep(0.1) + + cfg_preparse.metricsFilename = metricsfilename + + logfilename = get_log_filename( hostdir, component, config, child_inst) + + dir_not_there = not os.path.exists(os.path.dirname(logfilename)) + while dir_not_there: + try: + os.makedirs(os.path.dirname(logfilename), exist_ok=True) + dir_not_there = False + except FileExistsError: + dir_not_there = False + except Exception as ex: + logging.error( "makedirs {} failed err={}".format(os.path.dirname(logfilename),ex)) + logging.debug("Exception details:", exc_info=True) + time.sleep(0.1) + + log_format = '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s' + if logging.getLogger().hasHandlers(): + for h in logging.getLogger().handlers: + h.close() + logging.getLogger().removeHandler(h) + logger = logging.getLogger() + logger.setLevel(logLevel.upper()) + + handler = sarracenia.instance.RedirectedTimedRotatingFileHandler( + logfilename, + when=lr_when, + interval=logRotateInterval, + backupCount=cfg_preparse.logRotateCount) + handler.setFormatter(logging.Formatter(log_format)) + + logger.addHandler(handler) + + if hasattr(cfg_preparse, 'permLog'): + os.chmod(logfilename, cfg_preparse.permLog) + + # FIXME: https://docs.python.org/3/library/contextlib.html portable redirection... + if sys.platform != 'win32': + os.dup2(handler.stream.fileno(), 1) + os.dup2(handler.stream.fileno(), 2) + + else: + try: + logger.setLevel(logLevel) + except Exception: + logger.setLevel(logging.INFO) + +# add directory to python front of search path for plugins. +plugin_dir = get_user_config_dir() + os.sep + "plugins" +if os.path.isdir(plugin_dir) and not plugin_dir in sys.path: + sys.path.insert(0, plugin_dir) +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/credentials.html b/_modules/sarracenia/credentials.html new file mode 100644 index 000000000..71dc41b30 --- /dev/null +++ b/_modules/sarracenia/credentials.html @@ -0,0 +1,615 @@ + + + + + + sarracenia.credentials — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.credentials

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# Questions or bugs report: dps-client@ec.gc.ca
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+# credentials.py : python3 utility tool to configure all protocol credentials
+#
+#
+# Code contributed by:
+#  Michel Grenier - Shared Services Canada
+#  Last Changed   : Dec 29 11:42:11 EST 2015
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+
+#
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+logger.setLevel(logging.INFO)
+
+import os
+import re
+import urllib, urllib.parse
+import sys
+
+
+
+[docs] +class Credential: + r""" + + An object that holds information about a credential, read from a + credential file, which has one credential per line, format:: + + url option1=value1, option2=value2 + + Examples:: + sftp://alice@herhost/ ssh_keyfile=/home/myself/mykeys/.ssh.id_dsa + ftp://georges:Gpass@hishost/ passive = True, binary = True + + `Format Documentation. <https://metpx.github.io/sarracenia/Reference/sr3_credentials.7.html>`_ + + Attributes: + url (urllib.parse.ParseResult): object with URL, password, etc. + ssh_keyfile (str): path to SSH key file for SFTP + passive (bool): use passive FTP mode, defaults to ``True`` + binary (bool): use binary FTP mode, defaults to ``True`` + tls (bool): use FTPS with TLS, defaults to ``False`` + prot_p (bool): use a secure data connection for TLS + bearer_token (str): bearer token for HTTP authentication + login_method (str): force a specific login method for AMQP (PLAIN, + AMQPLAIN, EXTERNAL or GSSAPI) + + Usage: + + # build a credential from a url string: + + from sarracenia.credentials import Credential + + broker = Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca') + + + + """ + +
+[docs] + def __init__(self, urlstr=None): + """Create a Credential object. + + Args: + urlstr (str): a URL in string form to be parsed. + """ + + if urlstr is not None: + self.url = urllib.parse.urlparse(urlstr) + else: + self.url = None + + self.ssh_keyfile = None + self.passive = True + self.binary = True + self.tls = False + self.prot_p = False + self.bearer_token = None + self.login_method = None + self.s3_endpoint = None + self.s3_session_token = None + self.azure_credentials = None
+ + +
+[docs] + def __str__(self): + """Returns attributes of the Credential object as a readable string. + """ + + s = '' + if False: + s += self.url.geturl() + else: + s += self.url.scheme + '://' + if self.url.username: + s += self.url.username + #if self.url.password: + # s += ':' + self.url.password + if self.url.hostname: + s += '@' + self.url.hostname + if self.url.port: + s += ':' + str(self.url.port) + if self.url.path: + s += self.url.path + + s += " %s" % self.ssh_keyfile + s += " %s" % self.passive + s += " %s" % self.binary + s += " %s" % self.tls + s += " %s" % self.prot_p + s += " %s" % self.bearer_token + s += " %s" % self.login_method + s += " %s" % self.s3_endpoint + #want to show they provided a session token, but not leak it (like passwords above) + s += " %s" % 'Yes' if self.s3_session_token != None else 'No' + s += " %s" % 'Yes' if self.azure_credentials != None else 'No' + + return s
+
+ + + +
+[docs] +class CredentialDB: + """Parses, stores and manages Credential objects. + + Attributes: + credentials (dict): contains all sarracenia.credentials.Credential objects managed by the CredentialDB. + + Usage: + # build a credential via lookup in the normal files: + import CredentialDB from sarracenia.credentials + + credentials = CredentialDB.read( "/the/path/to/the/credentials.conf" ) + + # if there are corresponding passwords or modulation of login information look it up. + + broker = credentials.get( "amqps://hpfx.collab.science.gc.ca" ) + remote = credentials.get( "sftp://hoho@theserver" ) + """ + +
+[docs] + def __init__(self, Unused_logger=None): + """Create a CredentialDB. + + Args: + Unused_logger: logger argument no longer used... left there for API compat with old calls. + """ + self.credentials = {} + self.pwre = re.compile(':[^/:]*@') + + logger.debug("__init__")
+ + +
+[docs] + def add(self, urlstr, details=None): + """Add a new credential to the DB. + + Args: + urlstr (str): string-formatted URL to be parsed and added to DB. + details (sarracenia.credentials.Credential): a Credential object can be passed in, otherwise one is + created by parsing urlstr. + """ + + # need to create url object + key=urlstr + if details == None: + details = Credential() + details.url = urllib.parse.urlparse(urlstr) + if hasattr(details.url,'password'): + key = key.replace( f":{details.url.password}", "" ) + + self.credentials[key] = details
+ + +
+[docs] + def get(self, urlstr): + """Retrieve a Credential from the DB by urlstr. If the Credential is valid, but not already cached, it will be + added to the CredentialDB. + + Args: + urlstr (str): credentials as URL string to be parsed. + + Returns: + tuple: containing + cache_result (bool): ``True`` if the credential was retrieved from the CredentialDB cache, ``False`` + if it was not in the cache. Note that ``False`` does not imply the Credential or urlstr is + invalid. + credential (sarracenia.credentials.Credential): the Credential + object matching the urlstr, ``None`` if urlstr is invalid. + """ + #logger.debug("CredentialDB get %s" % urlstr) + + # already cached + + if self.has(urlstr): + #logger.debug("CredentialDB get in cache %s %s" % (urlstr,self.credentials[urlstr])) + return True, self.credentials[urlstr] + + # create url object if needed + + url = urllib.parse.urlparse(urlstr) + + # add anonymous default, if necessary. + if ( 'amqp' in url.scheme ) and \ + ( (url.username == None) or (url.username == '') ): + urlstr = urllib.parse.urlunparse( ( url.scheme, \ + 'anonymous:anonymous@%s' % url.netloc, url.path, None, None, url.port ) ) + url = urllib.parse.urlparse(urlstr) + if self.isValid(url): + self.add(urlstr) + return False, self.credentials[urlstr] + + # resolved from defined credentials + ok, details = self._resolve(urlstr, url) + if ok: return True, details + + # not found... is it valid ? + if not self.isValid(url): + return False, None + + # cache it as is... we dont want to validate every time + + self.add(urlstr) + return False, self.credentials[urlstr]
+ + +
+[docs] + def has(self, urlstr): + """Return ``True`` if the Credential matching the urlstr is already in the CredentialDB. + + Args: + urlstr(str): credentials in a URL string. + """ + logger.debug("has %s" % urlstr) + return urlstr in self.credentials
+ + +
+[docs] + def isTrue(self, S): + """Returns ``True`` if s is ``true``, ``yes``, ``on`` or ``1``. + + Args: + S (str): string to check if true. + """ + s = S.lower() + if s == 'true' or s == 'yes' or s == 'on' or s == '1': return True + return False
+ + +
+[docs] + def isValid(self, url, details=None): + """Validates a URL and Credential object. Checks for empty passwords, schemes, etc. + + Args: + url (urllib.parse.ParseResult): ParseResult object for a URL. + details (sarracenia.credentials.Credential): sarra Credential object containing additional details about + the URL. + + Returns: + bool: ``True`` if a URL is valid, ``False`` if not. + """ + + # network location + if url.netloc == '': + # file (why here? anyway) + if url.scheme == 'file': return True + logger.error( f'no network location, and not a file url' ) + return False + + # amqp... vhost not check: default / + + # user and password provided we are ok + user = url.username != None and url.username != '' + pasw = url.password != None and url.password != '' + both = user and pasw + + # we have everything + if both: return True + + # we have no user and no pasw (http normal, https... no cert, sftp hope for .ssh/config) + if not user and not pasw: + if url.scheme in ['http', 'https', 'sftp', 's3', 'azure', 'azblob']: return True + logger.error( f'unknown scheme: {url.scheme}') + return False + + # we have a pasw no user + if pasw: + # not sure... sftp hope to get user from .ssh/config + if url.scheme == 'sftp': return True + logger.error( f'password with no username specified') + return False + + # we only have a user ... permitted only for sftp + + if url.scheme != 'sftp': + logger.error( f'credential not found' ) + return False + + # sftp and an ssh_keyfile was provided... check that it exists + + if details and details.ssh_keyfile: + if not os.path.exists(details.ssh_keyfile): + logger.error( f'ssh_keyfile not found: {details.ssh_keyfile}') + return False + + # sftp with a user (and perhaps a valid ssh_keyfile) + + return True
+ + +
+[docs] + def _parse(self, line): + """Parse a line of a credentials file, add it to the CredentialDB. + + Args: + line (str): line to be parsed. + """ + #logger.debug("parse %s" % self.pwre.sub(':<secret!>@', line, count=1) ) + + try: + sline = line.strip() + if len(sline) == 0 or sline[0] == '#': return + + # first field url string = protocol://user:password@host:port[/vost] + parts = sline.split() + urlstr = parts[0] + url = urllib.parse.urlparse(urlstr) + + # credential details + details = Credential() + details.url = url + + # no option + if len(parts) == 1: + if not self.isValid(url, details): + logger.error("bad credential 1 (%s)" % line) + return + self.add(urlstr, details) + return + + # parsing options : comma separated option names + # some option has name = value : like ssh_keyfile + + optline = sline.replace(urlstr, '') + optline = optline.strip() + optlist = optline.split(',') + + for optval in optlist: + parts = optval.split('=') + keyword = parts[0].strip() + + if keyword == 'ssh_keyfile': + details.ssh_keyfile = os.path.expandvars(os.path.expanduser(parts[1].strip())) + elif keyword == 'passive': + details.passive = True + elif keyword == 'active': + details.passive = False + elif keyword == 'binary': + details.binary = True + elif keyword == 'ascii': + details.binary = False + elif keyword == 'ssl': + details.tls = False + elif keyword == 'tls': + details.tls = True + elif keyword == 'prot_p': + details.prot_p = True + elif keyword in ['bearer_token', 'bt']: + details.bearer_token = parts[1].strip() + elif keyword == 'login_method': + details.login_method = parts[1].strip() + elif keyword == 's3_session_token': + details.s3_session_token = urllib.parse.unquote(parts[1].strip()) + elif keyword == 's3_endpoint': + details.s3_endpoint = parts[1].strip() + elif keyword == 'azure_storage_credentials': + details.azure_credentials = urllib.parse.unquote(parts[1].strip()) + else: + logger.warning("bad credential option (%s)" % keyword) + + # need to check validity + if not self.isValid(url, details): + logger.error("bad credential 2 (%s)" % line) + return + + # seting options to protocol + + self.add(urlstr, details) + + except: + logger.error("credentials/parse %s" % line) + logger.debug('Exception details: ', exc_info=True)
+ + +
+[docs] + def read(self, path): + """Read in a file containing credentials (e.g. credentials.conf). All credentials are parsed and added to the + CredentialDB. + + Args: + path (str): path of file to be read. + """ + logger.debug("read") + + # read in provided credentials (not mandatory) + try: + if os.path.exists(path): + with open(path) as f: + lines = f.readlines() + + for line in lines: + self._parse(line) + except: + logger.error("credentials/read path = %s" % path) + logger.debug('Exception details: ', exc_info=True)
+ + #logger.debug("Credentials = %s\n" % self.credentials) + +
+[docs] + def _resolve(self, urlstr, url=None): + """Resolve credentials for AMQP vhost from ones passed as a string, and optionally a urllib.parse.ParseResult + object, into a sarracenia.credentials.Credential object. + + Args: + urlstr (str): credentials in a URL string. + url (urllib.parse.ParseResult): ParseResult object with creds. + + Returns: + tuple: containing + result (bool): ``False`` if the creds were not in the CredentialDB. ``True`` if they were. + details (sarracenia.credentials.Credential): the updated Credential object, or ``None``. + + + """ + + # create url object if needed + + if not url: + url = urllib.parse.urlparse(urlstr) + + # resolving credentials + + for s in self.credentials: + details = self.credentials[s] + u = details.url + + if url.scheme != u.scheme: continue + if url.hostname != u.hostname: continue + if url.port != u.port: continue + if url.username != u.username: + if url.username != None: continue + if url.password != u.password: + if url.password != None: continue + + # for AMQP... vhost checking + # amqp users have same credentials for any vhost + # default / may not be set... + if 'amqp' in url.scheme: + url_vhost = url.path + u_vhost = u.path + if url_vhost == '': url_vhost = '/' + if u_vhost == '': u_vhost = '/' + + if url_vhost != u_vhost: continue + + # resolved : cache it and return + + self.credentials[urlstr] = details + #logger.debug("Credentials get resolved %s %s" % (urlstr,details)) + return True, details + + return False, None
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/diskqueue.html b/_modules/sarracenia/diskqueue.html new file mode 100644 index 000000000..9bdc61d23 --- /dev/null +++ b/_modules/sarracenia/diskqueue.html @@ -0,0 +1,609 @@ + + + + + + sarracenia.diskqueue — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.diskqueue

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# more info: https://github.com/MetPX/sarracenia
+#
+# Code originally contributed by:
+#  Michel Grenier - Shared Services Canada
+#  first shot     : Wed Jan 10 16:06:16 UTC 2018
+#  re-factored beyond recognition by PSilva 2021. Don't blame Michel
+#
+
+from _codecs import decode, encode
+
+import jsonpickle, os, os.path, sarracenia, sys, time
+
+import logging
+
+# class sarra/retry
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class DiskQueue(): + """ + Process Persistent Queue... + + Persist messages to a file so that processing can be attempted again later. + For safety reasons, want to be writing to a file ASAP. + For performance reasons, all those writes need to be Appends. + + so continuous, but append-only io... with an occasional housekeeping cycle. + to resolve them + + not clear if we need multi-task safety... just one task writes to each queue. + + retry_ttl how long + + self.retry_cache + + * a dictionary indexed by some sort of key to prevent duplicate messages being stored in it. + + retry_path = ~/.cache/sr3/<component>/<config>/diskqueue_<name> + + with various suffixes: + + .new -- messages added to the retry list are appended to this file. + + whenever a message is added to the retry_cache, it is appended to a + cumulative list of entries to add to the retry list. + + every housekeeping interval, the two files are consolidated. + + note that the *ack_id* of messages retreived from the retry list, is + removed. Files must be acked around the time they are placed on the + retry_list, as reception from the source should have already been acknowledged. + + FIXME: would be fun to look at performance of this thing and compare it to + python persistent queue. the differences: + + This class does no locking (presumed single threading.) + could add locks... and they would be coarser grained than stuff in persistentqueue + this should be faster than persistent queue, but who knows what magic they did. + This class doesn't implement in-memory queue... it is entirely on disk... + saves memory, optimal for large queues. + probably good, since retries should be slow... + + not sure what will run better. + + """ +
+[docs] + def __init__(self, options, name): + + logger.debug(" %s __init__" % name) + + self.o = options + + self.name = name + + if not hasattr(self.o, 'retry_ttl'): + self.o.retry_ttl = None + + #logging.basicConfig(format=self.o.logFormat, + # level=getattr(logging, self.o.logLevel.upper())) + logger.setLevel(getattr(logging, self.o.logLevel.upper())) + + logger.debug('name=%s logLevel=%s' % (self.name, self.o.logLevel)) + + # initialize all retry path if retry_path is provided + self.working_dir = os.path.dirname(self.o.pid_filename) + + if not os.path.isdir(self.working_dir): + os.makedirs(self.working_dir) + + self.queue_file = self.working_dir + os.sep + 'diskqueue_' + name + self.now = sarracenia.nowflt() + + # retry messages + + self.queue_fp = None + + # newer retries + + self.new_path = self.queue_file + '.new' + self.new_fp = None + + # working file at housekeeping + self.housekeeping_path = self.queue_file + '.hk' + self.housekeeping_fp = None + + # initialize ages and message counts + + self.msg_count = 0 + self.msg_count_new = 0 + + if not os.path.isfile(self.queue_file): + return + + retry_age = os.path.getmtime(self.queue_file) + self.msg_count = self._count_msgs(self.queue_file) + + if os.path.isfile(self.new_path): + new_age = os.path.getmtime(self.new_path) + if retry_age > new_age: + os.unlink(self.new_path) + else: + self.msg_count_new = self._count_msgs(self.new_path)
+ + + + +
+[docs] + def put(self, message_list): + """ + add messages to the end of the queue. + """ + + if self.new_fp is None: + self.new_fp = open(self.new_path, 'a') + + for message in message_list: + logger.debug("DEBUG add to new file %s %s" % + (os.path.basename(self.new_path), message)) + self.new_fp.write(self.msgToJSON(message)) + self.msg_count_new += 1 + self.new_fp.flush()
+ + +
+[docs] + def cleanup(self): + """ + remove statefiles. + """ + if os.path.exists(self.queue_file): + os.unlink(self.queue_file) + self.msg_count = 0
+ + +
+[docs] + def close(self): + """ + clean shutdown. + """ + try: + self.housekeeping_fp.close() + except: + pass + try: + os.fsync(self.new_fp) + self.new_fp.close() + except: + pass + try: + self.queue_fp.close() + except: + pass + self.housekeeping_fp = None + self.new_fp = None + self.queue_fp = None + self.msg_count = 0 + self.msg_count_new = 0
+ + +
+[docs] + def _count_msgs(self, file_path) -> int: + """Count the number of messages (lines) in the queue file. This should be used only when opening an existing + file, because :func:`~sarracenia.diskqueue.DiskQueue.get` does not remove messages from the file. + + Args: + file_path (str): path to the file to be counted. + + Returns: + int: count of messages in file, -1 if the file could not be read. + """ + count = -1 + + if os.path.isfile(file_path): + count = 0 + with open(file_path, mode='r') as f: + for line in f: + if "{" in line: + count +=1 + + return count
+ + +
+[docs] + def __len__(self) -> int: + """Returns the total number of messages in the DiskQueue. + + Number of messages in the DiskQueue does not necessarily equal the number of messages available to ``get``. + Messages in the .new file are counted, but can't be retrieved until + :func:`~sarracenia.diskqueue.DiskQueue.on_housekeeping` has been run. + + Returns: + int: number of messages in the DiskQueue. + """ + return self.msg_count + self.msg_count_new
+ + + def msgFromJSON(self, line): + try: + msg = jsonpickle.decode(line) + except ValueError: + logger.error("corrupted line in retry file: %s " % line) + logger.debug("Error information: ", exc_info=True) + return None + + return msg + + def msgToJSON(self, message): + return jsonpickle.encode(message) + '\n' + +
+[docs] + def get(self, maximum_messages_to_get=1): + """ + qty number of messages to retrieve from the queue. + + """ + + ml = [] + count = 0 + while count < maximum_messages_to_get: + self.queue_fp, message = self.msg_get_from_file( + self.queue_fp, self.queue_file) + + # FIXME MG as discussed with Peter + # no housekeeping in get ... + # if no message (and new or state file there) + # we wait for housekeeping to present retry messages + if not message: + try: + os.unlink(self.queue_file) + except: + pass + self.queue_fp = None + #logger.debug("MG DEBUG retry get return None") + break + + if self.is_expired(message): + #logger.error("MG invalid %s" % message) + continue + + if 'ack_id' in message: + del message['ack_id'] + message['_deleteOnPost'].remove('ack_id') + + ml.append(message) + count += 1 + + self.msg_count -= count + + return ml
+ + +
+[docs] + def in_cache(self, message) -> bool: + """ + return whether the entry is message is in the cache or not. + side effect: adds it. + + """ + urlstr = message['baseUrl'] + '/' + message['relPath'] + + if 'noDupe' in message: + sumstr = jsonpickle.encode(message['noDupe']['key']) + elif 'fileOp' in message: + sumstr = jsonpickle.encode(message['fileOp']) + elif 'identity' in message: + sumstr = jsonpickle.encode(message['identity']) + elif 'pubTime' in message: + sumstr = jsonpickle.encode(message['pubTime']) + else: + logger.info('no key found for message, cannot add') + return False + + cache_key = urlstr + ' ' + sumstr + + if 'parts' in message: + cache_key += ' ' + message['parts'] + + if cache_key in self.retry_cache: return True + self.retry_cache[cache_key] = True + return False
+ + +
+[docs] + def is_expired(self, message) -> bool: + """ + return is the given message expired ? + """ + # no expiry + if self.o.retry_ttl is None: return False + if self.o.retry_ttl <= 0: return False + + # compute message age + msg_time = sarracenia.timestr2flt(message['pubTime']) + msg_age = self.now - msg_time + + # expired ? + return msg_age > self.o.retry_ttl
+ + +
+[docs] + def needs_requeuing(self, message) -> bool: + """ + return + * True if message is not expired, and not already in queue. + * False otherwise. + """ + if self.in_cache(message): + logger.info("discarding duplicate message (in %s cache) %s" % + (self.name, message)) + return False + + # log is info... it is good to log a retry message that expires + if self.is_expired(message): + logger.info("discarding expired message in (%s): %s" % + (self.name, message)) + return False + + return True
+ + +
+[docs] + def msg_get_from_file(self, fp, path): + """ + read a message from the state file. + """ + if fp is None: + if not os.path.isfile(path): return None, None + logger.debug("DEBUG %s open read" % path) + fp = open(path, 'r') + + line = fp.readline() + if not line: + try: + fp.close() + except: + pass + return None, None + + msg = self.msgFromJSON(line) + # a corrupted line : go to the next + if msg is None: return self.msg_get_from_file(fp, path) + + return fp, msg
+ + +
+[docs] + def on_housekeeping(self): + """ + + read rest of queue_file (from current point of unretried ones.) + - check if message is duplicate or expired. + - write to .hk + + read .new file, + - check if message is duplicate or expired. + - writing to .hk (housekeeping) + + remove .new + rename housekeeping to queue for next period. + """ + logger.debug("%s on_housekeeping" % self.name) + + # finish retry before reshuffling all retries entries + + if os.path.isfile(self.queue_file) and self.queue_fp != None: + logger.info( + "have not finished retry list. Resuming retries with %s" % + self.queue_file) + return + + self.now = sarracenia.nowflt() + self.retry_cache = {} + N = 0 + + # put this in try/except in case ctrl-c breaks something + + try: + self.close() + try: + os.unlink(self.housekeeping_path) + except: + pass + fp = open(self.housekeeping_path, 'w') + fp.close() + + i = 0 + last = None + + fp = self.queue_fp + self.housekeeping_fp = open(self.housekeeping_path, 'a') + + logger.debug("has queue %s" % + os.path.isfile(self.queue_file)) + + # remaining of retry to housekeeping + while True: + fp, message = self.msg_get_from_file(fp, self.queue_file) + if not message: break + i = i + 1 + if not self.needs_requeuing(message): continue + self.housekeeping_fp.write(self.msgToJSON(message)) + N = N + 1 + + try: + fp.close() + except: + pass + + i = 0 + j = N + + fp = None + # append new to housekeeping. + while True: + fp, message = self.msg_get_from_file(fp, self.new_path) + if not message: break + i = i + 1 + logger.debug("DEBUG message %s" % message) + if not self.needs_requeuing(message): continue + + #logger.debug("MG DEBUG flush retry to state %s" % message) + self.housekeeping_fp.write(self.msgToJSON(message)) + N = N + 1 + try: + fp.close() + except: + pass + + logger.debug("retrieved %d from the %d retry" % + (N - j, i)) + + self.housekeeping_fp.close() + + except Exception as Err: + logger.error("something went wrong") + logger.debug('Exception details: ', exc_info=True) + + # no more retry + + self.msg_count = N + if N == 0: + logger.debug("%s No retry in list" % self.name) + try: + os.unlink(self.housekeeping_path) + except: + pass + + # housekeeping file becomes new retry + + else: + logger.info("%s Number of messages in retry list %d" % (self.name, N)) + try: + os.rename(self.housekeeping_path, self.queue_file) + except: + logger.error("Something went wrong with rename") + + # cleanup + self.msg_count_new = 0 + try: + os.unlink(self.new_path) + except: + pass + + elapse = sarracenia.nowflt() - self.now + logger.debug("on_housekeeping elapse %f" % elapse)
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/filemetadata.html b/_modules/sarracenia/filemetadata.html new file mode 100644 index 000000000..9061ed923 --- /dev/null +++ b/_modules/sarracenia/filemetadata.html @@ -0,0 +1,359 @@ + + + + + + sarracenia.filemetadata — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.filemetadata

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# Questions or bugs report: dps-client@ec.gc.ca
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+# sr_post.py : python3 program allowing users to post an available product
+#
+# Code contributed by:
+#  Michel Grenier - Shared Services Canada
+#  Last Changed   : Nov  8 22:10:16 UTC 2017
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+from sarracenia.featuredetection import features
+
+if features['sftp']['present']:
+    import paramiko
+
+    FmdStat = paramiko.SFTPAttributes
+
+else:
+    class FmdStat(object):
+        def __init__(self):
+            """
+            (PAS: copied from paramiko)
+            Create a new (empty) SFTPAttributes object.  All fields will be empty.
+            """
+            self._flags = 0
+            self.st_size = None
+            self.st_uid = None
+            self.st_gid = None
+            self.st_mode = None
+            self.st_atime = None
+            self.st_mtime = None
+            self.attr = {}
+
+
+if features['xattr']['present']:
+    import xattr
+    supports_extended_attributes = True
+
+else:
+    supports_extended_attributes = False
+
+import sys
+
+supports_alternate_data_streams = False
+
+if sys.platform == 'win32':
+    try:
+        from sarracenia.pyads import ADS
+        supports_alternate_data_streams = True
+
+    except:
+        pass
+
+import json
+
+STREAM_NAME = 'sr_.json'
+
+xattr_disabled = False
+
+
+def disable_xattr():
+    global xattr_disabled
+    xattr_disabled = True
+
+
+
+[docs] +class FileMetadata: + r""" + This class implements storing metadata *with* a file. + + on unlix/linux/mac systems, we use extended attributes, + where we apply a *user.sr\_* prefix to the attribute names to avoid clashes. + + on Windows NT, create an "sr\_.json" Alternate Data Stream to store them. + + API: + + All values are utf-8, hence readable by some subset of humans. + not bytes. no binary, go away... + + x = sr_attr( path ) <- read metadata from file. + x.list() <- list all extant extended attributes. + + * sample return value: [ 'sum', 'mtime' ] + + x.get('sum') <- look at one value. + + * returns None if missing. + + x.set('sum', 'hoho') <- set one value. + + * fails silently (fall-back gracefully.) + + x.persist() <- write metadata back to file, if necessary. + + """ +
+[docs] + def __init__(self, path): + + global supports_alternate_data_streams + global supports_extended_attributes + + self.path = path + self.x = {} + self.dirty = False + + if xattr_disabled: + supports_alternate_data_streams = False + supports_extended_attributes = False + return + + if supports_alternate_data_streams: + self.ads = ADS(path) + s = list(self.ads) + if STREAM_NAME in s: + self.x = json.loads( + self.ads.get_stream_content(STREAM_NAME).decode('utf-8')) + + if supports_extended_attributes: + try: + d = xattr.listxattr(path) + for i in d: + if isinstance(i, bytes): + i = i.decode('utf-8') + if not i.startswith('user.sr_'): + continue + k = i.replace('user.sr_', '') + v = xattr.getxattr(path, i).decode('utf-8') + if v[0] == '{': + v = json.loads(v) + self.x[k] = v + except: + self.x = {} + + if 'integrity' in self.x: # id transition. + self.x['identity'] = self.x['integrity'] + del self.x['integrity']
+ + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.persist() + + def __del__(self): + self.persist() + + """ + return a dictionary of extended attributes. + + """ + +
+[docs] + def list(self): + """ + return the list of defined extended attributes. (keys to the dict.) + """ + return self.x.keys()
+ + +
+[docs] + def get(self, name) -> str: + """ + return the value of the named extended attribute. + """ + if name in self.x.keys(): + if name == 'blocks': + for k in ['manifest', 'waiting' ]: + m={} + if k in self.x['blocks']: + for db in self.x['blocks'][k]: # when json'd for writing, numeric indices are stringified. + m[db if type(db) is int else int(db)] = self.x['blocks'][k][db] + self.x['blocks'][k] = m + return self.x[name] + return None
+ + +
+[docs] + def set(self, name, value): + """ + set the name & value pair to the extended attributes for the file. + """ + self.dirty = True + self.x[name] = value
+ + +
+[docs] + def persist(self): + """ + write the in-memory extended attributes to disk. + """ + + global supports_alternate_data_streams + global supports_extended_attributes + + if not self.dirty: + return + + try: + if supports_alternate_data_streams: + + #replace STREAM_NAME with json.dumps(self.x) + s = list(self.ads) + if STREAM_NAME in s: + self.ads.delete_stream(STREAM_NAME) + + self.ads.add_stream_from_string( + STREAM_NAME, bytes(json.dumps(self.x, indent=4), 'utf-8')) + + if supports_extended_attributes: + #set the attributes in the list. encoding utf8... + for i in self.x: + if type(self.x[i]) is not str: + s = json.dumps(self.x[i]) + else: + s = self.x[i] + xattr.setxattr(self.path, 'user.sr_' + i, + bytes(s, 'utf-8')) + except: + # not really sure what to do in the exception case... + # permission would be a normal thing and just silently fail... + # could also be on windows, but not on an NTFS file system. + # silent failure means it falls back to using other means. + pass + + self.dirty = False
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow.html b/_modules/sarracenia/flow.html new file mode 100644 index 000000000..f7f75731a --- /dev/null +++ b/_modules/sarracenia/flow.html @@ -0,0 +1,2859 @@ + + + + + + sarracenia.flow — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow

+import copy
+import glob
+import importlib
+import logging
+import os
+import re
+
+# v3 plugin architecture...
+import sarracenia.flowcb
+import sarracenia.identity
+import sarracenia.transfer
+
+import stat
+import time
+import types
+import urllib.parse
+
+import sarracenia
+
+import sarracenia.filemetadata
+
+
+# for v2 subscriber routines...
+import json, os, sys, time
+
+from sys import platform as _platform
+
+from base64 import b64decode, b64encode
+from mimetypes import guess_type
+
+# end v2 subscriber
+
+from sarracenia.featuredetection import features
+
+if features['reassembly']['present']:
+    import sarracenia.blockmanifest
+
+from sarracenia import nowflt
+
+logger = logging.getLogger(__name__)
+
+allFileEvents = set(['create', 'delete', 'link', 'mkdir', 'modify','rmdir'])
+
+default_options = {
+    'accelThreshold': 0,
+    'acceptUnmatched': True,
+    'byteRateMax': None,
+    'discard': False,
+    'download': False,
+    'fileEvents': allFileEvents,
+    'housekeeping': 300,
+    'logReject': False,
+    'logFormat':
+    '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s',
+    'logLevel': 'info',
+    'mirror': True,
+    'permCopy': True,
+    'timeCopy': True,
+    'messageCountMax': 0,
+    'messageRateMax': 0,
+    'messageRateMin': 0,
+    'sleep': 0.1,
+    'topicPrefix': ['v03'],
+    'topicCopy': False,
+    'vip': []
+}
+
+if features['filetypes']['present']:
+    import magic
+
+if features['vip']['present']:
+    import netifaces
+
+
+
+[docs] +class Flow: + """ + Implement the General Algorithm from the Concepts Guide. + + All of the component types (e.g. poll, subscribe, sarra, winnow, shovel ) are implemented + as sub-classes of Flow. The constructor/factory accept a configuration + (sarracenia.config.Config class) with all the settings in it. + + This class takes care of starting up, running with callbacks, and clean shutdown. + + need to know whether to sleep between passes + o.sleep - an interval (floating point number of seconds) + o.housekeeping - + + A flow processes worklists of messages + + worklist given to callbacks... + + * worklist.incoming --> new messages to continue processing + * worklist.ok --> successfully processed + * worklist.rejected --> messages to not be further processed. + * worklist.failed --> messages for which processing failed. + * worklist.dirrectories_ok --> directories created. + + Initially all messages are placed in incoming. + if a callback decides: + + - a message is not relevant, it is moved to rejected. + - all processing has been done, it moves it to ok. + - an operation failed and it should be retried later, move to retry + + callbacks must not remove messages from all worklists, re-classify them. + it is necessary to put rejected messages in the appropriate worklist + so they can be acknowledged as received. + + interesting data structure: + self.plugins -- dict of modular functionality metadata. + + * self.plugins[ "load" ] contains a list of (v3) flow_callbacks to load. + + * self.plugins[ entry_point ] - one for each invocation times of callbacks. examples: + "on_start", "after_accept", etc... contains routines to run at each *entry_point* + + """ + @staticmethod + def factory(cfg): + + if cfg.flowMain: + flowMain=cfg.flowMain + else: + flowMain=cfg.component + + for sc in Flow.__subclasses__(): + if flowMain == sc.__name__.lower(): + return sc(cfg) + + if cfg.component == 'flow': + return Flow(cfg) + return None + +
+[docs] + def __init__(self, cfg=None): + """ + The cfg is should be an sarra/config object. + """ + + self._stop_requested = False + + me = 'flow' + logging.basicConfig( + format= + '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', + level=logging.DEBUG) + + self.o = cfg + + if 'sarracenia.flow.Flow' in self.o.settings and 'logLevel' in self.o.settings['sarracenia.flow.Flow']: + logger.setLevel( + getattr( + logging, + self.o.settings['sarracenia.flow.Flow']['logLevel'].upper())) + else: + logger.setLevel(getattr(logging, self.o.logLevel.upper())) + + if not hasattr(self.o, 'post_topicPrefix'): + self.o.post_topicPrefix = self.o.topicPrefix + + logging.basicConfig(format=self.o.logFormat, + level=getattr(logging, self.o.logLevel.upper())) + + self.plugins = {} + for entry_point in sarracenia.flowcb.entry_points: + self.plugins[entry_point] = [] + + # FIXME: open new worklist + self.worklist = types.SimpleNamespace() + self.worklist.ok = [] + self.worklist.incoming = [] + self.worklist.rejected = [] + self.worklist.failed = [] + self.worklist.directories_ok = [] + + # for poll only, mark if we are catching up on posted messages + # + self.worklist.poll_catching_up = False + + # Witness the creation of this list + self.plugins['load'] = self.o.plugins_early + [ + 'sarracenia.flowcb.retry.Retry', + 'sarracenia.flowcb.housekeeping.resources.Resources' + ] + + # open cache, get masks. + if self.o.nodupe_ttl > 0: + if self.o.nodupe_driver.lower() == "redis": + self.plugins['load'].append('sarracenia.flowcb.nodupe.redis.Redis') + else: + self.plugins['load'].append('sarracenia.flowcb.nodupe.disk.Disk') + + + if (( hasattr(self.o, 'delete_source') and self.o.delete_source ) or \ + ( hasattr(self.o, 'delete_destination') and self.o.delete_destination )) and \ + ('sarracenia.flowcb.work.delete.Delete' not in self.o.plugins_late): + self.o.plugins_late.append('sarracenia.flowcb.work.delete.Delete') + + # transport stuff.. for download, get, put, etc... + self.scheme = None + self.cdir = None + self.proto = {} + + # initialize plugins. + if hasattr(self.o, 'v2plugins') and len(self.o.v2plugins) > 0: + self.plugins['load'].append( + 'sarracenia.flowcb.v2wrapper.V2Wrapper') + + self.plugins['load'].extend(self.o.plugins_late) + + self.plugins['load'].extend(self.o.destfn_scripts) + + # metrics - dictionary with names of plugins as the keys + self.metricsFlowReset() + + self.had_vip = not os.path.exists( self.o.novipFilename )
+ + + def metricsFlowReset(self) -> None: + + self.new_metrics = { 'flow': { 'stop_requested': False, 'last_housekeeping': 0, + 'transferConnected': False, 'transferConnectStart': 0, 'transferConnectTime':0, + 'transferRxBytes': 0, 'transferTxBytes': 0, 'transferRxFiles': 0, 'transferTxFiles': 0 } } + + # carry over some metrics... that don't reset. + if hasattr(self,'metrics'): + if 'transferRxLast' in self.metrics: + self.new_metrics['transferRxLast'] = self.metrics['transferRxLast'] + + if 'transferTxLast' in self.metrics: + self.new_metrics['transferTxLast'] = self.metrics['transferTxLast'] + + self.metrics=self.new_metrics + + def loadCallbacks(self, plugins_to_load=None): + + if not plugins_to_load: + plugins_to_load=self.plugins['load'] + + for m in self.o.imports: + try: + importlib.import_module(m) + except Exception as ex: + logger.critical( f"python module import {m} load failed: {ex}" ) + logger.debug( "details:", exc_info=True ) + return False + + logger.info( f'flowCallback plugins to load: {plugins_to_load}' ) + for c in plugins_to_load: + try: + plugin = sarracenia.flowcb.load_library(c, self.o) + except Exception as ex: + logger.critical( f"flowCallback plugin {c} did not load: {ex}" ) + logger.debug( "details:", exc_info=True ) + return False + + #logger.debug( f'flowCallback plugin loading: {c} an instance of: {plugin}' ) + for entry_point in sarracenia.flowcb.entry_points: + if hasattr(plugin, entry_point): + fn = getattr(plugin, entry_point) + if callable(fn): + #logger.debug( f'registering {c}/{entry_point}' ) + if entry_point in self.plugins: + self.plugins[entry_point].append(fn) + else: + self.plugins[entry_point] = [fn] + + if not (hasattr(plugin, 'registered_as') + and callable(getattr(plugin, 'registered_as'))): + continue + + logger.debug('complete') + self.o.check_undeclared_options() + return True + + def _runCallbacksWorklist(self, entry_point): + + if hasattr(self, 'plugins') and (entry_point in self.plugins): + for p in self.plugins[entry_point]: + if self.o.logLevel.lower() == 'debug' : + p(self.worklist) + else: + try: + p(self.worklist) + except Exception as ex: + logger.error( f'flowCallback plugin {p}/{entry_point} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + + def runCallbacksTime(self, entry_point): + for p in self.plugins[entry_point]: + if self.o.logLevel.lower() == 'debug' : + p() + else: + try: + p() + except Exception as ex: + logger.error( f'flowCallback plugin {p}/{entry_point} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + +
+[docs] + def _runCallbackMetrics(self): + """Collect metrics from plugins with a ``metricsReport`` entry point. + + Expects the plugin to return a dictionary containing metrics, which is saved to ``self.metrics[plugin_name]``. + """ + + if 'transferConnected' in self.metrics['flow'] and self.metrics['flow']['transferConnected']: + now=nowflt() + self.metrics['flow']['transferConnectTime'] += now - self.metrics['flow']['transferConnectStart'] + self.metrics['flow']['transferConnectStart']=now + + modules=self.plugins["metricsReport"] + + if hasattr(self,'proto'): # gets re-spawned every batch, so not a permanent thing... + for scheme in self.proto: + if hasattr(self.proto[scheme], 'metricsReport'): + fn = getattr(self.proto[scheme], 'metricsReport') + if callable(fn): + modules.append( fn ) + + for p in modules: + if self.o.logLevel.lower() == 'debug' : + module_name = str(p.__module__).replace('sarracenia.flowcb.', '' ) + self.metrics[module_name] = p() + else: + try: + module_name = str(p.__module__).replace('sarracenia.flowcb.', '' ) + self.metrics[module_name] = p() + except Exception as ex: + logger.error( f'flowCallback plugin {p}/metricsReport crashed: {ex}' ) + logger.debug( "details:", exc_info=True )
+ + +
+[docs] + def _runHousekeeping(self, now) -> float: + """ Run housekeeping callbacks + Return the time when housekeeping should be run next + """ + logger.info(f'on_housekeeping pid: {os.getpid()} {self.o.component}/{self.o.config} instance: {self.o.no}') + self.runCallbacksTime('on_housekeeping') + self.metricsFlowReset() + self.metrics['flow']['last_housekeeping'] = now + + next_housekeeping = now + self.o.housekeeping + self.metrics['flow']['next_housekeeping'] = next_housekeeping + return next_housekeeping
+ + +
+[docs] + def has_vip(self) -> list: + """ + return list of vips which are active on the current machine, or an empty list. + """ + + if not features['vip']['present']: return True + + # no vip given... standalone always has vip. + if self.o.vip == []: + return [ 'AnyAddressIsFine' ] + + try: + for i in netifaces.interfaces(): + for a in netifaces.ifaddresses(i): + j = 0 + while (j < len(netifaces.ifaddresses(i)[a])): + k=netifaces.ifaddresses(i)[a][j].get('addr') + if k in self.o.vip: + return k + j += 1 + except Exception as ex: + logger.error( + f'error while looking for interfaces to compare with vip {self.o.vip}: {ex}' ) + logger.debug('Exception details: ', exc_info=True) + + return []
+ + +
+[docs] + def reject(self, m, code, reason) -> None: + """ + reject a message. + """ + self.worklist.rejected.append(m) + m.setReport(code, reason)
+ + + def please_stop(self) -> None: + logger.info( + f'ok, telling {len(self.plugins["please_stop"])} callbacks about it.' + ) + self.runCallbacksTime('please_stop') + self._stop_requested = True + self.metrics["flow"]['stop_requested'] = True + + + def close(self) -> None: + + self.runCallbacksTime('on_stop') + if os.path.exists( self.o.novipFilename ): + os.unlink( self.o.novipFilename ) + logger.info( + f'flow/close completed cleanly pid: {os.getpid()} {self.o.component}/{self.o.config} instance: {self.o.no}' + ) + + def ack(self, mlist) -> None: + if "ack" in self.plugins: + for p in self.plugins["ack"]: + if self.o.logLevel.lower() == 'debug' : + p(mlist) + else: + try: + p(mlist) + except Exception as ex: + logger.error( f'flowCallback plugin {p}/ack crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + + def _run_vip_update(self) -> bool: + + self.have_vip = self.has_vip() + if (self.o.component == 'poll') and not self.have_vip: + if self.had_vip: + logger.info("now passive on vips %s" % self.o.vip ) + with open( self.o.novipFilename, 'w' ) as f: + f.write(str(nowflt()) + '\n' ) + self.had_vip=False + else: + if not self.had_vip: + logger.info("now active on vip %s" % self.have_vip ) + self.had_vip=True + if os.path.exists( self.o.novipFilename ): + os.unlink( self.o.novipFilename ) + +
+[docs] + def run(self): + """ + This is the core routine of the algorithm, with most important data driven + loop in it. This implements the General Algorithm (as described in the Concepts Explanation Guide) + check if stop_requested once in a while, but never return otherwise. + """ + + + if not self.loadCallbacks(self.plugins['load']): + return + + logger.debug( f"working directory: {os.getpid()}" ) + + next_housekeeping = nowflt() + self.o.housekeeping + + current_rate = 0 + total_messages = 1 + start_time = nowflt() + now=start_time + current_sleep = self.o.sleep + last_time = start_time + self.metrics['flow']['last_housekeeping'] = start_time + + if self.o.logLevel == 'debug': + logger.debug("options:") + self.o.dump() + + logger.info("callbacks loaded: %s" % self.plugins['load']) + logger.info( + f'pid: {os.getpid()} {self.o.component}/{self.o.config} instance: {self.o.no}' + ) + + spamming = True + last_gather_len = 0 + stopping = False + + while True: + + if self._stop_requested: + if stopping: + logger.info('clean stop from run loop') + self.close() + break + else: + logger.info( 'starting last pass (without gather) through loop for cleanup.') + stopping = True + + self._run_vip_update() + + if now > next_housekeeping or stopping: + next_housekeeping = self._runHousekeeping(now) + elif now == start_time: + self.runCallbacksTime(f'on_start') + + self.worklist.incoming = [] + + if (self.o.component == 'poll') or self.have_vip: + + if ( self.o.messageRateMax > 0 ) and (current_rate > 0.8*self.o.messageRateMax ): + logger.info("current_rate (%.2f) vs. messageRateMax(%.2f)) " % (current_rate, self.o.messageRateMax)) + + if not stopping: + self.gather() + + last_gather_len = len(self.worklist.incoming) + if (last_gather_len == 0): + spamming = True + else: + current_sleep = self.o.sleep + spamming = False + + self.filter() + + # this for duplicate cache synchronization. + if self.worklist.poll_catching_up: + self.ack(self.worklist.incoming) + self.worklist.incoming = [] + + else: # normal processing, when you are active. + self.work() + self.post() + + now = nowflt() + run_time = now - start_time + total_messages += last_gather_len + + if (self.o.messageCountMax > 0) and (total_messages > self.o.messageCountMax): + self.please_stop() + + current_rate = total_messages / run_time + elapsed = now - last_time + + self.metrics['flow']['msgRate'] = current_rate + + if (last_gather_len == 0) and (self.o.sleep < 0): + if (self.o.retryEmptyBeforeExit and "retry" in self.metrics + and self.metrics['retry']['msgs_in_post_retry'] > 0): + logger.info("Not exiting because there are still messages in the post retry queue.") + # Sleep for a while. Messages can't be retried before housekeeping has run... + current_sleep = 60 + else: + self.please_stop() + + if spamming and (current_sleep < 5): + current_sleep *= 2 + + self.metrics['flow']['current_sleep'] = current_sleep + + # Run housekeeping based on time, and before stopping to ensure it's run at least once + if now > next_housekeeping or stopping: + next_housekeeping = self._runHousekeeping(now) + + if (self.o.messageRateMin > 0) and (current_rate < + self.o.messageRateMin): + logger.warning("receiving below minimum message rate") + + if (self.o.messageRateMax > 0) and (current_rate >= + self.o.messageRateMax): + stime = 1 + 2 * ((current_rate - self.o.messageRateMax) / + self.o.messageRateMax) + logger.info( + "current_rate/2 (%.2f) above messageRateMax(%.2f): throttling" + % (current_rate, self.o.messageRateMax)) + else: + logger.debug( f" not throttling: limit: {self.o.messageRateMax} " ) + stime = 0 + + if (current_sleep > 0): + if elapsed < current_sleep: + stime += current_sleep - elapsed + if stime > 60: # if sleeping for a long time, debug output is good... + logger.debug( + "sleeping for more than 60 seconds: %.2f seconds. Elapsed since wakeup: %.2f Sleep setting: %.2f " + % (stime, elapsed, self.o.sleep)) + else: + logger.debug('worked too long to sleep!') + last_time = now + continue + + if not self._stop_requested and (stime > 0): + # dividing into small sleeps so exit processing happens faster + # bug #595, still relatively low cpu usage in increment sized chunks. + if 5 < stime: + increment=5 + else: + increment=stime + while (stime > 0): + logger.debug( f"sleeping for {increment:.2f}" ) + time.sleep(increment) + if self._stop_requested: + break + else: + stime -= 5 + # Run housekeeping during long sleeps + now_for_hk = nowflt() + if now_for_hk > next_housekeeping: + next_housekeeping = self._runHousekeeping(now_for_hk) + + last_time = now
+ + + +
+[docs] + def sundew_getDestInfos(self, msg, currentFileOption, filename): + """ + modified from sundew client + + WHATFN -- First part (':') of filename + HEADFN -- Use first 2 fields of filename + NONE -- Use the entire filename + TIME or TIME: -- TIME stamp appended + DESTFN=fname -- Change the filename to fname + + ex: mask[2] = 'NONE:TIME' + """ + if currentFileOption is None or currentFileOption == 'None': return filename + + timeSuffix = '' + satnet = '' + parts = filename.split(':') + firstPart = parts[0] + + if 'sundew_extension' in msg.keys(): + parts = [parts[0]] + msg['sundew_extension'].split(':') + filename = ':'.join(parts) + + destFileName = filename + + for spec in currentFileOption.split(':'): + if spec == 'WHATFN': + destFileName = firstPart + elif spec == 'HEADFN': + headParts = firstPart.split('_') + if len(headParts) >= 2: + destFileName = headParts[0] + '_' + headParts[1] + else: + destFileName = headParts[0] + elif spec == 'SENDER' and 'SENDER=' in filename: + i = filename.find('SENDER=') + if i >= 0: destFileName = filename[i + 7:].split(':')[0] + if destFileName[-1] == ':': destFileName = destFileName[:-1] + elif spec == 'NONE': + if 'SENDER=' in filename: + i = filename.find('SENDER=') + destFileName = filename[:i] + else: + if len(parts) >= 6: + # PX default behavior : keep 6 first fields + destFileName = ':'.join(parts[:6]) + # PDS default behavior keep 5 first fields + if len(parts[4]) != 1: + destFileName = ':'.join(parts[:5]) + # extra trailing : removed if present + if destFileName[-1] == ':': destFileName = destFileName[:-1] + elif spec == 'NONESENDER': + if 'SENDER=' in filename: + i = filename.find('SENDER=') + j = filename.find(':', i) + destFileName = filename[:i + j] + else: + if len(parts) >= 6: + # PX default behavior : keep 6 first fields + destFileName = ':'.join(parts[:6]) + # PDS default behavior keep 5 first fields + if len(parts[4]) != 1: + destFileName = ':'.join(parts[:5]) + # extra trailing : removed if present + if destFileName[-1] == ':': destFileName = destFileName[:-1] + elif re.compile('SATNET=.*').match(spec): + satnet = ':' + spec + elif re.compile('DESTFN=.*').match(spec): + destFileName = spec[7:] + elif re.compile('DESTFNSCRIPT=.*').match(spec): + scriptclass = spec[13:].split('.')[-1] + for dfm in self.plugins['destfn']: + classname = dfm.__qualname__.split('.')[0] + if (scriptclass == classname) or (scriptclass.capitalize() == classname): + destFileName = dfm(msg) + elif spec == 'TIME': + timeSuffix = ':' + time.strftime("%Y%m%d%H%M%S", time.gmtime()) + if 'pubTime' in msg: + timeSuffix = ":" + msg['pubTime'].split('.')[0] + if 'pubTime' in msg: + timeSuffix = ":" + msg['pubTime'].split('.')[0] + timeSuffix = timeSuffix.replace('T', '') + # check for PX or PDS behavior ... + # if file already had a time extension keep his... + if len(parts[-1]) == 14 and parts[-1][0] == '2': + timeSuffix = ':' + parts[-1] + + else: + logger.error("Don't understand this DESTFN parameter: %s" % + spec) + return filename + return destFileName + satnet + timeSuffix
+ + + + # ============================================== + # how will the download file land on this server + # with all options, this is really tricky + # ============================================== + """ + to test changes to updateFieldsAccepted, run: make test_shim in the SarraC package... + because it tickles a lot of these settings, in addition to the flow_tests before + trying to PR changes here. + + """ +
+[docs] + def updateFieldsAccepted(self, msg, urlstr, pattern, maskDir, + maskFileOption, mirror, path_strip_count, pstrip, flatten) -> None: + """ + Set new message fields according to values when the message is accepted. + + * urlstr: the urlstr being matched (baseUrl+relPath+sundew_extension) + * pattern: the regex that was matched. + * maskDir: the current directory to base the relPath from. + * maskFileOption: filename option value (sundew compatibility options.) + * strip: number of path entries to strip from the left side of the path. + * pstrip: pattern strip regexp to apply instead of a count. + * flatten: a character to replace path separators with toe change a multi-directory + deep file name into a single long file name + + """ + + # relative path by default mirror + + relPath = '%s' % msg['relPath'] + + if self.o.baseUrl_relPath: + u = sarracenia.baseUrlParse(msg['baseUrl']) + relPath = u.path[1:] + '/' + relPath + + if self.o.download and 'rename' in msg: + # FIXME... why the % ? why not just assign it to copy the value? + relPath = '%s' % msg['rename'] + + # after download we dont propagate renaming... once used, get rid of it + del msg['rename'] + # FIXME: worry about publishing after a rename. + # the relpath should be replaced by rename value for downstream + # because the file was written to rename. + # not sure if this happens or not. + + + token = relPath.split('/') + filename = token[-1] + + # resolve a current base directory to which the relative path will eventually be added. + # update fileOp fields to replace baseDir. + #if self.o.currentDir : new_dir = self.o.currentDir + + new_dir='' + if maskDir: + new_dir = self.o.variableExpansion(maskDir, msg) + else: + if self.o.post_baseDir: + new_dir = self.o.variableExpansion(self.o.post_baseDir, msg) + d=None + if self.o.baseDir: + if new_dir: + d = new_dir + elif self.o.post_baseDir: + d = self.o.variableExpansion(self.o.post_baseDir, msg) + + # to get locally resolvable links and renames, need to mangle the pathnames. + # to get something to restore for downstream consumers, need to put the original + # names back. + + if 'fileOp' in msg: + msg['post_fileOp'] = copy.deepcopy(msg['fileOp']) + msg['_deleteOnPost'] |= set( [ 'post_fileOp' ] ) + + # if provided, strip (integer) ... strip N heading directories + # or pstrip (pattern str) strip regexp pattern from relPath + # cannot have both (see setting of option strip in sr_config) + + if path_strip_count > 0: + + logger.warning( f"path_strip_count:{path_strip_count} ") + strip=path_strip_count + if strip < len(token): + token = token[strip:] + + if 'fileOp' in msg: + """ + files are written with cwd being the directory containing the file written. + when stripping the root of the tree off, the path must be rendered relative to the + directory containing the file: the values are modified to create relative paths. + """ + for f in ['link', 'hlink', 'rename']: + if f in msg['fileOp']: + fopv = msg['fileOp'][f].split('/') + # an absolute path file posted is relative to '/' (in relPath) but the values in + # the link and rename fields may be absolute, requiring and adjustmeent when stripping + if fopv[0] == '': + strip += 1 + elif len(fopv) == 1: + toclimb=len(token)-1 + msg['fileOp'][f] = '../'*(toclimb) + fopv[0] + if len(fopv) > strip: + rest=fopv[strip:] + toclimb=len(token)-rest.count('..')-1 + msg['fileOp'][f] = '../'*(toclimb)+'/'.join(rest) + + # strip using a pattern + elif pstrip: + + #MG FIXME Peter's wish to have replacement in pstrip (ex.:${SOURCE}...) + + relstrip = re.sub(pstrip, '', relPath, 1) + + if not filename in relstrip: relstrip = filename + token = relstrip.split('/') + + if 'fileOp' in msg: + for f in ['link', 'hlink', 'rename']: + if f in msg['fileOp']: + msg['fileOp'][f] = re.sub(pstrip, '', msg['fileOp'][f] ) + + # if flatten... we flatten relative path + # strip taken into account + + if flatten != '/': + filename = flatten.join(token) + token[-1] = filename + + if 'fileOp' in msg: + for f in ['link', 'hlink', 'rename']: + if f in msg['fileOp']: + msg['fileOp'][f] = flatten.join(msg['fileOp'][f].split('/')) + + if self.o.baseDir: + # remove baseDir from relPath if present. + token_baseDir = self.o.baseDir.split('/')[1:] + remcnt=0 + if len(token) > len(token_baseDir): + for i in range(0,len(token_baseDir)): + if token_baseDir[i] == token[i]: + remcnt+=1 + else: + break + if remcnt == len(token_baseDir): + token=token[remcnt:] + + if d: + if 'fileOp' in msg and len(self.o.baseDir) > 1: + for f in ['link', 'hlink', 'rename']: + if (f in msg['fileOp']) : + if msg['fileOp'][f].startswith(self.o.baseDir): + msg['fileOp'][f] = msg['fileOp'][f].replace(self.o.baseDir, d, 1) + elif os.sep not in msg['fileOp'][f]: + toclimb=len(token)-1 + msg['fileOp'][f] = '../'*(toclimb) + msg['fileOp'][f] + + elif 'fileOp' in msg and new_dir: + u = sarracenia.baseUrlParse(msg['baseUrl']) + for f in ['link', 'hlink', 'rename']: + if (f in msg['fileOp']): + if (len(u.path) > 1): + if msg['fileOp'][f].startswith(u.path): + msg['fileOp'][f] = msg['fileOp'][f].replace(u.path, new_dir, 1) + elif '/' not in msg['fileOp'][f]: + toclimb=len(token)-1 + msg['fileOp'][f] = '../'*(toclimb) + msg['fileOp'][f] + + if self.o.mirror and len(token) > 1: + new_dir = new_dir + '/' + '/'.join(token[:-1]) + + new_dir = self.o.variableExpansion(new_dir, msg) + # resolution of sundew's dirPattern + + tfname = filename + # when sr_sender did not derived from sr_subscribe it was always called + new_dir = self.o.sundew_dirPattern(pattern, urlstr, tfname, new_dir) + msg.updatePaths(self.o, new_dir, filename) + + if maskFileOption: + msg['new_file'] = self.sundew_getDestInfos(msg, maskFileOption, filename) + msg['new_relPath'] = '/'.join( msg['new_relPath'].split('/')[0:-1] + [ msg['new_file'] ] )
+ + + + def filter(self) -> None: + + logger.debug( + 'start len(incoming)=%d, rejected=%d' % + (len(self.worklist.incoming), len(self.worklist.rejected))) + filtered_worklist = [] + + if hasattr(self.o, 'directory'): + default_accept_directory = self.o.directory + elif hasattr(self.o, 'post_baseDir'): + default_accept_directory = self.o.post_baseDir + elif hasattr(self.o, 'baseDir'): + default_accept_directory = self.o.baseDir + + now = nowflt() + for m in self.worklist.incoming: + then = sarracenia.timestr2flt(m['pubTime']) + lag = now - then + if self.o.messageAgeMax != 0 and lag > self.o.messageAgeMax: + self.reject( + m, 504, + "Excessive lag: %g sec. Skipping download of: %s, " % + (lag, m['new_file'])) + continue + + if 'fileOp' in m and 'rename' in m['fileOp']: + url = self.o.variableExpansion(m['baseUrl'], + m) + os.sep + m['fileOp']['rename'] + if 'sundew_extension' in m and url.count(":") < 1: + urlToMatch = url + ':' + m['sundew_extension'] + else: + urlToMatch = url + oldname_matched = False + for mask in self.o.masks: + pattern, maskDir, maskFileOption, mask_regexp, accepting, mirror, strip, pstrip, flatten = mask + if (pattern == '.*'): + oldname_matched = accepting + break + matches = mask_regexp.match(urlToMatch) + if matches: + m[ '_matches'] = matches + m['_deleteOnPost'] |= set(['_matches']) + oldname_matched = accepting + break + + url = self.o.variableExpansion(m['baseUrl'], m) + if (m['baseUrl'][-1] == '/') or (len(m['relPath']) > 0 and (m['relPath'][0] == '/')): + if (m['baseUrl'][-1] == '/') and (len(m['relPath'])>0) and (m['relPath'][0] == '/'): + url += m['relPath'][1:] + else: + url += m['relPath'] + else: + url += '/' + m['relPath'] + + if 'sundew_extension' in m and url.count(":") < 1: + urlToMatch = url + ':' + m['sundew_extension'] + else: + urlToMatch = url + + logger.debug( f" urlToMatch: {urlToMatch} " ) + # apply masks for accept/reject options. + matched = False + for mask in self.o.masks: + pattern, maskDir, maskFileOption, mask_regexp, accepting, mirror, strip, pstrip, flatten = mask + if (pattern != '.*') : + matches = mask_regexp.match(urlToMatch) + if matches: + m[ '_matches'] = matches + m['_deleteOnPost'] |= set(['_matches']) + + if (pattern == '.*') or matches: + matched = True + if not accepting: + if 'fileOp' in m and 'rename' in m['fileOp'] and oldname_matched: + # deletion rename case... need to accept with an extra field... + if not 'renameUnlink' in m: + m['renameUnlink'] = True + m['_deleteOnPost'] |= set(['renameUnlink']) + logger.debug("rename deletion 1 %s" % + (m['fileOp']['rename'])) + else: + self.reject( + m, 304, "mask=%s strip=%s url=%s" % + (str(mask), strip, urlToMatch)) + break + + self.updateFieldsAccepted(m, url, pattern, maskDir, + maskFileOption, mirror, strip, + pstrip, flatten) + + filtered_worklist.append(m) + break + + if not matched: + if 'fileOp' in m and ('rename' in m['fileOp']) and oldname_matched: + if not 'renameUnlink' in m: + m['renameUnlink'] = True + m['_deleteOnPost'] |= set(['renameUnlink']) + logger.debug("rename deletion 2 %s" % (m['fileOp']['rename'])) + filtered_worklist.append(m) + self.updateFieldsAccepted(m, url, None, + default_accept_directory, + self.o.filename, self.o.mirror, + self.o.strip, self.o.pstrip, + self.o.flatten) + continue + + if self.o.acceptUnmatched: + logger.debug("accept: unmatched pattern=%s" % (url)) + # FIXME... missing dir mapping with mirror, strip, etc... + self.updateFieldsAccepted(m, url, None, + default_accept_directory, + self.o.filename, self.o.mirror, + self.o.strip, self.o.pstrip, + self.o.flatten) + filtered_worklist.append(m) + else: + self.reject(m, 304, "unmatched pattern %s" % url) + + self.worklist.incoming = filtered_worklist + + logger.debug( 'end len(incoming)=%d, rejected=%d' % (len(self.worklist.incoming), len(self.worklist.rejected))) + + self._runCallbacksWorklist('after_accept') + + logger.debug( 'B filtered incoming: %d, ok: %d (directories: %d), rejected: %d, failed: %d stop_requested: %s have_vip: %s' + % (len(self.worklist.incoming), len(self.worklist.ok), len(self.worklist.directories_ok), + len(self.worklist.rejected), len(self.worklist.failed), self._stop_requested, self.have_vip)) + + self.ack(self.worklist.ok) + self.worklist.ok = [] + self.ack(self.worklist.rejected) + self.worklist.rejected = [] + self.ack(self.worklist.failed) + + + def gather(self) -> None: + so_far=0 + keep_going=True + for p in self.plugins["gather"]: + try: + retval = p(self.o.batch-so_far) + + # To avoid having to modify all existing gathers, support old API. + if type(retval) == tuple: + keep_going, new_incoming = retval + elif type(retval) == list: + new_incoming = retval + else: + logger.error( f"flowCallback plugin gather routine {p} returned unexpected type: {type(retval)}. Expected tuple of boolean and list of new messages" ) + except Exception as ex: + logger.error( f'flowCallback plugin {p} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + continue + + if len(new_incoming) > 0: + self.worklist.incoming.extend(new_incoming) + so_far += len(new_incoming) + + # if we gathered enough with a subset of plugins then return. + if not keep_going or (so_far >= self.o.batch): + if (self.o.component == 'poll' ): + self.worklist.poll_catching_up=True + + return + + # gather is an extended version of poll. + if self.o.component != 'poll': + return + + if len(self.worklist.incoming) > 0: + logger.info('ingesting %d postings into duplicate suppression cache' % len(self.worklist.incoming) ) + self.worklist.poll_catching_up = True + return + else: + self.worklist.poll_catching_up = False + + if self.have_vip: + for plugin in self.plugins['poll']: + new_incoming = plugin() + if len(new_incoming) > 0: + self.worklist.incoming.extend(new_incoming) + + + + def do(self) -> None: + + if self.o.download: + self.do_download() + else: + # mark all remaining messages as done. + self.worklist.ok = self.worklist.incoming + self.worklist.incoming = [] + + logger.debug('processing %d messages worked!' % len(self.worklist.ok)) + + def work(self) -> None: + + self.do() + + # need to acknowledge here, because posting will delete message-id + self.ack(self.worklist.ok) + self.ack(self.worklist.rejected) + self.ack(self.worklist.failed) + + # adjust message after action is done, but before 'after_work' so adjustment is possible. + for m in self.worklist.ok: + if ('new_baseUrl' in m) and (m['baseUrl'] != + m['new_baseUrl']): + m['old_baseUrl'] = m['baseUrl'] + m['_deleteOnPost'] |= set(['old_baseUrl']) + m['baseUrl'] = m['new_baseUrl'] + if ('new_retrievePath' in m) : + m['old_retrievePath'] = m['retrievePath'] + m['retrievePath'] = m['new_retrievePath'] + m['_deleteOnPost'] |= set(['old_retrievePath']) + + # if new_file does not match relPath, then adjust relPath so it does. + if 'relPath' in m and m['new_file'] != m['relPath'].split('/')[-1]: + if not 'new_relPath' in m: + if len(m['relPath']) > 1: + m['new_relPath'] = '/'.join( m['relPath'].split('/')[0:-1] + [ m['new_file'] ]) + else: + m['new_relPath'] = m['new_file'] + else: + if len(m['new_relPath']) > 1: + m['new_relPath'] = '/'.join( m['new_relPath'].split('/')[0:-1] + [ m['new_file'] ] ) + else: + m['new_relPath'] = m['new_file'] + + if ('new_relPath' in m) and (m['relPath'] != m['new_relPath']): + m['old_relPath'] = m['relPath'] + m['_deleteOnPost'] |= set(['old_relPath']) + m['relPath'] = m['new_relPath'] + m['old_subtopic'] = m['subtopic'] + m['_deleteOnPost'] |= set(['old_subtopic','subtopic']) + m['subtopic'] = m['new_subtopic'] + + if '_format' in m: + m['old_format'] = m['_format'] + m['_deleteOnPost'] |= set(['old_format']) + + if 'post_format' in m: + m['_format'] = m['post_format'] + + # restore adjustment to fileOp + if 'post_fileOp' in m: + m['fileOp'] = m['post_fileOp'] + + if self.o.download and 'retrievePath' in m: + # retrieve paths do not propagate after download. + del m['retrievePath'] + + self._runCallbacksWorklist('after_work') + + self.ack(self.worklist.rejected) + self.worklist.rejected = [] + self.ack(self.worklist.failed) + + + + def post(self) -> None: + + if len(self.plugins["post"]) > 0: + + # work-around for python3.5 not being able to copy re.match issue: + # https://github.com/MetPX/sarracenia/issues/857 + if sys.version_info.major == 3 and sys.version_info.minor <= 6: + for m in self.worklist.ok: + if '_matches' in m: + del m['_matches'] + + for p in self.plugins["post"]: + try: + p(self.worklist) + except Exception as ex: + logger.error( f'flowCallback plugin {p} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + + self._runCallbacksWorklist('after_post') + self._runCallbacksWorklist('report') + self._runCallbackMetrics() + + if hasattr(self.o, 'metricsFilename' ) and os.path.isdir(os.path.dirname(self.o.metricsFilename)): + metrics=json.dumps(self.metrics) + with open(self.o.metricsFilename, 'w') as mfn: + mfn.write(metrics+"\n") + if self.o.logMetrics: + if self.o.logRotateInterval >= 24*60*60: + tslen=8 + elif self.o.logRotateInterval > 60: + tslen=14 + else: + tslen=16 + timestamp=time.strftime("%Y%m%d-%H%M%S", time.gmtime()) + with open(self.o.metricsFilename + '.' + timestamp[0:tslen], 'a') as mfn: + mfn.write( f'\"{timestamp}\" : {metrics},\n') + + # removing old metrics files + logger.info( f"looking for old metrics for {self.o.metricsFilename}" ) + old_metrics=sorted(glob.glob(self.o.metricsFilename+'.*'))[0:-self.o.logRotateCount] + for o in old_metrics: + logger.info( f"removing old metrics file: {o} " ) + os.unlink(o) + + self.worklist.ok = [] + self.worklist.directories_ok = [] + self.worklist.failed = [] + +
+[docs] + def write_inline_file(self, msg) -> bool: + """ + write local file based on a message with inlined content. + + """ + # make sure directory exists, create it if not + if not os.path.isdir(msg['new_dir']): + try: + self.worklist.directories_ok.append(msg['new_dir']) + os.makedirs(msg['new_dir'], 0o775, True) + except Exception as ex: + logger.error("failed to make directory %s: %s" % + (msg['new_dir'], ex)) + return False + + logger.debug("data inlined with message, no need to download") + path = msg['new_dir'] + os.path.sep + msg['new_file'] + #path = msg['new_relPath'] + + try: + f = os.fdopen(os.open(path, os.O_RDWR | os.O_CREAT), 'rb+') + except Exception as ex: + logger.warning("could not open %s to write: %s" % (path, ex)) + return False + + if msg['content']['encoding'] == 'base64': + data = b64decode(msg['content']['value']) + else: + data = msg['content']['value'].encode(msg['content']['encoding']) + + if self.o.identity_method.startswith('cod,'): + algo_method = self.o.identity_method[4:] + elif msg['identity']['method'] == 'cod': + algo_method = msg['identity']['value'] + else: + algo_method = msg['identity']['method'] + + onfly_algo = sarracenia.identity.Identity.factory(algo_method) + data_algo = sarracenia.identity.Identity.factory(algo_method) + onfly_algo.set_path(path) + data_algo.set_path(path) + + if algo_method == 'arbitrary': + onfly_algo.value = msg['identity']['value'] + data_algo.value = msg['identity']['value'] + + onfly_algo.update(data) + + msg['onfly_checksum'] = { + 'method': algo_method, + 'value': onfly_algo.value + } + + if ((msg['size'] > 0) and len(data) != msg['size']): + if self.o.acceptSizeWrong: + logger.warning( + "acceptSizeWrong data size is (%d bytes) vs. expected: (%d bytes)" + % (len(data), msg['size'])) + else: + logger.warning( + "decoded data size (%d bytes) does not have expected size: (%d bytes)" + % (len(data), msg['size'])) + return False + + #try: + # for p in self.plugins['on_data']: + # data = p(data) + + #except Exception as ex: + # logger.warning("plugin failed: %s" % (p, ex)) + # return False + + data_algo.update(data) + + #FIXME: If data is changed by plugins, need to update content header. + # current code will reproduce the upstream message without mofification. + # need to think about whether that is OK or not. + + msg['data_checksum'] = { + 'method': algo_method, + 'value': data_algo.value + } + + msg['_deleteOnPost'] |= set(['onfly_checksum']) + + msg['_deleteOnPost'] |= set(['data_checksum']) + + try: + f.write(data) + f.truncate() + f.close() + self.set_local_file_attributes(path, msg) + + except Exception as ex: + logger.warning("failed writing and finalizing: %s" % (path, ex)) + return False + + return True
+ + + def compute_local_checksum(self, msg) -> None: + + if sarracenia.filemetadata.supports_extended_attributes: + try: + x = sarracenia.filemetadata.FileMetadata(msg['new_path']) + s = x.get('identity') + + if s: + metadata_cached_mtime = x.get('mtime') + if ((metadata_cached_mtime >= msg['mtime'])): + # file has not been modified since checksum value was stored. + + if (( 'identity' in msg ) and ( 'method' in msg['identity'] ) and \ + ( msg['identity']['method'] == s['method'] )) or \ + ( s['method'] == self.o.identity_method ) : + # file + # cache good. + msg['local_identity'] = s + msg['_deleteOnPost'] |= set(['local_identity']) + b = x.get('blocks') + msg['local_blocks'] = b + msg['_deleteOnPost'] |= set(['local_blocks']) + return + except: + pass + + local_identity = sarracenia.identity.Identity.factory( + msg['identity']['method']) + + if msg['identity']['method'] == 'arbitrary': + local_identity.value = msg['identity']['value'] + + local_identity.update_file(msg['new_path']) + msg['local_identity'] = { + 'method': msg['identity']['method'], + 'value': local_identity.value + } + msg['_deleteOnPost'] |= set(['local_identity']) + +
+[docs] + def file_should_be_downloaded(self, msg) -> bool: + """ + determine whether a comparison of local_file and message metadata indicates that it is new enough + that writing the file locally is warranted. + + return True to say downloading is warranted. + + False if the file in the message represents the same or an older version that what is corrently on disk. + + origin: refactor & translation of v2: content_should_not_be downloaded + + Assumptions: + new_path exists... there is a file to compare against. + """ + # assert + + lstat = sarracenia.stat(msg['new_path']) + fsiz = lstat.st_size + + # FIXME... local_offset... offset within the local file... partitioned... who knows? + # part of partitioning deferral. + #end = self.local_offset + self.length + if 'size' in msg: + end = msg['size'] + # compare sizes... if (sr_subscribe is downloading partitions into taget file) and (target_file isn't fully done) + # This check prevents random halting of subscriber (inplace on) if the messages come in non-sequential order + # target_file is the same as new_file unless the file is partitioned. + # FIXME If the file is partitioned, then it is the new_file with a partition suffix. + #if ('self.target_file == msg['new_file'] ) and ( fsiz != msg['size'] ): + if (fsiz != msg['size']): + logger.debug("%s file size different, so cannot be the same" % + (msg['new_path'])) + return True + else: + end = 0 + + # compare dates... + + if 'mtime' in msg: + new_mtime = sarracenia.timestr2flt(msg['mtime']) + old_mtime = 0.0 + + if self.o.timeCopy: + old_mtime = lstat.st_mtime + elif sarracenia.filemetadata.supports_extended_attributes: + try: + x = sarracenia.filemetadata.FileMetadata(msg['new_path']) + old_mtime = sarracenia.timestr2flt(x.get('mtime')) + except: + pass + + if new_mtime <= old_mtime: + self.reject(msg, 304, + "mtime not newer %s " % (msg['new_path'])) + return False + else: + logger.debug( + "{} new version is {} newer (new: {} vs old: {} )".format( + msg['new_path'], new_mtime - old_mtime, new_mtime, + old_mtime)) + + if 'identity' in msg and msg['identity']['method'] in ['random', 'cod']: + logger.debug("content_match %s sum 0/z never matches" % + (msg['new_path'])) + return True + + if end > fsiz: + logger.debug( + "new file not big enough... considered different") + return True + + if not 'identity' in msg: + # FIXME... should there be a setting to assume them the same? use cases may vary. + logger.debug( "no checksum available, assuming different" ) + return True + + try: + self.compute_local_checksum(msg) + except: + logger.debug( + "something went wrong when computing local checksum... considered different" + ) + return True + + logger.debug("checksum in message: %s vs. local: %s" % + (msg['identity'], msg['local_identity'])) + + if msg['local_identity'] == msg['identity']: + self.reject(msg, 304, "same checksum %s " % (msg['new_path'])) + return False + else: + return True
+ + +
+[docs] + def removeOneFile(self, path) -> bool: + """ + process an unlink event, returning boolean success. + """ + + logger.debug("path to remove: %s" % path) + + ok = True + try: + if os.path.isfile(path): os.unlink(path) + if os.path.islink(path): os.unlink(path) + if os.path.isdir(path): os.rmdir(path) + logger.info("removed %s" % path) + except: + logger.error("could not remove %s." % path) + logger.debug('Exception details: ', exc_info=True) + ok = False + + return ok
+ + +
+[docs] + def renameOneItem(self, old, path) -> bool: + """ + for messages with an rename file operation, it is to rename a file. + """ + ok = True + logger.info( f" pwd is {os.getcwd()} " ) + if not os.path.exists(old): + logger.info( + "old file %s not found, if destination (%s) missing, then fall back to copy" + % (old, path)) + # if the destination file exists, assume rename already happenned, + # otherwis return false so that caller falls back to downloading/sending the file. + # return os.path.isfile(path) + # PAS 2022/12/01 ... only 1 message to interpret, will never be a previous rename. + return False + + try: + + if os.path.isfile(path): os.unlink(path) + if os.path.islink(path): os.unlink(path) + if os.path.isdir(path): os.rmdir(path) + os.rename(old, path) + logger.info("renamed %s -> %s" % (old, path)) + except: + logger.error( + "sr_subscribe/doit_download: could not rename %s to %s " % + (old, path)) + logger.debug('Exception details: ', exc_info=True) + ok = False + return ok
+ + +
+[docs] + def mkdir(self, msg) -> bool: + """ + perform an mkdir. + """ + + ok=False + path = msg['new_dir'] + '/' + msg['new_file'] + logger.debug( f"message is to mkdir {path}" ) + + if not os.path.isdir(msg['new_dir']): + try: + os.makedirs(msg['new_dir'], self.o.permDirDefault, True) + except Exception as ex: + logger.warning("making %s: %s" % (msg['new_dir'], ex)) + logger.debug('Exception details:', exc_info=True) + + if os.path.isdir(path): + logger.debug( f"no need to mkdir {path} as it exists" ) + return True + + if 'mode' in msg: + mode=msg['mode'] + else: + mode=self.o.permDirDefault + + if type(mode) is not int: + mode=int(mode,base=8) + + try: + os.mkdir(path,mode=mode) + ok=True + except Exception as ex: + logger.error( f"mkdir {path} failed." ) + logger.debug('Exception details:', exc_info=True) + return ok
+ + +
+[docs] + def link1file(self, msg, symbolic=True) -> bool: + """ + perform a link of a single file, based on a message, returning boolean success + if it's Symbolic, then do that. else do a hard link. + + imported from v2/subscribe/doit_download "link event, try to link the local product given by message" + """ + logger.debug("message is to link %s to %s" % + (msg['new_file'], msg['fileOp']['link'])) + + # redundant, check is done in caller. + #if not 'link' in self.o.fileEvents: + # logger.info("message to link %s to %s ignored (events setting)" % \ + # ( msg['new_file'], msg['fileOp'][ 'link' ] ) ) + # return False + + if not os.path.isdir(msg['new_dir']): + try: + self.worklist.directories_ok.append(msg['new_dir']) + os.makedirs(msg['new_dir'], self.o.permDirDefault, True) + except Exception as ex: + logger.warning("making %s: %s" % (msg['new_dir'], ex)) + logger.debug('Exception details:', exc_info=True) + + ok = True + try: + path = msg['new_dir'] + '/' + msg['new_file'] + + if os.path.isfile(path): os.unlink(path) + if os.path.islink(path): os.unlink(path) + #if os.path.isdir(path): os.rmdir(path) + + if 'hlink' in msg['fileOp'] : + os.link(msg['fileOp']['hlink'], path) + logger.info("%s hard-linked to %s " % (msg['new_file'], msg['fileOp']['hlink'])) + else: + os.symlink(msg['fileOp']['link'], path) + logger.info("%s sym-linked to %s " % (msg['new_file'], msg['fileOp']['link'])) + + except: + ok = False + logger.error("link of %s %s failed." % + (msg['new_file'], msg['fileOp'])) + logger.debug('Exception details:', exc_info=True) + + return ok
+ + +
+[docs] + def do_download(self) -> None: + """ + do download work for self.worklist.incoming, placing files: + successfully downloaded in worklist.ok + temporary failures in worklist.failed + permanent failures (or files not to be downloaded) in worklist.rejected + + """ + + if not self.o.download: + self.worklist.ok = self.worklist.incoming + self.worklist.incoming = [] + return + + for msg in self.worklist.incoming: + + if 'newname' in msg: + """ + revamped rename algorithm requires only 1 message, ignore newname. + """ + self.worklist.ok.append(msg) + continue + + new_path = msg['new_dir'] + os.path.sep + msg['new_file'] + new_file = msg['new_file'] + + if not os.path.isdir(msg['new_dir']): + try: + logger.info( f"missing destination directories, makedirs: {msg['new_dir']} " ) + self.worklist.directories_ok.append(msg['new_dir']) + os.makedirs(msg['new_dir'], 0o775, True) + except Exception as ex: + logger.warning("making %s: %s" % (msg['new_dir'], ex)) + logger.debug('Exception details:', exc_info=True) + + os.chdir(msg['new_dir']) + logger.debug( f"chdir {msg['new_dir']}") + + if 'fileOp' in msg : + if 'rename' in msg['fileOp']: + + if 'renameUnlink' in msg: + self.removeOneFile(msg['fileOp']['rename']) + msg.setReport(201, 'old unlinked %s' % msg['fileOp']['rename']) + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + + else: + # actual rename... + ok = self.renameOneItem(msg['fileOp']['rename'], new_path) + # if rename succeeds, fall through to download object to find if the file renamed + # actually matches the one advertised, and potentially download it. + # if rename fails, recover by falling through to download the data anyways. + if ok: + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + msg.setReport(201, 'renamed') + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + continue + + elif ('directory' in msg['fileOp']) and ('remove' in msg['fileOp'] ): + if 'rmdir' not in self.o.fileEvents: + self.reject(msg, 202, "skipping rmdir %s" % new_path) + continue + + if self.removeOneFile(new_path): + msg.setReport(201, 'rmdired') + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + else: + #FIXME: should this really be queued for retry? or just permanently failed? + # in rejected to avoid retry, but wondering if failed and deferred + # should be separate lists in worklist... + self.reject(msg, 500, "rmdir %s failed" % new_path) + continue + + elif ('remove' in msg['fileOp']): + if 'delete' not in self.o.fileEvents: + self.reject(msg, 202, "skipping delete %s" % new_path) + continue + + if self.removeOneFile(new_path): + msg.setReport(201, 'removed') + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + else: + #FIXME: should this really be queued for retry? or just permanently failed? + # in rejected to avoid retry, but wondering if failed and deferred + # should be separate lists in worklist... + self.reject(msg, 500, "remove %s failed" % new_path) + continue + + # no elif because if rename fails and operation is an mkdir or a symlink.. + # need to retry as ordinary creation, similar to normal file copy case. + if 'directory' in msg['fileOp']: + if 'mkdir' not in self.o.fileEvents: + self.reject(msg, 202, "skipping mkdir %s" % new_path) + continue + + if self.mkdir(msg): + msg.setReport(201, 'made directory') + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + else: + # as above... + self.reject(msg, 500, "mkdir %s failed" % msg['new_file']) + continue + + elif 'link' in msg['fileOp'] or 'hlink' in msg['fileOp']: + if 'link' not in self.o.fileEvents: + self.reject(msg, 202, "skipping link %s" % new_path) + continue + + if self.link1file(msg): + msg.setReport(201, 'linked') + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxFiles'] += 1 + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + else: + # as above... + self.reject(msg, 500, "link %s failed" % msg['fileOp']) + continue + + # establish new_inflight_path which is the file to download into initially. + if self.o.inflight == None or ( + ('blocks' in msg) and (msg['blocks']['method'] == 'inplace')): + new_inflight_path = msg['new_file'] + elif type(self.o.inflight) == str: + if self.o.inflight == '.': + new_inflight_path = '.' + new_file + elif (self.o.inflight[-1] == '/') or (self.o.inflight[0] == '/'): + if not self.o.dry_run and not os.path.isdir(self.o.inflight): + try: + os.mkdir(self.o.inflight) + os.chmod(self.o.inflight, self.o.permDirDefault) + except: + pass + new_inflight_path = self.o.inflight + new_file + elif self.o.inflight[0] == '.': + new_inflight_path = new_file + self.o.inflight + else: + #inflight is interval: minimum the age of the source file, as per message. + logger.error('interval inflight setting: %s, not appropriate for downloads.' % + self.o.inflight) + # FIXME... what to do? + self.reject( + msg, 503, "invalid inflight %s settings %s" % + (self.o.inflight, new_path)) + continue + + msg['new_inflight_path'] = new_inflight_path + msg['new_path'] = new_path + + msg['_deleteOnPost'] |= set(['new_path']) + msg['_deleteOnPost'] |= set(['new_inflight_path']) + + # assert new_inflight_path is set. + + if os.path.exists(msg['new_inflight_path']): + + if self.o.inflight: + how_old = time.time() - os.path.getmtime(msg['new_inflight_path']) + #FIXME: if mtime > 5 minutes, perhaps rm it, and continue? what if transfer crashed? + # Added this with fixed value, should it be a setting? + if how_old > 300: + os.unlink( msg['new_inflight_path'] ) + logger.info( + f"inflight file is {how_old}s old. Removed previous attempt {msg['new_path']}" ) + else: + logger.warning( + 'inflight file already exists. race condition, deferring transfer of %s' + % msg['new_path']) + self.worklist.failed.append(msg) + continue + # overwriting existing file. + + # FIXME: decision of whether to download, goes here. + if os.path.isfile(new_path): + if not self.o.overwrite: + self.reject(msg, 204, + "not overwriting existing file %s" % new_path) + continue + + if not self.file_should_be_downloaded(msg): + continue + + # download content + if 'content' in msg.keys(): + if self.write_inline_file(msg): + msg.setReport(201, "Download successful (inline content)") + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + continue + logger.warning( + "failed to write inline content %s, falling through to download" + % new_path) + + parsed_url = sarracenia.baseUrlParse(msg['baseUrl']) + self.scheme = parsed_url.scheme + + i = 1 + while i <= self.o.attempts: + + if i > 1: + logger.warning("downloading again, attempt %d" % i) + + ok = self.download(msg, self.o) + if ok: + logger.debug("downloaded ok: %s" % new_path) + msg.setReport(201, "Download successful" ) + # if content is present, but downloaded anyways, then it is no good, and should not be forwarded. + if 'content' in msg: + del msg['content'] + self.worklist.ok.append(msg) + self.metrics['flow']['transferRxLast'] = msg['report']['timeCompleted'] + break + else: + logger.info("attempt %d failed to download %s/%s to %s" \ + % ( i, msg['baseUrl'], msg['relPath'], new_path) ) + i = i + 1 + + if not ok: + logger.error( + "gave up downloading for now, appending to retry queue") + self.worklist.failed.append(msg) + # FIXME: file reassembly missing? + #if self.inplace : file_reassemble(self) + + self.worklist.incoming = []
+ + + # v2 sr_util.py ... generic sr_transport imported here... + + # generalized download... +
+[docs] + def download(self, msg, options) -> bool: + """ + download/transfer one file based on message, return True if successful, otherwise False. + """ + + self.o = options + + if 'retrievePath' in msg: + logger.debug("%s_transport download override retrievePath=%s" % (self.scheme, msg['retrievePath'])) + remote_file = msg['retrievePath'] + cdir = None + if msg['relPath'][0] == '/' or msg['baseUrl'][-1] == '/': + urlstr = msg['baseUrl'] + msg['relPath'] + else: + urlstr = msg['baseUrl'] + '/' + msg['relPath'] + else: + logger.debug("%s_transport download relPath=%s" % (self.scheme, msg['relPath'])) + + # split the path to the file and the file + # if relPath is just the file remote_path will return empty + remote_path, remote_file = os.path.split(msg['relPath']) + + u = sarracenia.baseUrlParse(msg['baseUrl']) + logger.debug( f"baseUrl.path= {u.path} ") + if remote_path: + if u.path: + if ( u.path[-1] != '/' ) and ( remote_path[0] != '/' ) : + remote_path = u.path + '/' + remote_path + else: + remote_path = u.path + remote_path + + cdir = remote_path + else: + if u.path: + cdir=u.path + else: + cdir=None + + if msg['relPath'][0] == '/' or msg['baseUrl'][-1] == '/': + urlstr = msg['baseUrl'] + msg['relPath'] + else: + urlstr = msg['baseUrl'] + '/' + msg['relPath'] + + + istr =msg['identity'] if ('identity' in msg) else "None" + fostr = msg['fileOp'] if ('fileOp' in msg ) else "None" + + logger.debug( 'identity: %s, fileOp: %s' % ( istr, fostr ) ) + new_inflight_path = '' + + new_dir = msg['new_dir'] + new_file = msg['new_file'] + new_inflight_path = None + + if 'blocks' in msg: + if msg['blocks']['method'] in [ 'inplace' ]: # download only a specific block from a file, not the whole thing. + logger.debug( f"splitting 1 file into {len(msg['blocks']['manifest'])} block messages." ) + blkno = msg['blocks']['number'] + blksz_l = sarracenia.naturalSize(msg['blocks']['size']).split() + blksz = blksz_l[0]+blksz_l[1][0].lower() + if not '§block_' in new_file: + new_file += f"§block_{blkno:04d},{blksz}_§" + msg['new_file'] = new_file + + if options.inflight == None: + new_inflight_path = new_file + elif type(options.inflight) == str: + if options.inflight == '.': + new_inflight_path = '.' + new_file + elif ( options.inflight[-1] == '/' ) or (options.inflight[0] == '/'): + new_inflight_path = options.inflight + new_file + elif options.inflight[0] == '.': + new_inflight_path = new_file + options.inflight + else: + logger.error('inflight setting: %s, not for downloads.' % + options.inflight) + if new_inflight_path: + msg['new_inflight_path'] = new_inflight_path + msg['_deleteOnPost'] |= set(['new_inflight_path']) + + if 'download' in self.plugins and len(self.plugins['download']) > 0: + for plugin in self.plugins['download']: + try: + ok = plugin(msg) + except Exception as ex: + logger.error( f'flowCallback plugin {plugin} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + + if not ok: return False + return True + + if self.o.dry_run: + curdir = new_dir + else: + try: + curdir = os.getcwd() + except: + curdir = None + + if curdir != new_dir: + # make sure directory exists, create it if not + try: + if not os.path.isdir(new_dir): + self.worklist.directories_ok.append(new_dir) + os.makedirs(new_dir, 0o775, True) + os.chdir(new_dir) + logger.debug( f"local cd to {new_dir}") + except Exception as ex: + logger.warning("making %s: %s" % (new_dir, ex)) + logger.debug('Exception details:', exc_info=True) + return False + + try: + options.sendTo = msg['baseUrl'] + + if (not (self.scheme in self.proto)) or (self.proto[self.scheme] is None): + self.proto[self.scheme] = sarracenia.transfer.Transfer.factory(self.scheme, self.o) + self.metrics['flow']['transferConnected'] = True + self.metrics['flow']['transferConnectStart'] = time.time() + + if (not self.o.dry_run) and not self.proto[self.scheme].check_is_connected(): + + if self.metrics['flow']['transferConnected']: + now=nowflt() + self.metrics['flow']['transferConnectTime'] += now - self.metrics['flow']['transferConnectStart'] + self.metrics['flow']['transferConnectStart'] = 0 + self.metrics['flow']['transferConnected'] = False + + logger.debug("%s_transport download connects" % self.scheme) + ok = self.proto[self.scheme].connect() + if not ok: + self.proto[self.scheme] = None + return False + + self.metrics['flow']['transferConnected'] = True + self.metrics['flow']['transferConnectStart'] = time.time() + logger.debug('connected') + + #================================= + # if parts, check that the protol supports it + #================================= + + #if not hasattr(proto,'seek') and ('blocks' in msg) and ( msg['blocks']['method'] == 'inplace' ): + # logger.error("%s, inplace part file not supported" % self.scheme) + # return False + + cwd = None + + if (not self.o.dry_run) and hasattr(self.proto[self.scheme], 'getcwd'): + cwd = self.proto[self.scheme].getcwd() + logger.debug( f" from proto getcwd: {cwd} ") + + if cdir and cwd != cdir: + logger.debug("%s_transport remote cd to %s" % (self.scheme, cdir)) + if self.o.dry_run: + cwd = cdir + else: + try: + self.proto[self.scheme].cd(cdir) + except Exception as ex: + logger.error("chdir %s: %s" % (cdir, ex)) + return False + + remote_offset = 0 + exactLength=False + if ('blocks' in msg) and (msg['blocks']['method'] == 'inplace'): + blkno=msg['blocks']['number'] + remote_offset=0 + exactLength=True + while blkno > 0: + blkno -= 1 + remote_offset += msg['blocks']['manifest'][blkno]['size'] + + block_length=msg['blocks']['manifest'][msg['blocks']['number']]['size'] + logger.info( f"offset calculation: start={remote_offset} count={block_length}" ) + + elif 'size' in msg: + block_length = msg['size'] + else: + block_length = 0 + + #download file + + logger.debug( + 'Beginning fetch of %s %d-%d into %s %d-%d' % + (urlstr, remote_offset, block_length-1, new_inflight_path, msg['local_offset'], + msg['local_offset'] + block_length - 1)) + + # FIXME locking for i parts in temporary file ... should stay lock + # and file_reassemble... take into account the locking + + if self.o.identity_method.startswith('cod,'): + download_algo = self.o.identity_method[4:] + elif 'identity' in msg: + download_algo = msg['identity']['method'] + else: + download_algo = None + + if download_algo: + self.proto[self.scheme].set_sumalgo(download_algo) + + if download_algo == 'arbitrary': + self.proto[self.scheme].set_sumArbitrary( + msg['identity']['value']) + + if (type(options.inflight) == str) \ + and (options.inflight[0] == '/' or options.inflight[-1] == '/') \ + and not os.path.exists(options.inflight): + + try: + if not self.o.dry_run: + os.mkdir(options.inflight) + os.chmod(options.inflight, options.permDirDefault) + except: + logger.error('unable to make inflight directory %s/%s' % + (msg['new_dir'], options.inflight)) + logger.debug('Exception details: ', exc_info=True) + + logger.debug( "hasAccel=%s, thresh=%d, len=%d, remote_off=%d, local_off=%d inflight=%s" % \ + ( hasattr( self.proto[self.scheme], 'getAccelerated' ), \ + self.o.accelThreshold, block_length, remote_offset, msg['local_offset'], new_inflight_path ) ) + + accelerated = hasattr( self.proto[self.scheme], 'getAccelerated') and \ + (self.o.accelThreshold > 0 ) and (block_length > self.o.accelThreshold) and \ + (remote_offset == 0) and ( msg['local_offset'] == 0) + + if not self.o.dry_run: + try: + if accelerated: + len_written = self.proto[self.scheme].getAccelerated( + msg, remote_file, new_inflight_path, block_length, remote_offset, exactLength) + #FIXME: no onfly_checksum calculation during download. + else: + self.proto[self.scheme].set_path(new_inflight_path) + len_written = self.proto[self.scheme].get( + msg, remote_file, new_inflight_path, remote_offset, + msg['local_offset'], block_length, exactLength) + except Exception as ex: + logger.error( f"could not get {remote_file}: {ex}" ) + return False + + else: + len_written = block_length + + if ('blocks' in msg) and (msg['blocks']['method'] == 'inplace'): + msg['blocks']['method'] = 'separate' + + if (len_written == block_length): + if not self.o.dry_run: + if accelerated: + self.proto[self.scheme].update_file(new_inflight_path) + elif len_written < 0: + logger.error("failed to download %s" % new_file) + if (self.o.inflight != None) and os.path.isfile(new_inflight_path): + os.remove(new_inflight_path) + return False + else: + if block_length == 0: + if self.o.acceptSizeWrong: + logger.debug( + 'AcceptSizeWrong %d of with no length given for %s assuming ok' + % (len_written, new_inflight_path)) + else: + logger.warning( + 'downloaded %d of with no length given for %s assuming ok' + % (len_written, new_inflight_path)) + else: + if self.o.acceptSizeWrong: + logger.debug( + 'AcceptSizeWrong download size mismatch, received %d of expected %d bytes for %s' + % (len_written, block_length, new_inflight_path)) + else: + if len_written > block_length: + logger.error( f'download more {len_written} than expected {block_length} bytes for {new_inflight_path}' ) + else: + logger.error( f'incomplete download only {len_written} of expected {block_length} bytes for {new_inflight_path}' ) + if (self.o.inflight != None) and os.path.isfile(new_inflight_path): + os.remove(new_inflight_path) + return False + # when len_written is different than block_length + msg['size'] = len_written + + # if we haven't returned False by this point, assuming download was successful + if (new_inflight_path != new_file): + if os.path.isfile(new_file): + os.remove(new_file) + os.rename(new_inflight_path, new_file) + + # older versions don't include the contentType, so patch it here. + if features['filetypes']['present'] and 'contentType' not in msg: + msg['contentType'] = magic.from_file(new_file,mime=True) + + self.metrics['flow']['transferRxBytes'] += len_written + self.metrics['flow']['transferRxFiles'] += 1 + + if download_algo and not self.o.dry_run: + msg['onfly_checksum'] = self.proto[self.scheme].get_sumstr() + msg['data_checksum'] = self.proto[self.scheme].data_checksum + + if self.o.identity_method.startswith('cod,') and not accelerated: + msg['identity'] = msg['onfly_checksum'] + + msg['_deleteOnPost'] |= set(['onfly_checksum']) + msg['_deleteOnPost'] |= set(['data_checksum']) + + # fix message if no partflg (means file size unknown until now) + #if not 'blocks' in msg: + # #msg['size'] = self.proto[self.scheme].fpos ... fpos not set when accelerated. + + # fix permission + if not self.o.dry_run: + self.set_local_file_attributes(new_file, msg) + + if options.delete and hasattr(self.proto[self.scheme], 'delete'): + try: + if not self.o.dry_run: + self.proto[self.scheme].delete(remote_file) + logger.debug('file deleted on remote site %s' % + remote_file) + except Exception as ex: + logger.error( f'unable to delete remote file {remote_file}: {ex}' ) + logger.debug('Exception details: ', exc_info=True) + + if (self.o.acceptSizeWrong or (block_length == 0)) and (len_written > 0): + return True + + if (len_written != block_length): + return False + + except Exception as ex: + logger.debug('Exception details: ', exc_info=True) + logger.warning("failed to write %s: %s" % (new_inflight_path, ex)) + + #closing on problem + if not self.o.dry_run: + try: + self.proto[self.scheme].close() + except: + logger.debug('closing exception details: ', exc_info=True) + self.metrics['flow']["transferConnected"] = False + if 'transferConnectLast' in self.metrics['flow']: + self.metrics['flow']['transferConnectedTime'] = time.time() - self.metrics['flow']['transferConnectLast'] + else: + self.metrics['flow']['transferConnectedTime'] = 0 + self.cdir = None + self.proto[self.scheme] = None + + if (not self.o.dry_run) and os.path.isfile(new_inflight_path): + os.remove(new_inflight_path) + return False + return True
+ + + # generalized send... + def send(self, msg, options): + self.o = options + logger.debug( f"{self.scheme}_transport sendTo: {self.o.sendTo}" ) + logger.debug("%s_transport send %s %s" % + (self.scheme, msg['new_dir'], msg['new_file'])) + + if len(self.plugins['send']) > 0: + for plugin in self.plugins['send']: + try: + ok = plugin(msg) + except Exception as ex: + logger.error( f'flowCallback plugin {plugin} crashed: {ex}' ) + logger.debug( "details:", exc_info=True ) + + if not ok: return False + return True + + if self.o.baseDir: + local_path = self.o.variableExpansion(self.o.baseDir, + msg) + '/' + msg['relPath'] + else: + local_path = '/' + msg['relPath'] + + # older versions don't include the contentType, so patch it here. + if features['filetypes']['present'] and \ + ('contentType' not in msg) and (not 'fileOp' in msg): + msg['contentType'] = magic.from_file(local_path,mime=True) + + local_dir = os.path.dirname(local_path).replace('\\', '/') + local_file = os.path.basename(local_path).replace('\\', '/') + new_dir = msg['new_dir'].replace('\\', '/') + new_file = msg['new_file'].replace('\\', '/') + new_inflight_path = None + + try: + curdir = os.getcwd() + except: + curdir = None + + if (curdir != local_dir) and not self.o.dry_run: + try: + os.chdir(local_dir) + except Exception as ex: + logger.error("could not chdir to %s to write: %s" % (local_dir, ex)) + return False + + try: + + if not self.o.dry_run: + if (not (self.scheme in self.proto)) or \ + (self.proto[self.scheme] is None) or not self.proto[self.scheme].check_is_connected(): + logger.debug("%s_transport send connects" % self.scheme) + + if self.metrics['flow']['transferConnected']: + now = nowflt() + self.metrics['flow']['transferConnectTime'] += now - self.metrics['flow']['transferConnectStart'] + self.metrics['flow']['transferConnectStart'] = 0 + self.metrics['flow']['transferConnected'] = False + + self.proto[self.scheme] = sarracenia.transfer.Transfer.factory( self.scheme, options) + + ok = self.proto[self.scheme].connect() + if not ok: return False + self.cdir = None + self.metrics['flow']['transferConnected'] = True + self.metrics['flow']['transferConnectStart'] = time.time() + + elif not (self.scheme in self.proto) or self.proto[self.scheme] is None: + logger.debug("dry_run %s_transport send connects" % self.scheme) + self.proto[self.scheme] = sarracenia.transfer.Transfer.factory( self.scheme, options) + self.cdir = None + self.metrics['flow']['transferConnected'] = True + self.metrics['flow']['transferConnectStart'] = time.time() + + #================================= + # if parts, check that the protocol supports it + #================================= + + if not self.o.dry_run and not hasattr(self.proto[self.scheme], + 'seek') and ('blocks' in msg) and ( + msg['blocks']['method'] == 'inplace'): + logger.error("%s, inplace part file not supported" % + self.scheme) + return False + + #================================= + # if umask, check that the protocol supports it ... + #================================= + + inflight = options.inflight + if not hasattr(self.proto[self.scheme], + 'umask') and options.inflight == 'umask': + logger.warning("%s, umask not supported" % self.scheme) + inflight = None + + #================================= + # if renaming used, check that the protocol supports it ... + #================================= + + if not hasattr(self.proto[self.scheme], + 'rename') and options.inflight.startswith('.'): + logger.warning("%s, rename not supported" % self.scheme) + inflight = None + + #================================= + # remote set to new_dir + #================================= + + cwd = None + if hasattr(self.proto[self.scheme], 'getcwd'): + if not self.o.dry_run: + try: + cwd = self.proto[self.scheme].getcwd() + except Exception as ex: + logger.error( f"could not getcwd: {ex}" ) + return False + + if cwd != new_dir: + logger.debug("%s_transport send cd to %s" % + (self.scheme, new_dir)) + if not self.o.dry_run: + try: + self.proto[self.scheme].cd_forced(775, new_dir) + except Exception as ex: + logger.error( f"could not chdir to {new_dir}: {ex}" ) + return False + + #================================= + # delete event + #================================= + + if 'fileOp' in msg: + if 'remove' in msg['fileOp'] : + if hasattr(self.proto[self.scheme], 'delete'): + logger.debug("message is to remove %s" % new_file) + if not self.o.dry_run: + if 'directory' in msg['fileOp']: + try: + self.proto[self.scheme].rmdir(new_file) + except Exception as ex: + logger.error( f"could not rmdir {new_file}: {ex}" ) + return False + else: + try: + self.proto[self.scheme].delete(new_file) + except Exception as ex: + logger.error( f"could not delete {new_file}: {ex}" ) + return False + + msg.setReport(201, f'file or directory removed') + self.metrics['flow']['transferTxFiles'] += 1 + self.metrics['flow']['transferTxLast'] = msg['report']['timeCompleted'] + return True + logger.error("%s, delete not supported" % self.scheme) + return False + + if 'rename' in msg['fileOp'] : + if hasattr(self.proto[self.scheme], 'delete'): + logger.debug( f"message is to rename {msg['fileOp']['rename']} to {new_file}" ) + if not self.o.dry_run: + try: + self.proto[self.scheme].rename(msg['fileOp']['rename'], new_file) + except Exception as ex: + logger.error( f"could not rename {new_file}: {ex}" ) + return False + + msg.setReport(201, f'file renamed') + self.metrics['flow']['transferTxFiles'] += 1 + self.metrics['flow']['transferTxLast'] = msg['report']['timeCompleted'] + return True + logger.error("%s, delete not supported" % self.scheme) + return False + + if 'directory' in msg['fileOp'] : + if 'contentType' not in msg: + msg['contentType'] = 'text/directory' + if hasattr(self.proto[self.scheme], 'mkdir'): + logger.debug( f"message is to mkdir {new_file}") + if not self.o.dry_run: + try: + self.proto[self.scheme].mkdir(new_file) + except Exception as ex: + logger.error( f"could not mkdir {new_file}: {ex}" ) + return False + msg.setReport(201, f'directory created') + self.metrics['flow']['transferTxFiles'] += 1 + self.metrics['flow']['transferTxLast'] = msg['report']['timeCompleted'] + return True + logger.error("%s, mkdir not supported" % self.scheme) + return False + + + #================================= + # link event + #================================= + + if 'hlink' in msg['fileOp']: + if 'contentType' not in msg: + msg['contentType'] = 'text/link' + if hasattr(self.proto[self.scheme], 'link'): + logger.debug("message is to link %s to: %s" % (new_file, msg['fileOp']['hlink'])) + if not self.o.dry_run: + try: + self.proto[self.scheme].link(msg['fileOp']['hlink'], new_file) + except Exception as ex: + logger.error( f"could not link {new_file}: {ex}" ) + return False + return True + logger.error("%s, hardlinks not supported" % self.scheme) + return False + elif 'link' in msg['fileOp']: + if 'contentType' not in msg: + msg['contentType'] = 'text/link' + if hasattr(self.proto[self.scheme], 'symlink'): + logger.debug("message is to link %s to: %s" % (new_file, msg['fileOp']['link'])) + if not self.o.dry_run: + try: + self.proto[self.scheme].symlink(msg['fileOp']['link'], new_file) + except Exception as ex: + logger.error( f"could not symlink {new_file}: {ex}" ) + return False + msg.setReport(201, f'file linked') + self.metrics['flow']['transferTxFiles'] += 1 + self.metrics['flow']['transferTxLast'] = msg['report']['timeCompleted'] + return True + logger.error("%s, symlink not supported" % self.scheme) + return False + + #================================= + # send event + #================================= + + # the file does not exist... warn, sleep and return false for the next attempt + if not os.path.exists(local_file): + logger.warning( + "product collision or base_dir not set, file %s does not exist" + % local_file) + time.sleep(0.01) + return False + + offset = 0 + if ('blocks' in msg) and (msg['blocks']['method'] == 'inplace'): + offset = msg['offset'] + + new_offset = msg['local_offset'] + + if 'size' in msg: + block_length = msg['size'] + str_range = '' + if ('blocks' in msg) and ( + msg['blocks']['method'] == 'inplace'): + block_length = msg['blocks']['size'] + str_range = 'bytes=%d-%d' % (new_offset, new_offset + + block_length - 1) + + str_range = '' + if ('blocks' in msg) and (msg['blocks']['method'] == 'inplace'): + str_range = 'bytes=%d-%d' % (offset, offset + msg['size'] - 1) + + #upload file + + logger.debug( "hasattr=%s, thresh=%d, len=%d, remote_off=%d, local_off=%d " % \ + ( hasattr( self.proto[self.scheme], 'putAccelerated'), \ + self.o.accelThreshold, block_length, new_offset, msg['local_offset'] ) ) + + accelerated = hasattr( self.proto[self.scheme], 'putAccelerated') and \ + (self.o.accelThreshold > 0 ) and (block_length > self.o.accelThreshold) and \ + (new_offset == 0) and ( msg['local_offset'] == 0) + + if inflight == None or (('blocks' in msg) and + (msg['blocks']['method'] != 'inplace')): + try: + if not self.o.dry_run: + if accelerated: + len_written = self.proto[self.scheme].putAccelerated( msg, local_file, new_file) + else: + len_written = self.proto[self.scheme].put( msg, local_file, new_file) + except Exception as ex: + logger.error( f"could not send inflight=None {new_file}: {ex}" ) + return False + + elif (('blocks' in msg) + and (msg['blocks']['method'] == 'inplace')): + if not self.o.dry_run: + try: + self.proto[self.scheme].put(msg, local_file, new_file, offset, + new_offset, msg['size']) + except Exception as ex: + logger.error( f"could not send inplace {new_file}: {ex}" ) + return False + + elif inflight == '.': + new_inflight_path = '.' + new_file + if not self.o.dry_run: + try: + if accelerated: + len_written = self.proto[self.scheme].putAccelerated( + msg, local_file, new_inflight_path) + else: + len_written = self.proto[self.scheme].put( + msg, local_file, new_inflight_path) + except Exception as ex: + logger.error( f"could not send inflight={inflight} {new_file}: {ex}" ) + return False + try: + self.proto[self.scheme].rename(new_inflight_path, new_file) + except Exception as ex: + logger.error( f"could not rename inflight={inflight} {new_file}: {ex}" ) + return False + else: + len_written = msg['size'] + + elif inflight[0] == '.': + new_inflight_path = new_file + inflight + if not self.o.dry_run: + try: + if accelerated: + len_written = self.proto[self.scheme].putAccelerated( + msg, local_file, new_inflight_path) + else: + len_written = self.proto[self.scheme].put(msg, local_file, new_inflight_path) + except Exception as ex: + logger.error( f"could not send inflight={inflight} {new_file}: {ex}" ) + return False + try: + self.proto[self.scheme].rename(new_inflight_path, new_file) + except Exception as ex: + logger.error( f"could not rename inflight={inflight} {new_file}: {ex}" ) + return False + elif options.inflight[-1] == '/': + if not self.o.dry_run: + try: + self.proto[self.scheme].cd_forced( + 775, new_dir + '/' + options.inflight) + self.proto[self.scheme].cd_forced(775, new_dir) + except: + pass + new_inflight_path = options.inflight + new_file + if not self.o.dry_run: + try: + if accelerated: + len_written = self.proto[self.scheme].putAccelerated( + msg, local_file, new_inflight_path) + else: + len_written = self.proto[self.scheme].put( + msg, local_file, new_inflight_path) + except Exception as ex: + logger.error( f"could not send inflight={inflight} {new_file}: {ex}" ) + return False + try: + self.proto[self.scheme].rename(new_inflight_path, new_file) + except Exception as ex: + logger.error( f"could not rename inflight={inflight} {new_file}: {ex}" ) + return False + else: + len_written = msg['size'] + elif inflight == 'umask': + if not self.o.dry_run: + self.proto[self.scheme].umask() + try: + if accelerated: + len_written = self.proto[self.scheme].putAccelerated( + msg, local_file, new_file) + else: + len_written = self.proto[self.scheme].put( + msg, local_file, new_file) + except Exception as ex: + logger.error( f"could not send inflight={inflight} {new_file}: {ex}" ) + return False + try: + self.proto[self.scheme].put(msg, local_file, new_file) + except Exception as ex: + logger.error( f"could not rename inflight={inflight} {new_file}: {ex}" ) + return False + else: + len_written = msg['size'] + + msg.setReport(201, 'file sent') + self.metrics['flow']['transferTxBytes'] += len_written + self.metrics['flow']['transferTxFiles'] += 1 + self.metrics['flow']['transferTxLast'] = msg['report']['timeCompleted'] + + # fix permission + + if not self.o.dry_run: + self.set_remote_file_attributes(self.proto[self.scheme], new_file, + msg) + + logger.info('Sent: %s %s into %s/%s %d-%d' % + (local_path, str_range, new_dir, new_file, offset, + offset + msg['size'] - 1)) + + return True + + except Exception as err: + + #removing lock if left over + if new_inflight_path != None and hasattr(self.proto[self.scheme], + 'delete'): + if not self.o.dry_run: + try: + self.proto[self.scheme].delete(new_inflight_path) + except: + pass + + #closing on problem + if not self.o.dry_run: + try: + self.proto[self.scheme].close() + except: + pass + + now = nowflt() + self.metrics['flow']['transferConnectTime'] += now - self.metrics['flow']['transferConnectStart'] + self.metrics['flow']['transferConnectStart']=0 + self.metrics['flow']['transferConnected']=False + self.cdir = None + self.proto[self.scheme] = None + + logger.error("Delivery failed %s" % msg['new_dir'] + '/' + + msg['new_file']) + logger.debug('Exception details: ', exc_info=True) + + return False + + # set_local_file_attributes +
+[docs] + def set_local_file_attributes(self, local_file, msg): + """ + after a file has been written, restore permissions and ownership if necessary. + """ + logger.debug("%s" % local_file) + + # if the file is not partitioned, the the onfly_checksum is for the whole file. + # cache it here, along with the mtime. + + if ('blocks' in msg) and sarracenia.features['reassembly']['present']: + with sarracenia.blockmanifest.BlockManifest(local_file) as y: + y.set( msg['blocks'] ) + + x = sarracenia.filemetadata.FileMetadata(local_file) + # FIXME ... what to do when checksums don't match? + if 'onfly_checksum' in msg: + x.set( 'identity', msg['onfly_checksum'] ) + elif 'identity' in msg: + x.set('identity', msg['identity'] ) + + if self.o.timeCopy and 'mtime' in msg and msg['mtime']: + x.set('mtime', msg['mtime']) + else: + x.set('mtime', sarracenia.timeflt2str(os.path.getmtime(local_file))) + + x.persist() + + mode = 0 + if self.o.permCopy and 'mode' in msg: + try: + mode = int(msg['mode'], base=8) + except: + mode = 0 + if mode > 0: + os.chmod(local_file, mode) + + if mode == 0 and self.o.permDefault != 0: + os.chmod(local_file, self.o.permDefault) + + if self.o.timeCopy and 'mtime' in msg and msg['mtime']: + mtime = sarracenia.timestr2flt(msg['mtime']) + atime = mtime + if 'atime' in msg and msg['atime']: + atime = sarracenia.timestr2flt(msg['atime']) + os.utime(local_file, (atime, mtime))
+ + + # set_remote_file_attributes + def set_remote_file_attributes(self, proto, remote_file, msg): + #logger.debug("sr_transport set_remote_file_attributes %s" % remote_file) + + if hasattr(proto, 'chmod'): + mode = 0 + if self.o.permCopy and 'mode' in msg: + try: + mode = int(msg['mode'], base=8) + except: + mode = 0 + if mode > 0: + try: + proto.chmod(mode, remote_file) + except: + pass + + if mode == 0 and self.o.permDefault != 0: + try: + proto.chmod(self.o.permDefault, remote_file) + except: + pass + + if hasattr(proto, 'chmod'): + if self.o.timeCopy and 'mtime' in msg and msg['mtime']: + mtime = sarracenia.timestr2flt(msg['mtime']) + atime = mtime + if 'atime' in msg and msg['atime']: + atime = sarracenia.timestr2flt(msg['atime']) + try: + proto.utime(remote_file, (atime, mtime)) + except: + pass + + # v2 sr_util sr_transport stuff. end. + + # imported from v2: sr_sender/doit_send + +
+[docs] + def do_send(self): + """ + """ + if not self.o.download: + self.worklist.ok = self.worklist.incoming + self.worklist.incoming = [] + return + + for msg in self.worklist.incoming: + + # weed out non-file transfer operations that are configured to not be done. + if 'fileOp' in msg: + if ('directory' in msg['fileOp']) and ('remove' in msg['fileOp']) and ( 'rmdir' not in self.o.fileEvents ): + self.reject(msg, 202, "skipping rmdir here." ) + continue + + elif ('remove' in msg['fileOp']) and ( 'delete' not in self.o.fileEvents ): + self.reject(msg, 202, "skipping delete here." ) + continue + + if ('directory' in msg['fileOp']) and ( 'mkdir' not in self.o.fileEvents ): + self.reject(msg, 202, "skipping mkdir here." ) + continue + + if ('hlink' in msg['fileOp']) and ( 'link' not in self.o.fileEvents ): + self.reject(msg, 202, "skipping hlink here." ) + continue + + if ('link' in msg['fileOp']) and ( 'link' not in self.o.fileEvents ): + self.reject(msg, 202, "skipping link here." ) + continue + + #================================= + # proceed to send : has to work + #================================= + + # N attempts to send + + i = 1 + while i <= self.o.attempts: + if i != 1: + logger.warning("sending again, attempt %d" % i) + + ok = self.send(msg, self.o) + if ok: + self.worklist.ok.append(msg) + break + + i = i + 1 + if not ok: + self.worklist.failed.append(msg) + self.worklist.incoming = []
+
+ + + +import sarracenia.flow.poll +import sarracenia.flow.post +import sarracenia.flow.report +import sarracenia.flow.sarra +import sarracenia.flow.sender +import sarracenia.flow.shovel +import sarracenia.flow.subscribe +import sarracenia.flow.watch +import sarracenia.flow.winnow +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/poll.html b/_modules/sarracenia/flow/poll.html new file mode 100644 index 000000000..77a804acd --- /dev/null +++ b/_modules/sarracenia/flow/poll.html @@ -0,0 +1,193 @@ + + + + + + sarracenia.flow.poll — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.poll

+#
+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2021
+#
+
+import logging
+import sarracenia
+from sarracenia.flow import Flow
+from sarracenia.featuredetection import features
+import sys
+
+
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+    'acceptUnmatched': True,
+    'blocksize': 1,
+    'bufsize': 1024 * 1024,
+    'chmod': 0o400,
+    'pollUrl': None,
+    'follow_symlinks': False,
+    'force_polling': False,
+    'inflight': None,
+    'identity_method': 'cod,sha512',
+    'part_ext': 'Part',
+    'partflg': '1',
+    'post_baseDir': None,
+    'permCopy': True,
+    'timeCopy': True,
+    'randomize': False,
+    'post_on_start': False,
+    'nodupe_ttl': 7 * 60 * 60,
+    'fileAgeMax': 30 * 24 * 60 * 60,
+}
+
+#  'sumflg': 'cod,md5',
+
+
+
+[docs] +class Poll(Flow): + """ + repeatedly query a remote (non-sarracenia) server to list the files there. + post messages (to post_broker) for every new file discovered there. + + the sarracenia.flowcb.poll class is used to implement the remote querying, + and is highly customizable to that effect. + + if the vip option is set, + * subscribe to the same settings that are being posted to. + * consume all the messages posted, keeping new file duplicate cache updated. + + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + + if hasattr(self.o,'post_exchange') and hasattr(self.o,'exchange'): + px = self.o.post_exchange if type(self.o.post_exchange) != list else self.o.post_exchange[0] + if px != self.o.exchange: + logger.warning( f"post_exchange: {px} is different from exchange: {self.o.exchange}. The settings need for multiple instances to share a poll." ) + else: + logger.info( f"Good! post_exchange: {px} and exchange: {self.o.exchange} match so multiple instances to share a poll." ) + + if not 'scheduled' in ','.join(self.plugins['load']): + self.plugins['load'].append('sarracenia.flowcb.scheduled.poll.Poll') + + if not 'flowcb.poll.Poll' in ','.join(self.plugins['load']): + logger.info( f"adding poll plugin, because missing from: {self.plugins['load']}" ) + self.plugins['load'].append('sarracenia.flowcb.poll.Poll') + + if options.vip: + self.plugins['load'].insert( 0, 'sarracenia.flowcb.gather.message.Message') + + self.plugins['load'].insert( 0, 'sarracenia.flowcb.post.message.Message') + + if self.o.nodupe_ttl < self.o.fileAgeMax: + logger.warning( f"nodupe_ttl < fileAgeMax means some files could age out of the cache and be re-ingested ( see : https://github.com/MetPX/sarracenia/issues/904") + + if not features['ftppoll']['present']: + if hasattr( self.o, 'pollUrl' ) and ( self.o.pollUrl.startswith('ftp') ): + logger.critical( f"attempting to configure an FTP poll pollUrl={self.o.pollUrl}, but missing python modules: {' '.join(features['ftppoll']['modules_needed'])}" )
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/post.html b/_modules/sarracenia/flow/post.html new file mode 100644 index 000000000..c3c1d2d96 --- /dev/null +++ b/_modules/sarracenia/flow/post.html @@ -0,0 +1,147 @@ + + + + + + sarracenia.flow.post — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.post

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+    'acceptUnmatched': True,
+    'blocksize': 1,
+    'bufsize': 1024 * 1024,
+    'follow_symlinks': False,
+    'force_polling': False,
+    'inflight': None,
+    'part_ext': 'Part',
+    'partflg': '1',
+    'post_baseDir': None,
+    'permCopy': True,
+    'timeCopy': True,
+    'randomize': False,
+    'post_on_start': False,
+    'sleep': -1,
+    'nodupe_ttl': 0
+}
+
+#'sumflg': 'sha512',
+
+
+
+[docs] +class Post(Flow): + """ + post messages about local files. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert(0, 'sarracenia.flowcb.gather.file.File') + self.plugins['load'].insert(0, + 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/report.html b/_modules/sarracenia/flow/report.html new file mode 100644 index 000000000..aaa8bfa22 --- /dev/null +++ b/_modules/sarracenia/flow/report.html @@ -0,0 +1,135 @@ + + + + + + sarracenia.flow.report — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.report

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {'acceptUnmatched': True, 'nodupe_ttl': 0}
+
+
+
+[docs] +class Report(Flow): + """ + forward report messages. + + Not really implemented at the moment. It is just a shovel synonym for now. + more logic should be added. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + + if hasattr(self.o, 'post_exchange'): + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/sarra.html b/_modules/sarracenia/flow/sarra.html new file mode 100644 index 000000000..50e10ae5b --- /dev/null +++ b/_modules/sarracenia/flow/sarra.html @@ -0,0 +1,134 @@ + + + + + + sarracenia.flow.sarra — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.sarra

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {'acceptUnmatched': True, 'download': True}
+
+
+
+[docs] +class Sarra(Flow): + """ + * download files from a remote server to the local one + * modify the messages so they refer to the downloaded files. + * re-post them to another exchange for the next other subscribers. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + + if hasattr(self.o, 'post_exchange'): + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/sender.html b/_modules/sarracenia/flow/sender.html new file mode 100644 index 000000000..8cc233249 --- /dev/null +++ b/_modules/sarracenia/flow/sender.html @@ -0,0 +1,144 @@ + + + + + + sarracenia.flow.sender — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.sender

+from sarracenia.flow import Flow
+import logging
+import urllib.parse
+
+logger = logging.getLogger(__name__)
+
+default_options = {'acceptUnmatched': True, 'download': True}
+
+
+
+[docs] +class Sender(Flow): + """ + * subscribe to a stream of messages about local files. + * send the files to a remote server. + * modify the messages to refer to the remote file copies. + * post the messages for subscribers of the remote server. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + + if hasattr(self.o, 'post_exchange'): + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.post.message.Message') + + self.scheme = urllib.parse.urlparse(self.o.sendTo).scheme
+ + + def do(self): + + self.do_send() + + logger.debug('processing %d messages worked!' % len(self.worklist.ok))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/shovel.html b/_modules/sarracenia/flow/shovel.html new file mode 100644 index 000000000..1f6005c24 --- /dev/null +++ b/_modules/sarracenia/flow/shovel.html @@ -0,0 +1,134 @@ + + + + + + sarracenia.flow.shovel — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.shovel

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+        'acceptUnmatched': True, 
+        'nodupe_ttl': 0,
+}
+
+
+
+[docs] +class Shovel(Flow): + """ + * subscribe to some messages. + * post them somewhere else. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + self.plugins['load'].insert(0, + 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/subscribe.html b/_modules/sarracenia/flow/subscribe.html new file mode 100644 index 000000000..783c8ed01 --- /dev/null +++ b/_modules/sarracenia/flow/subscribe.html @@ -0,0 +1,133 @@ + + + + + + sarracenia.flow.subscribe — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.subscribe

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {'acceptUnmatched': True, 'download': True, 'mirror': False}
+
+
+
+[docs] +class Subscribe(Flow): + """ + * subscribe to messages about files. + * download the corresponding files. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + + if hasattr(self.o, 'post_exchange'): + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/watch.html b/_modules/sarracenia/flow/watch.html new file mode 100644 index 000000000..c1fb4a496 --- /dev/null +++ b/_modules/sarracenia/flow/watch.html @@ -0,0 +1,148 @@ + + + + + + sarracenia.flow.watch — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.watch

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+    'acceptUnmatched': True,
+    'blocksize': 1,
+    'bufsize': 1024 * 1024,
+    'follow_symlinks': False,
+    'force_polling': False,
+    'inflight': None,
+    'part_ext': 'Part',
+    'partflg': '1',
+    'post_baseDir': None,
+    'permCopy': True,
+    'timeCopy': True,
+    'randomize': False,
+    'sumflg': 'sha512',
+    'post_on_start': False,
+    'sleep': 5,
+    'nodupe_ttl': 0
+}
+
+
+
+[docs] +class Watch(Flow): + """ + * create messages for files that appear in a directory. + + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + logger.info('watching!') + self.plugins['load'].insert(0, 'sarracenia.flowcb.gather.file.File') + self.plugins['load'].insert(0, + 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flow/winnow.html b/_modules/sarracenia/flow/winnow.html new file mode 100644 index 000000000..801b872bb --- /dev/null +++ b/_modules/sarracenia/flow/winnow.html @@ -0,0 +1,135 @@ + + + + + + sarracenia.flow.winnow — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flow.winnow

+from sarracenia.flow import Flow
+import logging
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+        'acceptUnmatched': True, 
+        'nodupe_ttl': 300,
+}
+
+
+
+[docs] +class Winnow(Flow): + """ + * subscribe to a stream of messages. + * suppress duplicates, + * post the thinned out stream somewhere else. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options) + self.plugins['load'].insert( + 0, 'sarracenia.flowcb.gather.message.Message') + self.plugins['load'].insert(0, + 'sarracenia.flowcb.post.message.Message')
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb.html b/_modules/sarracenia/flowcb.html new file mode 100644 index 000000000..b7afb84de --- /dev/null +++ b/_modules/sarracenia/flowcb.html @@ -0,0 +1,390 @@ + + + + + + sarracenia.flowcb — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb

+
+#
+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Shared Services Canada, 2020
+#
+
+import copy
+import importlib
+import logging
+import sys
+
+entry_points = [
+
+    'ack', 'after_accept', 'after_post', 'after_work', 'destfn', 'do_poll', 
+    'download', 'gather', 'metricsReport', 'on_cleanup', 'on_declare', 'on_features',
+    'on_housekeeping', 'on_sanity', 'on_start', 'on_stop', 
+    'please_stop', 'poll', 'post', 'report', 'send', 
+
+]
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class FlowCB: + """ + Flow Callback is the main class for implementing plugin customization to flows. + + sample activation in a configuration file: + + flowCallback sarracenia.flowcb.name.Name + + will instantiate an object of that type whose appropriately name methods + will be called at the right time. + + __init__ accepts options as an argument. + + options is a sarracenia.config.Config object, used to override default behaviour + + a setting is declared in a configuration file like so:: + + set sarracenia.flowcb.filter.log.Log.level debug + + (the prefix for the setting matches the type hierarchy in flowCallback) + the plugin should get the setting:: + + options.level = 'debug' + + + worklist given to on_plugins... + + * worklist.incoming --> new messages to continue processing + * worklist.ok --> successfully processed + * worklist.rejected --> messages to not be further processed. + * worklist.failed --> messages for which processing failed. Failed messages will be retried. + * worklist.directories_ok --> list of directories created during processing. + + Initially, all messages are placed in incoming. + if a plugin entry_point decides: + + - a message is not relevant, it is moved to the rejected worklist. + - all processing has been done, it moves it to the ok worklist + - an operation failed and it should be retried later, append it to the failed + worklist + + Do not remove any message from all lists, only move messages between them. + it is necessary to put rejected messages in the appropriate worklist + so they can be acknowledged as received. Messages can only removed after ack. + + + def __init__(self,options) -> None:: + + Task: initialization of the flowCallback at instantiation time. + + usually contains: + + self.o = options + + def ack(self,messagelist) -> None:: + + Task: acknowledge messages from a gather source. + + def gather(self, messageCountMax) -> (gather_more, messages) :: + + Task: gather messages from a source... return a tuple: + + * gather_more ... bool whether to continue gathering + * messages ... list of messages + + or just return a list of messages. + + In a poll, gather is always called, regardless of vip posession. + + In all other components, gather is only called when in posession + of the vip. + + return (True, list) + OR + return list + + def after_accept(self,worklist) -> None:: + + Task: just after messages go through accept/reject masks, + operate on worklist.incoming to help decide which messages to process further. + and move messages to worklist.rejected to prevent further processing. + do not delete any messages, only move between worklists. + + def after_work(self,worklist) -> None:: + + Task: operate on worklist.ok (files which have arrived.) + + All messages on the worklist.ok list have been acknowledged, so to suppress posting + of them, or futher processing, the messages must be removed from worklist.ok. + + worklist.failed processing should occur in here as it will be zeroed out after this step. + The flowcb/retry.py plugin, for example, processes failed messages. + + def destfn(self,msg) -> str:: + + Task: look at the fields in the message, and perhaps settings and + return a new file name for the target of the send or download. + + kind of a last resort function, exists mostly for sundew compatibility. + can be used for selective renaming using accept clauses. + + def download(self,msg) -> bool:: + + Task: looking at msg['new_dir'], msg['new_file'], msg['new_inflight_file'] + and the self.o options perform a download of a single file. + return True on a successful transfer, False otherwise. + + if self.o.dry_run is set, simulate the output of a download without + performing it. + + This replaces built-in download functionality, providing an override. + for individual file transfers. ideally you set checksums as you download. + + def metricsReport(self) -> dict: + + Return a dictionary of metrics. Example: number of messages remaining in retry queues. + + def on_cleanup(self) -> None:: + allow plugins to perform additional work after broker resources are eliminated. + local state files are still present when this runs. + + def on_declare(self) -> None:: + local state files are still already present when this runs. + allow plugins to perform additional work besides broker resource setup. + + def on_housekeeping(self) -> None:: + + do periodic processing. + + def on_start(self) -> None:: + + After the connection is established with the broker and things are instantiated, but + before any message transfer occurs. + + def on_stop(self) -> None:: + + what it says on the tin... clean up processing when stopping. + + def poll(self) -> list:: + + Task: gather messages from a destination... return a list of messages. + works like a gather, but... + + When specified, poll replaces the built-in poll of the poll component. + it runs only when the machine running the poll has the vip. + in components other than poll, poll is never called. + return [] + + def post(self,worklist) -> None:: + + Task: operate on worklist.ok, and worklist.failed. modifies them appropriately. + message acknowledgement has already occurred before they are called. + + to indicate failure to process a message, append to worklist.failed. + worklist.failed processing should occur in here as it will be zeroed out after this step. + + def send(self,msg) -> bool:: + + Task: looking at msg['new_dir'], msg['new_file'], and the self.o options perform a transfer + of a single file. + return True on a successful transfer, False otherwise. + + if self.o.dry_run is set, simulate the output of a send without + performing it. + + This replaces built-in send functionality for individual files. + + def please_stop(self): + Pre-warn a flowcb that a stop has been requested, allowing processing to wrap up + before the full stop happens. + + """ +
+[docs] + def __init__(self, options, class_logger=None): + self.o = options + self.stop_requested = False + + if hasattr(self.o,'logFormat'): + logging.basicConfig(format=self.o.logFormat, + level=getattr(logging, self.o.logLevel.upper())) + + if hasattr(self.o,'logLevel') and class_logger: + class_logger.setLevel(getattr(logging, self.o.logLevel.upper()))
+ + + +
+[docs] + def please_stop(self): + """ + flow callbacks should not time.sleep for long periods, but only nap and check + between naps if a stop has been requested. + """ + self.stop_requested = True
+
+ + +
+[docs] +def load_library(factory_path, options): + """ + Loading the entry points for a python module. It searches + the normal python module path using the importlib module. + + the factory_path is a combined file specification with a dot separator + with a special last entry being the name of the class within the file. + + factory_path a.b.c.C + + means import the module named a.b.c and instantiate an object of type + C. In that class-C object, look for the known callback entry points. + + or C might be guessed by the last class in the path not following + python convention by not starting with a capital letter, in which case, + it will just guess. + + re + note that the ~/.config/sr3/plugins will also be in the python library + path, so modules placed there will be found, in addition to those in the + package itself in the *sarracenia/flowcb* directory + + callback foo -> foo.Foo + sarracenia.flowcb.foo.Foo + + callback foo.bar -> foo.bar.Bar + sarracenia.flowcb.foo.bar.Bar + foo.bar + sarracenia.flowcb.foo.bar + """ + + if not '.' in factory_path: + packagename = factory_path + classname =factory_path.capitalize() + else: + if factory_path.split('.')[-1][0].islower(): + packagename = factory_path + classname = factory_path.split('.')[-1].capitalize() + else: + packagename, classname = factory_path.rsplit('.', 1) + + try: + module = importlib.import_module(packagename) + class_ = getattr(module, classname) + except ModuleNotFoundError: + module = importlib.import_module('sarracenia.flowcb.' + packagename) + class_ = getattr(module, classname) + + if hasattr(options, 'settings'): + opt = copy.deepcopy(options) + # strip off the class prefix. + if factory_path in options.settings: + for s in options.settings[factory_path]: + setattr(opt, s, options.settings[factory_path][s]) + else: + opt = options + + plugin = class_(opt) + return plugin
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/delete.html b/_modules/sarracenia/flowcb/accept/delete.html new file mode 100644 index 000000000..ce89c4ff9 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/delete.html @@ -0,0 +1,146 @@ + + + + + + sarracenia.flowcb.accept.delete — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.delete

+"""
+Plugin delete.py:
+    the default after_accept handler for log.
+    prints a simple notice. # FIXME is this doc accurate? seems it is also modifying a message.
+
+Usage:
+    flowcb sarracenia.flowcb.accept.delete.Delete
+"""
+
+import logging
+import os
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+[docs] +class Delete(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger)
+ + + def after_accept(self, worklist): + new_incoming = [] + for message in worklist.incoming: + f = "%s/%s" % (message['new_dir'], message['new_file']) + try: + os.unlink(f) + os.unlink(f.replace('/cfr/', '/cfile/')) + logger.info("deleted: %s and the cfile version." % f) + new_incoming.append(message) + except OSError as err: + logger.error("could not unlink {}: {}".format(f, err)) + logger.debug("Exception details:", exc_info=True) + self.o.consumer.sleep_now = self.o.consumer.sleep_min + self.o.consumer.msg_to_retry() + worklist.rejected.append(message) + + worklist.incoming = new_incoming
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/downloadbaseurl.html b/_modules/sarracenia/flowcb/accept/downloadbaseurl.html new file mode 100644 index 000000000..bd613b931 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/downloadbaseurl.html @@ -0,0 +1,146 @@ + + + + + + sarracenia.flowcb.accept.downloadbaseurl — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.downloadbaseurl

+"""
+Plugin downloadbaseurl.py:
+    Downloads files sourced from the baseUrl of the poster, and saves them in the
+    directory specified in the config. Created to use with the poll_nexrad.py
+    plugin to download files uploaded in the NEXRAD US Weather Radar public dataset.
+    Compatible with Python 3.5+.
+
+Example:
+    A sample do_download option for subscribe.
+	Downloads the file located at message['baseUrl'] and saves it
+
+Usage:
+	flowcb sarracenia.flowcb.accept.downloadbaseurl.DownloadBaseUrl
+"""
+
+import logging
+import os
+import urllib.request
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+[docs] +class DownloadBaseUrl(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + # if mirror is set to True, comment these two lines out + #TODO: this self.o.new_dir could be instead message['new_dir'] I think.. to see.. + keypath, key = os.path.split(self.o.new_dir + message['new_file']) + if not os.path.exists(keypath): os.makedirs(keypath) + + with open(keypath + '/' + key, 'wb') as f: + with urllib.request.urlopen(message['baseUrl']) as k: + f.write(k.read())
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/hourtree.html b/_modules/sarracenia/flowcb/accept/hourtree.html new file mode 100644 index 000000000..62292ba4b --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/hourtree.html @@ -0,0 +1,141 @@ + + + + + + sarracenia.flowcb.accept.hourtree — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.hourtree

+"""
+Plugin hourtree.py:
+    When receiving a file, insert an hourly directory into the local delivery path hierarchy.
+
+Example:
+    input A/B/c.gif  --> output A/B/<hour>/c.gif
+
+Usage:
+    flowcb sarracenia.flowcb.accept.hourtree.HourTree
+
+"""
+
+import logging
+import sys, os, os.path, time, stat
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+[docs] +class HourTree(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + datestr = time.strftime('%H', time.localtime()) # pick the hour. + message['new_dir'] += '/' + datestr # append the hourly element to the directory tree. + + # insert the additional hourly directory into the path of the file to be written. + new_fname = message['new_file'].split('/') + message['new_file'] = '/'.join(new_fname[0:-1]) + '/' + datestr + '/' + new_fname[-1]
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/httptohttps.html b/_modules/sarracenia/flowcb/accept/httptohttps.html new file mode 100644 index 000000000..9a3b97c2f --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/httptohttps.html @@ -0,0 +1,138 @@ + + + + + + sarracenia.flowcb.accept.httptohttps — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.httptohttps

+"""
+Plugin httptohttps.py:
+    This plugin simply turns messages with baseUrl http://...  into   https://...
+
+    The process would need to be restarted. From now on, all http messages that would be
+    consumed, would be turned into an https message. The remaining of the process will
+    treat the message as if posted that way. That plugin is an easy way to turn transaction
+    between dd.weather.gc.ca and the user into secured https transfers.
+
+Usage:
+    flowcb sarracenia.flowcb.accept.httptohttps.HttpToHttps
+"""
+
+import logging
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class HttpToHttps(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + if not 'http:' in message['baseUrl']: + continue + message['baseUrl'] = message['baseUrl'].replace('http:', 'https:')
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/longflow.html b/_modules/sarracenia/flowcb/accept/longflow.html new file mode 100644 index 000000000..9085daeef --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/longflow.html @@ -0,0 +1,138 @@ + + + + + + sarracenia.flowcb.accept.longflow — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.longflow

+"""
+Plugin longflow.py:
+    This plugin is strictly for self-test purposes.
+    Creates a header 'toolong' that is longer than 255 characters, and so gets truncated.
+    Each header in a message that is too long should generate a warning message in the sarra log.
+    flow_test checks for the 'truncated' error message.
+    Put some utf characters in there to make it interesting... (truncation complex.)
+
+Usage:
+    flowcb sarracenia.flowcb.accept.longflow.LongFlow
+"""
+
+import logging
+import os, stat, time
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class LongFlow(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + logger.info('setting toolong message key') + message['toolong'] = '1234567890ßñç' * 26
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/posthourtree.html b/_modules/sarracenia/flowcb/accept/posthourtree.html new file mode 100644 index 000000000..c8af2e970 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/posthourtree.html @@ -0,0 +1,139 @@ + + + + + + sarracenia.flowcb.accept.posthourtree — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.posthourtree

+"""
+Plugin posthourtree.py:
+    When posting a file, insert an hourly directory into the delivery path hierarchy.
+
+Example:
+    input A/B/c.gif  --> output A/B/<hour>/c.gif
+
+Usage:
+    callback accept.posthourtree
+
+"""
+import logging
+import sys, os, os.path, time, stat
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Posthourtree(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + datestr = time.strftime('%H', time.gmtime()) # pick the hour + # insert the hour into the rename header of the message to be posted. + message['new_dir'] += '/' + datestr + logger.info( f"post_hour_tree: new_dir: {message['new_dir']}" )
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/postoverride.html b/_modules/sarracenia/flowcb/accept/postoverride.html new file mode 100644 index 000000000..6998a4769 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/postoverride.html @@ -0,0 +1,162 @@ + + + + + + sarracenia.flowcb.accept.postoverride — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.postoverride

+"""
+Plugin postoverride.py:
+    Override message header for products that are posted. This can be useful or necessary 
+    when re-distributing beyond the original intended destinations.
+
+Example:
+    for example company A delivers to their own DMZ server. ACME is a client of them,
+    and so subscribes to the ADMZ server, but the to_cluster=ADMZ, when ACME downloads, they
+    need to override the destination to specify the distribution within ACME.
+    * postOverride to_clusters ACME
+    * postOverrideDel from_cluster
+
+Usage: 
+    flowcb sarracenia.flowcb.accept.postoverride.PostOverride
+    postOverride x y
+    postOverrideDel z
+
+"""
+
+import logging
+import copy
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PostOverride(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + self.o.add_option('postOverride', 'list') + self.o.add_option('postOverrideDel', 'list') + + if self.o.postOverride != None: + logger.info('postOverride settings: %s' % self.o.postOverride) + if self.o.postOverrideDel != None: + logger.info('postOverrideDel settings: %s' % self.o.postOverrideDel)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + + if self.o.postOverride != None: + for o in self.o.postOverride: + (osetting, ovalue) = o.split() + logger.debug('postOverride applying key:%s value:%s' % (osetting, ovalue)) + message[osetting] = ovalue + + if self.o.postOverrideDel != None: + for od in self.o.postOverrideDel: + if od in message: + logger.debug('postOverride deleting key:%s ' % od) + del message[od]
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/printlag.html b/_modules/sarracenia/flowcb/accept/printlag.html new file mode 100644 index 000000000..308952213 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/printlag.html @@ -0,0 +1,150 @@ + + + + + + sarracenia.flowcb.accept.printlag — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.printlag

+"""
+Plugin printlag.py:
+     This is an after_accept plugin.
+     print a message indicating how old messages received are.
+     this should be used as an on_part script. For each part received it will print a line
+     in the local log that looks like this:
+
+     2015-12-23 22:54:30,328 [INFO] posted: 20151224035429.115 (lag: 1.21364 seconds ago) to deliver: /home/peter/test/dd/bulletins/alphanumeric/20151224/SA/EGGY/03/SAUK32_EGGY_240350__EGAA_64042,
+
+     the number printed after "lag:" the time between the moment the message was originally posted on the server,
+     and the time the script was called, which is very near the end of writing the file to local disk.
+
+     This can be used to gauge whether the number of instances or internet link are sufficient
+     to transfer the data selected.  if the lag keeps increasing, then something must be done.
+
+Usage:
+    flowcb sarracenia.flowcb.accept.printlag.PrintLag
+
+"""
+import calendar
+import logging
+import os
+import stat
+import time
+from sarracenia import timestr2flt, nowflt
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PrintLag(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + then = timestr2flt(message['pubTime']) + now = nowflt() + + logger.info("print_lag, posted: %s, lag: %.2f sec. to deliver: %s, " % (message['pubTime'], (now - then), message['new_file']))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/rename4jicc.html b/_modules/sarracenia/flowcb/accept/rename4jicc.html new file mode 100644 index 000000000..e9fc692cf --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/rename4jicc.html @@ -0,0 +1,152 @@ + + + + + + sarracenia.flowcb.accept.rename4jicc — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.rename4jicc

+import sys, os, os.path, time, stat
+"""
+Plugin rename4jicc.py:
+    This is a very specific script unlikely to be useful to other except as a code example.
+    
+Example:    
+    This renamer takes the complete px name : ccstn.dat
+    and restructures it into an incoming pds  name : jicc.yyyymmddhhmm.ccstn.dat
+                        2016-03-02 21:27:22,015 [INFO] 201 Downloaded : v02.report.20160302.MSC-CMC.METADATA.ccstn.dat:pull-ccstn:NCP:JICC:5:Codecon:20160302212706 20160302212715.58 http://ddi2.edm.ec.gc.ca/ 20160302/MSC-CMC/METADATA/ccstn.dat:pull-ccstn:NCP:JICC:5:Codecon:20160302212706 201 dms-op3-host3.edm.ec.gc.ca dms 6.955598 parts=1,55024600,1,0,0 sum=d,3da695f047174462ebe5d0352f4f8295 from_cluster=DDI.CMC source=metpx to_clusters=DDI.CMC,DDI.EDM rename=/apps/dms/dms-decoder-jicc/data/import/jicc.201603022127.ccstn.dat:pull-ccstn:NCP:JICC:5:Codecon:20160302212706 message=Downloaded 
+    
+    This renamer takes the complete px name :       http://ddi2.edm.ec.gc.ca/ 20160302/MSC-CMC/METADATA/ccstn.dat:pull-ccstn:NCP:JICC:5:Codecon:20160302212706
+    and restructures it into an incoming pds  name : /apps/dms/dms-decoder-jicc/data/import/jicc.201603022127.ccstn.dat:pull-ccstn:NCP:JICC:5:Codecon:20160302212706   (jicc.yyyymmddhhmm.ccstn.dat )
+
+Usage:
+    flowcb sarracenia.flowcb.accept.rename4jicc.Rename4Jicc
+"""
+
+import logging
+import time
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+[docs] +class Rename4Jicc(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + if not 'ccstn.dat' in message['new_file']: + continue + + # build new name + local_file = message['new_file'] + datestr = time.strftime('%Y%m%d%H%M', time.localtime()) + local_file = local_file.replace('ccstn.dat', 'jicc.' + datestr + '.ccstn.dat') + + # set in message (and headers for logging) + message['new_file'] = local_file
+ + + # dont use this... new_file is where the file will be downloaded... so need to keep a rename in headers + #message['headers']['rename'] = local_file +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/renamedmf.html b/_modules/sarracenia/flowcb/accept/renamedmf.html new file mode 100644 index 000000000..b7c0c568c --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/renamedmf.html @@ -0,0 +1,141 @@ + + + + + + sarracenia.flowcb.accept.renamedmf — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.renamedmf

+"""
+Plugin renamedmf.py:
+    This is an  example of the usage of after_accept script
+    to rename the product when it is downloaded
+    It adds a ':datetime' to the name of the product
+    unlikely to be useful to others, except as example.
+
+Example:
+    takes px name     : CACN00_CWAO_081900__WZS_14623:pxatx:CWAO:CA:5:Direct:20081008190602
+    add datetimestamp : CACN00_CWAO_081900__WZS_14623:pxatx:CWAO:CA:5:Direct:20081008190602:20081008190612
+
+Usage:
+    flowcb sarracenia.flowcb.accept.renamedmf.RenameDMF
+
+"""
+import logging
+import time
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+[docs] +class RenameDMF(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + datestr = time.strftime(':%Y%m%d%H%M%S', time.localtime()) + message['new_file'] += datestr + message['rename'] += datestr
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/renamewhatfn.html b/_modules/sarracenia/flowcb/accept/renamewhatfn.html new file mode 100644 index 000000000..a7e093fbf --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/renamewhatfn.html @@ -0,0 +1,151 @@ + + + + + + sarracenia.flowcb.accept.renamewhatfn — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.renamewhatfn

+"""
+Plugin renamewhatfn.py:
+    This plugin is no longer needed.  Sundew compoatibility was added to Sarracenia, 
+    so now can get the same effect by using the *filename* option which works like it
+    does in Sundew:
+
+    filename WHATFN
+
+    what it was used for:
+    This renamer strips everything from the first colon in the file name to the end.
+    This does the same thing as a 'WHATFN' config on a sundew sender.
+
+Example:
+    takes px name     : /apps/dms/dms-metadata-updater/data/international_surface/import/mdicp4d:pull-international-metadata:CMC:DICTIONARY:4:ASCII:20160223124648
+    rename for        : /apps/dms/dms-metadata-updater/data/international_surface/import/mdicp4d
+
+Usage:
+    flowcb sarracenia.flowcb.accept.renamewhatfn.RenameWhatFn
+
+"""
+
+import logging
+import sys, os, os.path, time, stat
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class RenameWhatFn(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + parts = message['new_file'].split(':') + + # join mets les ':' entre les parts... donc ajout de ':' au debut + extra = ':' + ':'.join(parts[1:]) + message['new_file'] = message['new_file'].replace(extra, '') + message['rename'] = message['rename'].replace(extra, '')
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/save.html b/_modules/sarracenia/flowcb/accept/save.html new file mode 100644 index 000000000..75ef358a0 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/save.html @@ -0,0 +1,156 @@ + + + + + + sarracenia.flowcb.accept.save — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.save

+"""
+Plugin save.py:
+    Converts a consuming component into one that writes the queue into a file. 
+    If there is some sort of problem with a component, then add callback save and restart.
+
+    The messages will accumulate in a save file in ~/.cache/<component>/<config>/ ??<instance>.save
+    When the situation is returned to normal (you want the component to process the data as normal):
+    * remove the callback save
+    * note the queue this configuration is using to read (should be in the log on startup.)
+    * run an sr_shovel with -restore_to_queue and the queue name.
+
+Options:
+    save.py takes 'msgSaveFile' as an argument.
+    "_9999.sav" will be added to msgSaveFile, where the 9999 represents the instance number.
+    As instances run in parallel, rather than sychronizing access, just writes to one file per instance.
+
+Usage:
+    callback accept.save
+    msgSaveFile x
+"""
+
+import jsonpickle
+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger('__name__')
+
+
+
+[docs] +class Save(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options, logger) + + self.o.add_option('msgSaveFile', 'str') + if not self.o.msgSaveFile: + logger.error("msg_save: setting msgSaveFile setting is mandatory") + return + + self.msgSaveFile = open(self.o.msgSaveFile, "a") + logger.debug("msg_save initialized")
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + self.msgSaveFile.write(jsonpickle.encode(message) + '\n') + self.msgSaveFile.flush() + logger.info("msg_save saving msg with topic:%s (aborting further processing)" % message['topic'])
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/speedo.html b/_modules/sarracenia/flowcb/accept/speedo.html new file mode 100644 index 000000000..d30d49db9 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/speedo.html @@ -0,0 +1,187 @@ + + + + + + sarracenia.flowcb.accept.speedo — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.speedo

+"""
+Plugin speedo.py:
+    Gives a *speedometer* reading on the messages going through an exchange.
+    as this is an after_accept
+    Accumulate the number of messages and the bytes they represent over a period of time.
+
+Options:
+    msgSpeedoInterval -> how often the speedometer is updated. (default: 5)
+    msg_speedo_maxlag  -> if the message flow indicates that messages are 'late', emit warnings. (default 60)
+
+Usage: 
+    callback accept.speedo
+    msgSpeedoInterval x
+    msg_speedo_maxlag y
+"""
+
+
+from sarracenia import timestr2flt, nowflt, naturalSize, naturalTime
+import logging
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Speedo(FlowCB): +
+[docs] + def __init__(self, options): + """ + set defaults for options. can be overridden in config file. + """ + super().__init__(options, logger) + + self.o.add_option('msg_speedo_maxlag', 'count', 60) + #if hasattr(self.o, 'msg_speedo_maxlag'): + # if type(self.o.msg_speedo_maxlag) is list: + # self.o.msg_speedo_maxlag = int(self.o.msg_speedo_maxlag[0]) + #else: + # self.o.msg_speedo_maxlag = 60 + logger.debug("speedo init: 2 ") + + self.o.add_option('msgSpeedoInterval', 'count', 5) + #if hasattr(self.o, 'msgSpeedoInterval'): + # if type(self.o.msgSpeedoInterval) is list: + # self.o.msgSpeedoInterval = int(self.o.msgSpeedoInterval[0]) + #else: + # self.o.msgSpeedoInterval = 5 + + now = nowflt() + self.msg_speedo_last = now + self.msg_speedo_msgcount = 0 + self.msg_speedo_bytecount = 0
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + msgtime = timestr2flt(message['pubTime']) + now = nowflt() + self.msg_speedo_msgcount = self.msg_speedo_msgcount + 1 + + (method, psize, ptot, prem, pno) = message['partstr'].split(',') + + self.msg_speedo_bytecount += int(psize) + + #not time to report yet. + if self.o.msgSpeedoInterval > now - self.msg_speedo_last: + continue + + lag = now - msgtime + msgpersec = self.msg_speedo_msgcount / (now - self.msg_speedo_last) + bytespersec = self.msg_speedo_bytecount / (now - self.msg_speedo_last) + logger.info("speedo: %3d messages received: %5.4f msg/s, %4.2f bytes/s, lag: %4.0f s" % (self.msg_speedo_msgcount, msgpersec, bytespersec, lag)) + + # If lag is higher than max allowed, emmit a warning + if lag > self.o.msg_speedo_maxlag: + logger.warning("speedo: Excessive lag! Messages posted %4.0f s ago" % lag) + + self.msg_speedo_last = now + self.msg_speedo_msgcount = 0 + self.msg_speedo_bytecount = 0
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/sundewpxroute.html b/_modules/sarracenia/flowcb/accept/sundewpxroute.html new file mode 100644 index 000000000..7f6723227 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/sundewpxroute.html @@ -0,0 +1,205 @@ + + + + + + sarracenia.flowcb.accept.sundewpxroute — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.sundewpxroute

+"""
+Plugin sundewpxroute.py:
+    Implement message filtering based on a routing table from MetPX-Sundew.
+    Make it easier to feed clients exactly the same products with sarracenia,
+    that they are used to with sundew.
+
+Usage:
+    the pxrouting option must be set in the configuration before the plugin
+    is configured, like so:
+    * pxRouting /local/home/peter/src/pdspx/routing/etc/pxRouting.conf
+    * pxClient  navcan-amis
+    flowcb sarracenia.flowcb.accept.sundewpxroute.SundewPxRoute
+"""
+
+import logging
+import os
+
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class SundewPxRoute(FlowCB): +
+[docs] + def __init__(self, options): + """ + + For World Meteorological Organization message oriented routing. + Read the configured metpx-sundew routing table, and figure out which + Abbreviated Header Lines (AHL's) are configured to be sent to 'target' + being careful to account for membership in clientAliases. + + init sets 'ahls_to_route' according to the contents of pxrouting + + """ + super().__init__(options, logger) + + self.o.add_option('pxRouting', 'str', '') + self.o.add_option('pxClient', 'str', '') + + if not self.o.pxRouting: + logger.error("sundew_pxroute pxRouting file not defined") + return + elif not os.path.exists(self.o.pxRouting): + logger.error("sundew_pxroute pxRouting file (%s) not found" % self.o.pxRouting) + return + + self.ahls_to_route = {} + + pxrf = open(self.o.pxRouting, 'r') + possible_references = self.o.pxClient.split(',') + logger.info("sundew_pxroute, target clients: %s" % possible_references) + + for line in pxrf: + words = line.split() + + if (len(words) < 2) or words[0] == '#': + continue + + if words[0] == 'clientAlias': + expansion = words[2].split(',') + for i in possible_references: + if i in expansion: + possible_references.append(words[1]) + logger.debug("sundew_pxroute adding clientAlias %s to possible_reference %s" % (words[1], possible_references)) + continue + + if words[0] == 'key': + expansion = words[2].split(',') + for i in possible_references: + if i in expansion: + self.ahls_to_route[words[1]] = True + pxrf.close() + + logger.debug("sundew_pxroute For %s, the following headers are routed %s" % (self.o.pxClient, self.ahls_to_route.keys()))
+ + + def after_accept(self, worklist): + new_incoming = [] + + for message in worklist.incoming: + ahl = message['new_file'].split('/')[-1][0:11] + + if (len(ahl) < 11) or (ahl[6] != '_'): + logger.debug("sundew_pxroute not an AHL: %s" % ahl) + worklist.rejected.append(message) + continue + + if (ahl in self.ahls_to_route.keys()): + logger.debug("sundew_pxroute yes, deliver: %s" % ahl) + new_incoming.append(message) + continue + else: + logger.debug("sundew_pxroute no, do not deliver: %s" % ahl) + worklist.rejected.append(message) + + worklist.incoming = new_incoming
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/testretry.html b/_modules/sarracenia/flowcb/accept/testretry.html new file mode 100644 index 000000000..1eb7b2594 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/testretry.html @@ -0,0 +1,184 @@ + + + + + + sarracenia.flowcb.accept.testretry — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.testretry

+"""
+Plugin testretry.py:
+    This changes the message randomly so that it will cause
+    a download or send an error.
+    When a message is being retried, it is randomly fixed
+
+Usage:
+    flowcb sarracenia.flowcb.accept.testretry.TestRetry
+"""
+
+import logging
+import random
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class TestRetry(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + self.sendTo = None + self.msg_baseUrl_good = None + self.details_bad = None + self.msg_baseUrl_bad = 'sftp://ruser:rpass@retryhost'
+ + + def after_accept(self, worklist): + new_incoming = [] + for message in worklist.incoming: + logger.debug("testretry after_accept") + + if self.sendTo == None: + self.sendTo = self.o.sendTo + if self.msg_baseUrl_good == None: + self.msg_baseUrl_good = message['baseUrl'] + + # retry message : recover it + # update: this is set somewhere as true in /diskqueue.py, should think about initializing first so we + # dont have to test for existence + # 2022-06-10: isRetry was removed. Maybe can check if the message has msg_baseUrl_bad? + # see issues #466 and #527. + if 'isRetry' in message and message['isRetry']: + self.o.sendTo = self.sendTo + ok, self.o.details = self.o.credentials.get(self.sendTo) + + ## # FIXME dont see 'set_notice' as an entry in the message dictionary, could cause an error + ## #message['set_notice'](self.msg_baseUrl_good, message['relPath'], message['pubTime']) + # Fixed missing message.set_notice method; now just set baseUrl + message['baseUrl'] = self.msg_baseUrl_good + + # original message : 50% chance of breakage + elif random.randint(0, 2): + + # if sarra or subscribe break download + if self.o.component != 'sender': + logger.debug("making it bad 1") + ok, self.o.details = self.o.credentials.get(self.sendTo) + + ## # FIXME dont see 'set_notice' as an entry in the message dictionary, could cause an error + ## #message['set_notice'](self.msg_baseUrl_bad, message['relpath'], message['pubTime']) + # Fixed missing message.set_notice method; now just set baseUrl + message['baseUrl'] = self.msg_baseUrl_bad + + # if sender break destination + else: + logger.debug("making it bad 2") + self.o.sleep_connect_try_interval_max = 1.0 + self.o.sendTo = self.msg_baseUrl_bad + self.o.credentials._parse(self.msg_baseUrl_bad) + ok, self.o.details = self.o.credentials.get(self.msg_baseUrl_bad) + + logger.debug("return from testretry after_accept") + # TODO not sure where to add to new_incoming. as of now not appending to new_incoming or worklist.rejected + worklist.incoming = new_incoming
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/toclusters.html b/_modules/sarracenia/flowcb/accept/toclusters.html new file mode 100644 index 000000000..6ade1d90a --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/toclusters.html @@ -0,0 +1,156 @@ + + + + + + sarracenia.flowcb.accept.toclusters — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.toclusters

+"""
+Plugin toclusters.py:
+    Implements inter-pump routing by filtering destinations.
+    This is placed on a sarra process copying data between pumps.
+    Whenever it gets a message, it looks at the destination and processing
+    only continues if it is beleived that that message is a valid destination for the local pump.
+
+Options:
+    The local pump will select messages destined for the DD or DDI clusters, and reject those for DDSR, which isn't in the list.
+        - msgToClusters DDI
+        - msgToClusters DD
+Usage:
+    flowcb sarracenia.flowcb.accept.toclusters.ToClusters
+    msgToClusters x
+    msgToClusters y
+    ...
+"""
+
+import os, stat, time
+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class ToClusters(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + + self.o.add_option('msgToClusters', 'list') + + if self.o.msgToClusters == None: + logger.info("msgToClusters setting mandatory") + return + + logger.info("msgToClusters valid destinations: %s " % self.o.msgToClusters)
+ + + def after_accept(self, worklist): + new_incoming = [] + for message in worklist.incoming: + if message['to_clusters'] in self.o.msgToClusters: + new_incoming.append(message) + else: + worklist.rejected.append(message) + worklist.incoming = new_incoming
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/tohttp.html b/_modules/sarracenia/flowcb/accept/tohttp.html new file mode 100644 index 000000000..31c35862a --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/tohttp.html @@ -0,0 +1,160 @@ + + + + + + sarracenia.flowcb.accept.tohttp — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.tohttp

+"""
+Plugin tohttp.py:
+    ToHttp is the converse of ToFile.
+    After processing on a filter, a file URL needs to be turned back into a web url.
+    Uses savedUrl created by ToFile, to convert file url back to original.
+
+Usage:
+    flowcb sarracenia.flowcb.accept.tohttp.ToHttp
+"""
+import logging
+import re
+import urllib
+
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class ToHttp(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + + self._ldocroot = None + + #self.o.add_option('baseDir', 'str') + if self.o.baseDir: + self._ldocroot = self.o.baseDir + + self.o.add_option('toHttpRoot', 'str') + if self.o.toHttpRoot: + self._ldocroot = self.o.toHttpRoot
+ + + #self.o.hurlre = re.compile('file:/' + self.o.ldocroot) + + def after_accept(self, worklist): + for message in worklist.incoming: + logger.info("ToHttp message input: baseUrl=%s, relPath=%s" % (message['baseUrl'], message['relPath'])) + + url = urllib.parse.urlparse(message['baseUrl']) + + new_baseUrl = 'http://' + url.netloc + if self._ldocroot != None: + new_baseUrl += self._ldocroot + + new_baseUrl += url.path + + message['baseUrl'] = new_baseUrl.replace('///', '//') + + logger.debug("ToHttp config; baseDir=%s, toHttpRoot=%s" % (self.o.baseDir, self.o.toHttpRoot)) + logger.info("ToHttp message output: baseUrl=%s, relPath=%s" % (message['baseUrl'], message['relPath']))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/tolocal.html b/_modules/sarracenia/flowcb/accept/tolocal.html new file mode 100644 index 000000000..40e85702e --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/tolocal.html @@ -0,0 +1,193 @@ + + + + + + sarracenia.flowcb.accept.tolocal — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.tolocal

+"""
+Plugin tolocal.py:
+    This is a helper script to work with filters.
+    What a data pump advertises, it will usually use Web URL, but if one is
+    on a server where the files are available, it is more efficient to access 
+    them as local files, so filters operate on file urls.  
+    
+Example:
+    baseDir /var/www/html
+    url is http://localhost/<date>/<src>/input/file.txt
+
+    flowcb sarracenia.flowcb.accept.tolocal.ToLocal   # converts web URL to file URL
+
+            http://localhost/ --> file://var/www/html/
+            url is now file://var/www/html/<date>/<src>/input/file.txt
+            m.savedurl = http://localhost/
+
+    flowcb sarracenia.flowcb.accept.<some converter that works on local files.>
+
+            A new file is created in another directory.
+            url is now file://var/www/<date>/<src>/output/file.txt
+
+    flowcb sarracenia.flowcb.accept.tohttp.ToHttp     # turns the file URL back into a web one.
+
+            file://var/www/html/ --> http:///localhost/
+            url is now:   http://localhost/<date>/<src>/output/file.txt
+
+
+    The regular expression used to find the web url matches either http or https
+    and just captures upto the first '/'.
+
+    if you need to capture a different kind of url, such as ...
+
+    https://hostname/~user/ ....
+
+    The easiest way is to set toLocalUrl as follows:
+
+    baseDir /home/user/www
+    toLocalUrl (https://hostname/~user/)
+
+    the parentheses around the URL set the value of to be put in m.savedurl that
+    will be restored when the companion plugin msg_2http is called.
+
+Usage: 
+    flowcb sarracenia.flowcb.accept.tolocal.ToLocal
+"""
+import logging
+import re
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class ToLocal(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + + self._ldocroot = None + + if self.o.baseDir: + self._ldocroot = self.o.baseDir + + self.o.add_option('toLocalRoot', 'str') + if self.o.toLocalRoot: + self._ldocroot = self.o.toLocalRoot + + self._lurlre = re.compile("(http[s]{0,1}://[^/]+/)") + + self.o.add_option('toLocalUrl', 'str') + if self.o.toLocalUrl: + self._lurlre = re.compile(self.o.toLocalUrl)
+ + + def after_accept(self, worklist): + for message in worklist.incoming: + # TODO should all these be logger.error? should we append + # to worklist.rejected or worklist.failed at some point? + logger.debug("input: urlstr: %s" % message['urlstr']) + + message['savedurl'] = self._lurlre.match(message['urlstr']).group(1) + message['urlstr'] = 'file:/%s' % self._lurlre.sub(self._ldocroot + '/', message['urlstr']) + + logger.debug("doc_root=%s " % (self.o.baseDir)) + logger.debug("output: savedurl: %s" % (message['savedurl']))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/tolocalfile.html b/_modules/sarracenia/flowcb/accept/tolocalfile.html new file mode 100644 index 000000000..7ba5f55db --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/tolocalfile.html @@ -0,0 +1,214 @@ + + + + + + sarracenia.flowcb.accept.tolocalfile — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.tolocalfile

+"""
+Plugin tolocalfile.py:
+    This is a helper script to work with converters (filters) and senders.
+
+    What a data pump advertises, it will usually use Web URL, but if one is
+    on a server where the files are available, it is more efficient to access
+    them as local files, and so this plugin turn the message's notice Web URL
+    into a File URL (file:/d1/d2/.../fn)
+
+Normal Usage:
+    A Web URL in an amqp message is hold in the following values:
+    message['baseUrl'] (ex.: http://localhost)  and
+    message['relPath'] (ex.: /<data>/<src>/d3/.../fn)
+
+    We will save these values before their modification :
+    message['saved_baseUrl'] = message['baseUrl']
+    message['saved_relPath'] = message['relPath']
+
+    We will then turn them into an absolute File Url: (Note if a baseDir was set it prefix the relPath)
+    message['baseUrl'] = 'file:'
+    message['relPath'] = [baseDir] + message['relPath']
+
+Example:
+    baseDir /var/www/html
+
+    message pubtime=20171003131233.494 baseUrl=http://localhost relPath=/20171003/CMOE/productx.gif
+
+    flowcb sarracenia.flowcb.accept.tolocalfile.ToLocalFile
+
+    will receive this::
+    * message['baseUrl']  is  'http://localhost'
+    * message['relPath']  is  '/20171003/CMOE/GIF/productx.gif'
+
+    * will copy/save these values
+    * message['saved_baseUrl'] = message['baseUrl']
+    * message['saved_relPath'] = message['relPath']
+
+    * turn the original values into a File URL
+    * message['baseUrl'] = 'file:'
+    * if parent['baseDir'] :
+      *  message['relPath'] = parent['baseDir'] + '/' + message['relPath']
+      *  message['relPath'] = message['relPath'].replace('//','/')
+
+
+    A sequence of after_accept plugins can perform various changes to the messages and/or
+    to the product...  so here lets pretend we have an after_accept plugin that converts
+    gif to png  and prepares the proper message for it
+
+    flowcb sarracenia.flowcb.accept.giftopng.GifToPng
+    After the tolocalfile this script could perform something like::
+
+        # build the absolute path of the png product
+        new_path = message['relPath'].replace('GIF','PNG')
+        new_path[-4:] = '.png'
+
+        # proceed to the conversion gif2png
+        ok = self.gif2png(gifpath=message.relPath,pngpath=new_path)
+
+    change the message to announce the new png product::
+    
+        if ok :
+            message['baseUrl'] = message['saved_baseUrl']
+        message['relPath'] = new_path
+        if self.o.baseDir :
+            message['relPath'] = new_path.replace(self.o.baseDir,'',1)
+        else :
+            logger.error(...
+        # we are ok... proceed with this png file
+
+Usage:
+    flowcb sarracenia.flowcb.accept.tolocalfile.ToLocalFile
+
+"""
+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger('__name__')
+
+
+
+[docs] +class ToLocalFile(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def after_accept(self, worklist): + new_incoming = [] + for message in worklist.incoming: + if message['baseUrl'] == 'file:': + new_incoming.append(message) + continue + + message['saved_baseUrl'] = message['baseUrl'] + message['saved_relPath'] = message['relPath'] + message['baseUrl'] = 'file:' + + if self.o.baseDir and not message['relPath'].startswith(self.o.baseDir): + message['relPath'] = self.o.baseDir + '/' + message['relPath'] + message['relPath'].replace('//', '/') + new_incoming.append(message) + continue + else: + worklist.rejected.append(message) + + worklist.incoming = new_incoming
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/accept/wmotypesuffix.html b/_modules/sarracenia/flowcb/accept/wmotypesuffix.html new file mode 100644 index 000000000..9ffc253b9 --- /dev/null +++ b/_modules/sarracenia/flowcb/accept/wmotypesuffix.html @@ -0,0 +1,169 @@ + + + + + + sarracenia.flowcb.accept.wmotypesuffix — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.accept.wmotypesuffix

+"""
+Plugin wmotypesuffix.py:
+    Given the WMO-386 TT designator of a WMO file, file type suffix to the file name.
+    Web browsers and modern operating systems may do *the right thing* if files have a recognizable suffix.
+
+    http://www.wmo.int/pages/prog/www/ois/Operational_Information/Publications/WMO_386/AHLsymbols/TableA.html
+
+    Status: proof of concept demonstrator... missing many TT's. please add!
+    Tested with UNIDATA feeds, discrepancies:
+    TableA says L is Aviation XML, but UNIDATA Feed, it is all GRIB.
+    XW - should be CAP, but is GRIB.
+    IX used by Americans for HDF, unsure if that is kosher/halal/blessed, but it is in the UNIDATA feed.
+
+    IU/IS/IB are BUFR
+    other type designators welcome... for example, GRIB isn't identified yet.
+    default to .txt.
+
+Usage:
+    flowcb sarracenia.flowcb.accept.wmotypesuffix.WmoTypeSuffix
+  
+"""
+import logging
+from sarracenia.flowcb import FlowCB
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class WmoTypeSuffix(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger)
+ + + def __find_type(self, TT): + if TT[0] in ['G']: return '.grid' + if TT in ['IX']: return '.hdf' + if TT[0] in ['I']: return '.bufr' + if TT[0] in ['K']: return '.crex' + if TT in ['LT']: return '.iwxxm' + if TT[0] in ['L']: return '.grib' + if TT in ['XW']: return '.txt' + if TT[0] in ['X']: return '.cap' + if TT[0] in ['D', 'H', 'O', 'Y']: return '.grib' + if TT[0] in ['E', 'P', 'Q', 'R']: return '.bin' + return '.txt' + + def after_accept(self, worklist): + for message in worklist.incoming: + type_suffix = self.__find_type(message['new_file'][0:2]) + ## FIXME confused as to how this could ever be true since find_type never returns "UNKNOWN" + #if type_suffix == 'UNKNOWN': + # continue + + # file name already has suffix + if message['new_file'][-len(type_suffix):] == type_suffix: + continue + + message['new_file'] = message['new_file'] + type_suffix + if 'rename' in message: + message['rename'] = message['rename'] + type_suffix
+ + # TODO else -> worklist.rejected.append(message) ?? should this be happening at any point? +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/clamav.html b/_modules/sarracenia/flowcb/clamav.html new file mode 100644 index 000000000..d43ce9e46 --- /dev/null +++ b/_modules/sarracenia/flowcb/clamav.html @@ -0,0 +1,226 @@ + + + + + + sarracenia.flowcb.clamav — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.clamav

+"""
+ A sample on_part plugin to perform virus scanning, using the ClamAV engine.
+
+ requires a clamd binding package to be installed. On debian derived systems:: 
+
+    sudo apt-get install python3-pyclamd
+
+ on others::
+
+    pip3 install pyclamd
+
+ author: Peter Silva
+"""
+
+import logging
+import os
+import stat
+import time
+
+from sarracenia import nowflt
+import sarracenia
+from sarracenia.flowcb import FlowCB
+
+#
+# Support for features inventory mechanism.
+#
+from sarracenia.featuredetection import features
+
+features['clamd'] = { 'modules_needed': [ 'pyclamd' ], 'Needed': True,
+        'lament' : 'cannot use clamd to av scan files transferred',
+        'rejoice' : 'can use clamd to av scan files transferred' }
+
+try:
+    import pyclamd
+    features['clamd']['present'] = True
+except:
+    features['clamd']['present'] = False
+
+
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Clamav(FlowCB): + """ + Invoke ClamAV anti-virus scanning on files as they pass through a data pump. + when it is invoked depends on the component it is used from. + + from a sender, post, or poll, the scan should stop processing prior + to the transfer. + + for other components, subsscribers that download, it needs to take place + after downloading. + + """ +
+[docs] + def __init__(self, options) -> None: + + super().__init__(options,logger) + + self.metric_scanned = 0 + self.metric_hits = 0 + + if sarracenia.features['pyclamd']['present']: + import pyclamd + self.av = pyclamd.ClamdAgnostic() + print("clam_scan on_part plugin initialized")
+ + + def avscan_hit(self, scanfn) -> bool: + + # worried about how long the scan will take. + start = nowflt() + virus_found = self.av.scan_file(scanfn) + end = nowflt() + self.metric_scanned += 1 + + if virus_found: + logger.error( + "part_clamav_scan took %g not forwarding, virus detected in %s" + % (end - start, scanfn)) + self.metric_hits += 1 + return False + + logger.info("part_clamav_scan took %g seconds, no viruses in %s" % + (end - start, scanfn)) + return True + + def after_accept(self, worklist) -> None: + if self.o.component in ['sender', 'post', 'watch']: + new_incoming = [] + for m in worklist.incoming: + scanfn = m['new_dir'] + os.sep + m['new_file'] + logger.info(f'scanning: {scanfn}') + if self.avscan_hit(scanfn): + worklist.rejected.append(m) + else: + new_incoming.append(m) + worklist.incoming = new_incoming + + def after_work(self, worklist) -> None: + if self.o.component in ['subscribe', 'sarra']: + new_ok = [] + for m in worklist.ok: + scanfn = m['new_dir'] + os.sep + m['new_file'] + logger.info(f'scanning: {scanfn}') + if self.avscan_hit(scanfn): + worklist.rejected.append(m) + else: + new_ok.append(m) + worklist.ok = new_ok + + def on_housekeeping(self): + logger.info( + f'files scanned {self.metric_scanned}, hits: {self.metric_hits} ') + self.metric_scanned = 0 + self.metric_hits = 0
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/destfn/sample.html b/_modules/sarracenia/flowcb/destfn/sample.html new file mode 100644 index 000000000..c387a4d17 --- /dev/null +++ b/_modules/sarracenia/flowcb/destfn/sample.html @@ -0,0 +1,154 @@ + + + + + + sarracenia.flowcb.destfn.sample — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.destfn.sample

+"""
+
+  a destfn plugin script is used by senders or subscribers to do complex file naming.
+  this is an API demonstrator that prefixes the name delivered with 'renamed\_'::
+
+     filename DESTFNSCRIPT=sarracenia.flowcb.destfn.sample.Sample
+
+  An alternative method of invocation is to apply it selectively::
+
+     accept k* DESTFNSCRIPT=sarracenia.flowcb.destfn.sample.Sample
+
+  As with other flowcb plugins, the import will be done using normal
+  python import mechanism equivalent to::
+
+     import sarracenia.flowcb.destfn.sample
+
+  and then in that class file, there is a Sample class, the sample class
+  contains the destfn method, or entry_point.
+ 
+  The destfn routine consults the fields in the given message, and based on them,
+  return a new file name for the file to have after transfer (download or send.)
+
+  the routines have access to the settings via options provided to init,
+  accessed, by convention, as self.o.
+
+  The routine can also modify fields create new ones in the message.
+
+  the destfn routine returns the new name of the file.
+
+"""
+
+import logging
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+[docs] +class Sample(FlowCB): + + def destfn(self,msg) -> str: + + logger.info('before: m=%s' % msg ) + relPath = msg["relPath"].split('/') + msg['destfn_added_prefix'] = 'renamed_' + return 'renamed_' + relPath[-1]
+ + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/filter/deleteflowfiles.html b/_modules/sarracenia/flowcb/filter/deleteflowfiles.html new file mode 100644 index 000000000..397ce1b2e --- /dev/null +++ b/_modules/sarracenia/flowcb/filter/deleteflowfiles.html @@ -0,0 +1,137 @@ + + + + + + sarracenia.flowcb.filter.deleteflowfiles — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.filter.deleteflowfiles

+import logging
+import os
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class DeleteFlowFiles(FlowCB): + """ + This is a custom callback class for the sr_insects flow tests. + delete files for messages in two directories. + """ + + def after_accept(self, worklist): + + for m in worklist.incoming: + + f = "%s%s%s" % (m['new_dir'], os.sep, m['new_file']) + try: + os.unlink(f) + os.unlink(f.replace('/cfr/', '/cfile/')) + worklist.ok.append(m) + logger.info("msg_delete: %s" % f) + except OSError as err: + logger.error("could not unlink {}: {}".format(f, err)) + logger.debug("Exception details:", exc_info=True) + worklist.failed.append(m) + worklist.incoming = []
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/filter/fdelay.html b/_modules/sarracenia/flowcb/filter/fdelay.html new file mode 100644 index 000000000..74eb95dbd --- /dev/null +++ b/_modules/sarracenia/flowcb/filter/fdelay.html @@ -0,0 +1,202 @@ + + + + + + sarracenia.flowcb.filter.fdelay — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.filter.fdelay

+"""
+  This plugin delays processing of messages by *message_delay* seconds
+
+  sarracenia.flowcb.msg.fdelay 30
+  import sarracenia.flowcb.filter.fdelay.Fdelay
+
+  or more simply:
+
+  fdelay 30
+  callback filter.fdelay
+
+  every message will be at least 30 seconds old before it is forwarded by this plugin.
+  in the meantime, the message is placed on the retry queue by marking it as failed.
+
+"""
+import logging
+import os
+import os.path
+import stat
+
+from sarracenia.flowcb import FlowCB
+from sarracenia import timestr2flt, nowflt
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Fdelay(FlowCB): +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + + logging.basicConfig(format=self.o.logFormat, + level=getattr(logging, self.o.logLevel.upper())) + + self.o.add_option('msg_fdelay', 'duration', 60) + self.o.add_option('fdelay', 'duration', 60) + + #parent.declare_option('fdelay') + if hasattr(self.o, 'msg_fdelay'): + self.o.fdelay = self.o.msg_fdelay
+ + + + def after_accept(self, worklist): + # Prepare msg delay test + outgoing = [] + for m in worklist.incoming: + # Test msg delay + elapsedtime = nowflt() - timestr2flt(m['pubTime']) + + if 'fileOp' in m and 'remove' in m['fileOp'] : + # 'remove' msg will be removed by itself + worklist.rejected.append(m) + logger.debug('marked rejected 0 (file removal)') + continue + + # Test msg delay + elapsedtime = nowflt() - timestr2flt(m['pubTime']) + if elapsedtime < self.o.fdelay: + dbg_msg = "message not old enough, sleeping for {:.3f} seconds" + logger.debug( + dbg_msg.format(elapsedtime, self.o.fdelay - elapsedtime)) + worklist.failed.append(m) + logger.debug('marked failed 1 (message not old enough)') + continue + + # Prepare file delay test + if '/cfr/' in m['new_dir']: + f = os.path.join(m['new_dir'], m['new_file']) + else: + f = '/' + m['relPath'] + if not os.path.exists(f): + logger.debug("did not find file {}".format(f)) + worklist.failed.append(m) + logger.debug('marked failed 2 (file not found)') + continue + + # Test file delay + filetime = os.path.getmtime(f) + elapsedtime = nowflt() - filetime + if elapsedtime < self.o.fdelay: + dbg_msg = "file not old enough, sleeping for {:.3f} seconds" + logger.debug( + dbg_msg.format(elapsedtime, self.o.fdelay - elapsedtime)) + worklist.failed.append(m) + logger.debug('marked failed 3 file not old enough') + continue + + logger.debug('appending to outgoing') + outgoing.append(m) + + worklist.incoming = outgoing
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/filter/pclean_f90.html b/_modules/sarracenia/flowcb/filter/pclean_f90.html new file mode 100644 index 000000000..58b54f15a --- /dev/null +++ b/_modules/sarracenia/flowcb/filter/pclean_f90.html @@ -0,0 +1,240 @@ + + + + + + sarracenia.flowcb.filter.pclean_f90 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.filter.pclean_f90

+""" 
+   msg_pclean_f90 module: file propagation test for Sarracenia components (in flow test)
+   https://github.com/MetPX/sr_insects/
+
+"""
+
+from difflib import Differ
+import filecmp
+import logging
+import os
+import random
+
+from sarracenia.flowcb.pclean import PClean
+from sarracenia import timestr2flt, nowflt
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PClean_F90(PClean): + """ + functionality within the flow_tests of the sr_insects project. + This plugin class receive a msg from xflow_public and check propagation of the underlying file + + - it checks if the propagation was ok + - it randomly set a new test file with a different type in the watch dir (f31 amqp) + - it posts the product to be treated by f92 + - when the msg for the extension file comes back, recheck the propagation + + When a product is not fully propagated, the error is reported and the test is considered as a + failure. It also checks if the file differs from original + """ + def after_accept(self, worklist): + + logger.info("start len(worklist.incoming) = %d" % + len(worklist.incoming)) + + outgoing = [] + + for msg in worklist.incoming: + + result = True + f20_path = '/' + msg['relPath'].replace( + "{}/".format(self.all_fxx_dirs[1]), self.all_fxx_dirs[0]) + path_dict = self.build_path_dict(self.all_fxx_dirs[2:], + '/' + msg['relPath']) + ext = self.get_extension('/' + msg['relPath']) + logger.info('looking at: %s' % msg['relPath']) + logger.info('path_dict: %s' % path_dict) + + for fxx_dir, path in path_dict.items(): + # f90 test + logger.info('for looping: %s' % path) + if not (os.path.isfile(path) or os.path.islink(path)): + # propagation check to all path except f20 which is the origin + err_msg = "file not in folder {} with {:.3f}s elapsed" + lag = nowflt() - timestr2flt(msg['pubTime']) + logger.error(err_msg.format(fxx_dir, lag)) + logger.debug("file missing={}".format(path)) + result = False + worklist.failed.append(msg) + break + elif ext not in self.test_extension_list and not filecmp.cmp( + f20_path, path): + # file differ check: f20 against others + logger.error( + "skipping, file differs from f20 file: {}".format( + path)) + with open(f20_path, 'r', encoding='iso-8859-1') as f: + f20_lines = f.readlines() + with open(path, 'r', encoding='iso-8859-1') as f: + f_lines = f.readlines() + diff = Differ().compare(f20_lines, f_lines) + diff = [d for d in diff + if d[0] != ' '] # Diffs without context + logger.info("a: len(%s) = %d" % (f20_path, len(f20_lines))) + logger.info("b: len(%s) = %d" % (path, len(f_lines))) + logger.info("diffs found:\n{}".format("".join(diff))) + + if not result: + logger.info('queued for retry because propagation not done yet.') + continue + + if ext not in self.test_extension_list: + # prepare next f90 test + test_extension = self.test_extension_list[self.ext_count % len( + self.test_extension_list)] + self.ext_count += 1 + # pick one test identified by file extension + src = '/' + msg['relPath'] # src file is in f30 dir + dest = "{}{}".format( + src, test_extension + ) # format input file for extension test (next f90) + + try: + if test_extension == '.slink': + os.symlink(src, dest) + logger.info('symlinked %s %s' % (src, dest)) + elif test_extension == '.hlink': + os.link(src, dest) + logger.info('hlinked %s %s' % (src, dest)) + elif test_extension == '.moved': + os.rename(src, dest) + logger.info('moved %s %s' % (src, dest)) + else: + logger.error("test '{}' is not supported".format( + test_extension)) + except FileNotFoundError as err: + # src is not there + logger.error("test failed: {}".format(err)) + logger.debug("Exception details:", exc_info=True) + result = False + except FileExistsError as err: + # dest is already there + logger.error( + 'skipping, found a moving target {}'.format(err)) + logger.debug("Exception details:", exc_info=True) + result = False + else: + logger.info('ext not in test_extenion_list') + + if 'toolong' in msg: + # cleanup + del msg['toolong'] + + if result: + outgoing.append(msg) + else: + worklist.rejected.append(msg) + + worklist.incoming = outgoing + logger.info("end len(worklist.incoming) = %d" % len(worklist.incoming)) + logger.info("end len(worklist.rejected) = %d" % len(worklist.rejected))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/filter/pclean_f92.html b/_modules/sarracenia/flowcb/filter/pclean_f92.html new file mode 100644 index 000000000..d19916133 --- /dev/null +++ b/_modules/sarracenia/flowcb/filter/pclean_f92.html @@ -0,0 +1,179 @@ + + + + + + sarracenia.flowcb.filter.pclean_f92 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.filter.pclean_f92

+""" msg_pclean_f90 module: second file propagation test for Sarracenia components (in flow test)
+"""
+
+from difflib import Differ
+import filecmp
+import logging
+import os
+import random
+
+from sarracenia.flowcb.pclean import PClean
+from sarracenia import timestr2flt, nowflt
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PClean_F92(PClean): + """ This plugin that manage the removal of every file + + - it fails if one removal failed + """ + def after_accept(self, worklist): + import os + + logger.info("start len(worklist.incoming) = %d" % + len(worklist.incoming)) + + outgoing = [] + + for msg in worklist.incoming: + result = True + ext = self.get_extension('/' + msg['relPath']) + logger.info("relPath=%s ext: %s in %s ?" % + (msg['relPath'], ext, self.test_extension_list)) + + if ext in self.test_extension_list: + f20_path = '/' + msg['relPath'].replace( + "{}/".format(self.all_fxx_dirs[1]), self.all_fxx_dirs[0]) + f20_path = f20_path.replace(ext, '') + try: + os.unlink(f20_path) + logger.info("unlinked 1: %s" % f20_path) + except FileNotFoundError as err: + logger.error("could not unlink 1 in {}: {}".format( + f20_path, err)) + logger.debug("Exception details:", exc_info=True) + result = False + fxx_dirs = self.all_fxx_dirs[1:2] + self.all_fxx_dirs[6:] + path_dict = self.build_path_dict(fxx_dirs, + '/' + msg['relPath']) + for fxx_dir, path in path_dict.items(): + try: + os.unlink(path) + logger.info("unlinked 2: %s" % path) + if ext != '.moved': + os.unlink(path.replace(ext, '')) + logger.info("unlinked 3: %s" % + path.replace(ext, '')) + except OSError as err: + logger.error("could not unlink 2or3 in {}: {}".format( + fxx_dir, err)) + logger.debug("Exception details:", exc_info=True) + result = False + if result: + logger.debug('passing to pclean_f92') + outgoing.append(msg) + else: + worklist.rejected.append(msg) + + worklist.incoming = outgoing + logger.info("len(worklist.incoming) = %d" % len(worklist.incoming)) + logger.info("len(worklist.rejected) = %d" % len(worklist.rejected))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/filter/wmo2msc.html b/_modules/sarracenia/flowcb/filter/wmo2msc.html new file mode 100644 index 000000000..f61714308 --- /dev/null +++ b/_modules/sarracenia/flowcb/filter/wmo2msc.html @@ -0,0 +1,500 @@ + + + + + + sarracenia.flowcb.filter.wmo2msc — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.filter.wmo2msc

+r"""
+wmo2msc.py is an on_message plugin script to convert WMO bulletins on local disk
+to MSC internal format in an alternate tree.  It is analogous to Sundew's 'bulletin-file'.
+Meant to be called as an sr_shovel plugin.
+
+It prints an output line:
+
+wmo2msc: <input_file> -> <output_file> (<detected format>)
+
+usage:
+
+Use the directory setting to know the root of tree where files are placed.
+FIXME: well, likely what you really what is something like::
+
+     <date>/<source>/dir1/dir2/dir3
+
+     <date>/<source>/dir1/dir2/newdir4/...
+
+     -- so Directory doesn't cut it.
+
+In a sr_shovel configuration:: 
+
+    directory /.... 
+    callback filter.wmo2msc
+
+
+Parameters:
+
+* filter_wmo2msc_replace_dir  old,new
+
+* filter_wmo2msc_uniquify hash|time|anything else
+  - whether to add a string in addition to the AHL to make the filename unique.
+  - hash - means apply a hash, so that the additional string is content based.
+  - if time, add a suffix _YYYYMMDDHHMMSS_99999 which ensures file name uniqueness.
+  - otherwise, no suffix will be added.
+  - default: hash
+
+* filter_wmo2msc_convert on|off
+  if on, then traditional conversion to MSC-BULLETINS is done as per TANDEM/APPS & MetPX Sundew
+  this involves \n as termination character, and other charater substitutions.
+
+* filter_wmo2msc_tree  on|off
+  if tree is off, files are just placed in destination directory.
+  if tree is on, then the file is placed in a subdirectory tree, based on
+  the WMO 386 AHL::
+
+         TTAAii CCCC YYGGgg  ( example: SACN37 CWAO 300104 )
+ 
+         TT = SA - surface observation.
+         AA = CN - Canada ( but the AA depends on TT value, in many cases not a national code. )
+         ii = 37 - a number.. there are various conventions, they are picked to avoid duplication.
+     
+  The first line of the file is expected to contain an AHL. and when we build a tree
+  from it, we build it as follows::
+
+      TT/CCCC/GG/TTAAii_CCCC_YYGGgg_<uniquify>
+
+  assuming tree=on, uniquify=hash:
+
+     SA/CWAO/01/SACN37_CWAO_300104_1c699da91817cc4a84ab19ee4abe4e22
+
+NOTE: Look at the end of the file for SUPPLEMENTARY INFORMATION 
+      including hints about debugging.
+
+"""
+
+import sys
+import os
+import re
+import time
+import hashlib
+import logging
+import random
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger('__name__')
+
+
+
+[docs] +class Wmo2msc(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + + self.o.add_option( 'filter_wmo2msc_uniquify', 'str', 'hash' ) + self.o.add_option( 'filter_wmo2msc_replace_dir', 'str' ) + self.o.add_option( 'filter_wmo2msc_treeify', 'flag', True ) + self.o.add_option( 'filter_wmo2msc_convert', 'flag', True ) + + if not hasattr(self.o, 'filter_wmo2msc_replace_dir'): + logger.error("filter_wmo2msc_replace_dir setting is mandatory") + return + + (self.o.filter_olddir, self.o.filter_newdir) = self.o.filter_wmo2msc_replace_dir.split(',') + + logger.info("filter_wmo2msc old-dir=%s, newdir=%s" % + (self.o.filter_olddir, self.o.filter_newdir)) + + self.trimre = re.compile(b" +\n")
+ + +
+[docs] + def replaceChar(self, oldchar, newchar): + """ + replace all occurrences of oldchar by newchar in the the message byte stream. + started as a direct copy from sundew of routine with same name in bulletin.py + - the storage model is a bit different, we store the entire message as one bytearray. + - sundew stored it as a series of lines, so replaceChar implementation changed. + + """ + self.bintxt = self.bintxt.replace(bytearray(oldchar, 'latin_1'), + bytearray(newchar, 'latin_1'))
+ + +
+[docs] + def doSpecificProcessing(self): + """doSpecificProcessing() + + Modify bulletins received from Washington via the WMO socket protocol. + started as a direct copy from sundew of routine with same name in bulletinManagerWmo.py + + - encode/decode, and binary stuff came because of python3 + """ + + ahl2 = self.bulletin[0][:2].decode('ascii') + ahl4 = self.bulletin[0][:4].decode('ascii') + + if ahl2 in [ + 'SD', 'SO', 'WS', 'SR', 'SX', 'FO', 'WA', 'AC', 'FA', 'FB', + 'FD' + ]: + self.replaceChar('\x1e', '') + + if ahl2 in ['SR', 'SX']: + self.replaceChar('~', '\n') + + if ahl2 in ['UK']: + self.replaceChar('\x01', '') + + if ahl4 in ['SICO']: + self.replaceChar('\x01', '') + + if ahl2 in ['SO', 'SR']: + self.replaceChar('\x02', '') + + if ahl2 in ['SX', 'SR', 'SO']: + self.replaceChar('\x00', '') + + if ahl2 in ['SX']: + self.replaceChar('\x11', '') + self.replaceChar('\x14', '') + self.replaceChar('\x19', '') + self.replaceChar('\x1f', '') + + if ahl2 in ['SR']: + self.replaceChar('\b', '') + self.replaceChar('\t', '') + self.replaceChar('\x1a', '') + self.replaceChar('\x1b', '') + self.replaceChar('\x12', '') + + if ahl2 in ['FX']: + self.replaceChar('\x10', '') + self.replaceChar('\xf1', '') + + if ahl2 in ['WW']: + self.replaceChar('\xba', '') + + if ahl2 in ['US']: + self.replaceChar('\x18', '') + + if ahl4 in ['SXUS', 'SXCN', 'SRCN']: + self.replaceChar('\x7f', '?') + + if ahl4 in ['SRCN']: + self.replaceChar('\x0e', '') + self.replaceChar('\x11', '') + self.replaceChar('\x16', '') + self.replaceChar('\x19', '') + self.replaceChar('\x1d', '') + self.replaceChar('\x1f', '') + + if ahl4 in ['SXVX', 'SRUS', 'SRMT']: + self.replaceChar('\x7f', '') + + self.replaceChar('\r', '') + + if ahl2 in ['SA', 'SM', 'SI', 'SO', 'UJ', 'US', 'FT']: + self.replaceChar('\x03', '') + + #trimming of trailing blanks. + lenb = len(self.bintxt) + self.bintxt = self.trimre.sub(b"\n", self.bintxt) + if len(self.bintxt) < lenb: + print('Trimmed %d trailing blanks!' % (lenb - len(self.bintxt)))
+ + + def after_accept(self, worklist): + new_incoming = [] + for message in worklist.incoming: + if message['baseUrl'] != 'file:': + logger.error( + 'filter_wmo2msc needs local files invalid url: %s ' % + (message['baseUrl'] + message['relPath'])) + worklist.rejected.append(message) + continue + + input_file = message['relPath'] + + # read once to get headers and type. + + logger.debug('filter_wmo2msc reading file: %s' % (input_file)) + + with open(input_file, 'rb') as s: + self.bulletin = [s.readline(), s.read(4)] + + AHLfn = (self.bulletin[0].replace(b' ', + b'_').strip()).decode('ascii') + + if len(AHLfn) < 18: + logger.error( + 'filter_wmo2msc: not a WMO bulletin, malformed header: (%s)' + % (AHLfn)) + worklist.rejected.append(message) + continue + + # read second time for the body in one string. + with open(input_file, 'rb') as s: + self.bintxt = s.read() + + logger.debug('filter_wmo2msc read twice: %s ' % (input_file)) + + # Determine file format (fmt) and apply transformation. + if self.bulletin[1].lstrip()[:4] in ['BUFR', 'GRIB', '\211PNG']: + fmt = 'wmo-binary' + self.replaceChar('\r', '') + elif self.bulletin[0][:11] in ['SFUK45 EGRR']: + # This file is encoded in an indecipherably non-standard format. + fmt = 'unknown-binary' + + #self.replaceChar('\r','',2) replace only the first 2 carriage returns. + self.bintxt = self.bintxt.replace(bytearray('\r', 'latin_1'), + bytearray('', 'latin_1'), 2) + else: + fmt = 'wmo-alphanumeric' + if self.o.filter_wmo2msc_convert: + self.doSpecificProcessing() + + # apply 'd' checksum (md5) + + s = hashlib.md5() + s.update(self.bintxt) + sumstr = ''.join(format(x, '02x') for x in s.digest()) + + # Determine local file name. + if self.o.filter_wmo2msc_uniquify in ['time']: + + AHLfn += '_' + time.strftime( "%Y%m%d%H%M%S", time.gmtime(time.time()) ) + \ + '_%05d' % random.randint(0,9999) + elif self.o.filter_wmo2msc_uniquify in ['hash']: + #AHLfn += '_%s' % ''.join( format(x, '02x') for x in s.digest() ) + AHLfn += '_' + sumstr + + if self.o.filter_wmo2msc_treeify: + d = os.path.dirname(input_file) + logger.debug('filter_wmo2msc check %s start match: %s' % + (d, self.o.filter_olddir)) + d = d.replace(self.o.filter_olddir, self.o.filter_newdir) + logger.debug('filter_wmo2msc check %s after replace' % (d)) + if not os.path.isdir(d): + os.makedirs(d, self.o.permDirDefault, True) + + d = d + os.sep + self.bulletin[0][0:2].decode('ascii') + d = d + os.sep + self.bulletin[0][7:11].decode('ascii') + logger.debug('filter_wmo2msc check %s' % (d)) + if not os.path.isdir(d): + os.makedirs(d, self.o.permDirDefault, True) + + d = d + os.sep + self.bulletin[0][14:16].decode('ascii') + logger.debug('filter_wmo2msc check %s' % (d)) + if not os.path.isdir(d): + os.makedirs(d, self.o.permDirDefault, True) + + local_file = d + os.sep + AHLfn + else: + local_file = self.o.currentDir + os.sep + AHLfn + + # write the data. + fileOK = False + + if not self.o.filter_wmo2msc_convert: + try: + os.link(input_file, local_file) + fileOK = True + except: + pass + + if self.o.filter_wmo2msc_convert or not fileOK: + d = open(local_file, 'wb+') + d.write(self.bintxt) + d.close() + + logger.debug('filter_wmo2msc %s -> %s (%s)' % + (input_file, local_file, fmt)) + + # set how the file will be announced + + baseDir = self.o.base_dir + if baseDir == None: baseDir = self.o.post_base_dir + + relPath = local_file + if baseDir != None: relPath = local_file.replace(baseDir, '') + + baseUrl = 'file:' + # from tolocal.py if used + if 'savedUrl' in message.keys(): baseUrl = message['savedUrl'] + # from tolocalfile.py if used + if 'saved_baseUrl' in message.keys(): + baseUrl = message['saved_baseUrl'] + + relPath = relPath.replace('//', '/') + logger.debug('filter_wmo2msc relPath %s' % relPath) + + message['set_topic'](self.o.topic_prefix, relPath) + #message['set_notice'](baseUrl, relPath) + message['baseUrl'] = baseUrl + message['relPath'] = relPath + new_incoming.append(message) + worklist.incoming = new_incoming
+ + + +""" + +SUPPLEMENTARY INFORMATION + +SUNDEW COMPATIBILITY: This script is the sarra version of a 'bulletin-file' receiver. + +dod_filter_wmo2msc.py is a do_download plugin script used with sr_sarra to convert +World Meteorological Organisation (WMO) standard (See WMO-386 and WMO-306) bulletins +into the similar but different internal format used by the Meteorological Service of +Canada (MSC.) + +transformation of bulletins is based on detecting the format by reading the first +line of the file, and the first four bytes of after the first line. detected format +is one of: + wmo-binary: GRIB, BUFR, or PNG, which require carriage returns to be removed ? + wmo-alpha: extensive filtering. + unknown-binary: unknown format, remove only carriage returns from AHL. + +The file is read entirely into memory as the WMO standard specifies a maximum message +size of 500,000 bytes with no segmentation and re-assembly being ruled out. + +The output files are named based based on the Abbreviated Header Line (AHL) +from first line of each input file. + +It operates on local files. One subscribes to a source messages that are already +downloaded, then this 'download' filter produces a second tree of converted bulletins. + +STANDALONE DEBUGGING: +Instead of being used purely as a plugin, the script can also be invoked directly. +In that case, if given no arguments, it will read the current working directory, +looking for files that start with a digit (which is what NWS feed provides.) +and feed those file for processing. + +To process a particular files, go into the directory containing the file, and supply +the file names as an arguments. + + +MSC Format description: + +Given a file containing a single WMO 386 / WMO 306 conformant encoded bulletin, +produce an MSC formatted one. Most obvious change is the line termination, but there +are other subtle differences learned through pain, over a year or two of parallel testing. + +The MSC format is simply the output of the (late lamented Tandem computer system +which was the bulletin switch from the 80's until 2007) The internal 'AM' circuits +used by the Meteorological Service of Canada used only line feed for termination, as +is standard in Unix/Linux land, and not two carriage returns followed by a line feed required +by the ancient tomes of the WMO. + +This processing was determined by blackbox reverse engineering for MetPX sundew in the mid-2000's. +when a study was done over a few days in the mid 2000's, it was determined that approximately +6% of traffic on the GTS is carriage returns, which seems sad, but the formats are too entrenched +to be changed. + +Hopefully identical logic has been ported to Sarracenia for use as a sarra plugin in 2017. + +Adaptation of sundew code to sarracenia by Peter Silva - 2017/01 + +""" +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/gather/file.html b/_modules/sarracenia/flowcb/gather/file.html new file mode 100644 index 000000000..ff0651951 --- /dev/null +++ b/_modules/sarracenia/flowcb/gather/file.html @@ -0,0 +1,875 @@ + + + + + + sarracenia.flowcb.gather.file — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.gather.file

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2020
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+import copy
+from base64 import b64decode, b64encode
+from collections import *
+from hashlib import sha512
+import json
+import logging
+from mimetypes import guess_type
+import os
+import os.path
+import random
+from random import choice
+
+import sarracenia
+from sarracenia import *
+
+from sarracenia.featuredetection import features
+
+if features['reassembly']['present']:
+    import sarracenia.blockmanifest
+
+from sarracenia.flowcb import FlowCB
+import sarracenia.identity
+
+
+import stat
+from sys import platform as _platform
+import sys
+import time
+
+if features['watch']['present']:
+    from watchdog.observers import Observer
+    from watchdog.observers.polling import PollingObserver
+    from watchdog.events import PatternMatchingEventHandler
+
+logger = logging.getLogger(__name__)
+
+
+if features['watch']['present']:
+
+[docs] + class SimpleEventHandler(PatternMatchingEventHandler): +
+[docs] + def __init__(self, parent): + self.on_created = parent.on_created + self.on_deleted = parent.on_deleted + self.on_modified = parent.on_modified + self.on_moved = parent.on_moved + super().__init__()
+
+ + + +
+[docs] +class File(FlowCB): + """ + read the file system, create messages for the files you find. + + this is taken from v2's sr_post.py + + FIXME FIXME FIXME + FIXME: the sr_post version would post files on the fly as it was traversing trees. + so it was incremental and had little overhead. This one is does the whole recursion + in one gather. + + It will fail horribly for large trees. Need to re-formulate to replace recursion with interation. + perhaps a good time to use python iterators. + + also should likely switch from listdir to scandir + """ + def on_add(self, event, src, dst): + logger.debug("on_add %s %s %s" % ( event, src, dst ) ) + self.new_events['%s %s' % (src, dst)] = (event, src, dst) + + def on_created(self, event): + # on_created (for SimpleEventHandler) + if event.is_directory: + self.on_add('mkdir', event.src_path, None) + else: + self.on_add('create', event.src_path, None) + + def on_deleted(self, event): + # on_deleted (for SimpleEventHandler) + if event.is_directory: + self.on_add('rmdir', event.src_path, None) + else: + self.on_add('delete', event.src_path, None) + + def on_modified(self, event): + # on_modified (for SimpleEventHandler) + if not event.is_directory: + self.on_add('modify', event.src_path, None) + + def on_moved(self, event): + # on_moved (for SimpleEventHandler) + self.on_add('move', event.src_path, event.dest_path) + +
+[docs] + def __init__(self, options): + """ + """ + + super().__init__(options,logger) + + if not features['watch']['present']: + logger.critical("watchdog module must be installed to watch directories") + + logger.debug("%s used to be overwrite_defaults" % self.o.component) + + self.obs_watched = [] + self.watch_handler = None + self.post_topicPrefix = ["v03"] + + self.inl = OrderedDict() + self.new_events = OrderedDict() + self.left_events = OrderedDict() + + #self.o.blocksize = 200 * 1024 * 1024 + self.o.create_modify = ('create' in self.o.fileEvents) or ( + 'modify' in self.o.fileEvents)
+ + + def post_delete(self, path, key=None, value=None,is_directory=False): + #logger.debug("post_delete %s (%s,%s)" % (path, key, value)) + + msg = sarracenia.Message.fromFileInfo(path, self.o, None) + + msg['fileOp'] = { 'remove':'' } + + if is_directory: + msg['fileOp']['directory'] = '' + + # partstr + partstr = None + + # used when moving a file + if key != None: + msg[key] = value + if key == 'newname' and self.o.post_baseDir: + msg['new_dir'] = os.path.dirname(value) + msg['new_file'] = os.path.basename(value) + msg[key] = value.replace(self.o.post_baseDir, '') + + return [msg] + + def post_file(self, path, lstat, key=None, value=None): + #logger.debug("start %s" % path) + + # check the value of blocksize + + fsiz = lstat.st_size + blksz = self.set_blocksize(self.o.blocksize, fsiz) + + # if we should send the file in parts + + if (blksz > 0 and blksz < fsiz) and os.path.isfile(path): + return self.post_file_in_parts(path, lstat) + + msg = sarracenia.Message.fromFileData(path, self.o, lstat) + + # used when moving a file + if key != None: + if not 'fileOp' in msg: + msg['fileOp'] = { key : value } + else: + msg['fileOp'][key] = value + + if os_stat.S_ISDIR(lstat.st_mode): + return [msg] + + # complete message + if (self.o.post_topicPrefix[0] == 'v03') and self.o.inline: + if fsiz < self.o.inlineByteMax: + + if self.o.inlineEncoding == 'guess': + e = guess_type(path)[0] + binary = not e or not ('text' in e) + else: + binary = (self.o.inlineEncoding == 'text') + + f = open(path, 'rb') + d = f.read() + f.close() + + if binary: + msg["content"] = { + "encoding": "base64", + "value": b64encode(d).decode('utf-8') + } + else: + try: + msg["content"] = { + "encoding": "utf-8", + "value": d.decode('utf-8') + } + except: + msg["content"] = { + "encoding": "base64", + "value": b64encode(d).decode('utf-8') + } + else: + if self.o.inlineOnly: + logger.error('skipping file %s too large (%d bytes > %d bytes max)) for inlining' % \ + ( path, fsiz, self.o.inlineByteMax ) ) + return [] + + return [msg] + + def post_file_in_parts(self, path, lstat): + #logger.info("start %s" % path ) + + msg = sarracenia.Message.fromFileInfo(path, self.o, lstat) + + logger.debug( f"initial msg:{msg}" ) + # check the value of blocksize + + fsiz = lstat.st_size + chunksize = self.set_blocksize(self.o.blocksize, fsiz) + + # count blocks and remainder + + block_count = int(fsiz / chunksize) + remainder = fsiz % chunksize + if remainder > 0: block_count = block_count + 1 + + #logger.debug( f" fiz:{fsiz}, chunksize:{chunksize}, block_count:{block_count}, remainder:{remainder}" ) + + # loop on blocks + + blocks = list(range(0, block_count)) + if self.o.randomize: + random.shuffle(blocks) + #blocks = [8, 3, 1, 2, 9, 6, 0, 7, 4, 5] # Testing + logger.info('Sending partitions in the following order: ' + + str(blocks)) + + msg['blocks'] = { + 'method': 'inplace', + 'size': chunksize, + 'number': -1, + 'manifest': {} + } + logger.debug( f" blocks:{blocks} " ) + + for current_block in blocks: + + # compute block stuff + offset = current_block * chunksize + length = chunksize + + last = current_block == block_count - 1 + + if last and remainder > 0: + length = remainder + + msg['size']=length + + # set partstr + msg.computeIdentity(path, self.o, offset=offset ) + msg['blocks']['manifest'][current_block] = { 'size':length, 'identity': msg['identity']['value'] } + + + if features['reassembly']['present'] and \ + (not hasattr(self.o, 'block_manifest_delete') or not self.o.block_manifest_delete): + with sarracenia.blockmanifest.BlockManifest( path ) as x: + x.set(msg['blocks']) + + messages = [] + for current_block in blocks: + + msg['blocks']['number'] = current_block + msg['size'] = msg['blocks']['manifest'][current_block]['size'] + msg['identity']['value'] = msg['blocks']['manifest'][current_block]['identity'] + + #logger.info( f" size: {msg['size']} blocks: {msg['blocks']}, offset: {offset} identity: {msg['identity']} " ) + + messages.append(copy.deepcopy(msg)) + + return messages + + def post_link(self, path, key='link', value=None): + #logger.debug("post_link %s" % path ) + + msg = sarracenia.Message.fromFileInfo(path, self.o, None) + + # resolve link + + if key == 'link': + value = os.readlink(path) + + # used when moving a file + if not 'fileOp' in msg: + msg['fileOp'] = { key: value } + else: + msg['fileOp'][key] = value + + return [msg] + + def post_move(self, src, dst): + #logger.debug("post_move %s %s" % (src,dst) ) + + # watchdog funny ./ added at end of directory path ... removed + + messages = [] + src = src.replace('/./', '/') + dst = dst.replace('/./', '/') + + if os.path.islink(dst) and self.o.realpathPost: + dst = os.path.realpath(dst) + if sys.platform == 'win32': + dst = dst.replace('\\', '/') + + # file + + if os.path.isfile(dst): + if hasattr(self.o,'v2compatRenameDoublePost') and self.o.v2compatRenameDoublePost: + messages.extend(self.post_delete(src, 'newname', dst)) + messages.extend(self.post_file(dst, sarracenia.stat(dst), 'rename', src)) + return messages + + # link + + if os.path.islink(dst): + if hasattr(self.o,'v2compatRenameDoublePost') and self.o.v2compatRenameDoublePost: + messages.extend(self.post_delete(src, 'newname', dst)) + messages.extend(self.post_link(dst, 'rename', src)) + return messages + + # directory + if os.path.isdir(dst): + for x in os.listdir(dst): + + dst_x = dst + '/' + x + src_x = src + '/' + x + + messages = self.post_move(src_x, dst_x) + + # directory list to delete at end + self.move_dir_lst.append((src, dst)) + + return messages + +
+[docs] + def post1file(self, path, lstat, is_directory=False) -> list: + """ + create the notification message for a single file, based on the lstat metadata. + + when lstat is present it is used to decide whether the file is an ordinary file, a link + or a directory, and the appropriate message is built and returned. + + if the lstat metadata is None, then that signifies a "remove" message to be created. + In the remove case, without the lstat, one needs the is_directory flag to decide whether + it is an ordinary file remove, or a directory remove. is_directory is not used other + than for the remove case. + + The return value is a list that usually contains a single message. It is a list to allow + for if options are combined such that a symbolic link and the realpath it posts to may + involve multiple messages for a single file. Similarly in the multi-block transfer case. + + """ + + messages = [] + + # watchdog funny ./ added at end of directory path ... removed + path = path.replace('/./', '/') + + # always use / as separator for paths being posted. + if os.sep != '/': # windows + path = path.replace(os.sep, '/') + + # path is a link + + if os.path.islink(path): + messages.extend(self.post_link(path)) + + if self.o.follow_symlinks: + link = os.readlink(path) + try: + rpath = os.path.realpath(link) + if sys.platform == 'win32': + rpath = rpath.replace('\\', '/') + + except: + return messages + + lstat = None + if os.path.exists(rpath): + lstat = sarracenia.stat(rpath) + + messages.extend(self.post1file(rpath, lstat)) + + # path deleted + + elif lstat == None: + messages.extend(self.post_delete(path,key=None,value=None,is_directory=is_directory)) + + # path is a file + + elif os.path.isfile(path) or os.path.isdir(path): + messages.extend(self.post_file(path, lstat)) + + return messages
+ + + def post1move(self, src, dst): + #logger.debug("post1move %s %s" % (src,dst) ) + + self.move_dir_lst = [] + + messages = self.post_move(src, dst) + + for tup in self.move_dir_lst: + src, dst = tup + #logger.debug("deleting moved directory %s" % src ) + messages.extend(self.post_delete(src, 'newname', dst)) + + return messages + +
+[docs] + def process_event(self, event, src, dst): + """ + return a tuple: pop? + list of messages. + + + """ + #logger.debug("process_event %s %s %s " % (event,src,dst) ) + + # delete + + if event == 'delete' : + if event in self.o.fileEvents: + return (True, self.post1file(src, None)) + return (True, []) + + if event == 'rmdir' : + if event in self.o.fileEvents: + return (True, self.post1file(src, None, is_directory=True)) + return (True, []) + + # move + + if event == 'move': + if self.o.create_modify: + return (True, self.post1move(src, dst)) + + # create or modify + + # directory : skipped, its content is watched + #if self.o.recursive and os.path.isdir(src): + # dirs = list(map(lambda x: x[1][1], self.inl.items())) + # #logger.debug("skipping directory %s list: %s" % (src, dirs)) + + # link ( os.path.exists = false, lstat = None ) + + if os.path.islink(src): + if 'link' in self.o.fileEvents: + return (True, self.post1file(src, None)) + return (True, []) + + # file : must exists + # (may have been deleted since event caught) + + if not os.path.exists(src): return (True, []) + + # file : must be old enough + + lstat = sarracenia.stat(src) + + if lstat and hasattr(lstat,'st_mtime'): + age = time.time() - lstat.st_mtime + + if age < self.o.fileAgeMin: + logger.debug( "%d vs (inflight setting) %d seconds. Too New!" % (age,self.o.fileAgeMin) ) + return (False, []) + + if self.o.fileAgeMax > 0 and age > self.o.fileAgeMax: + logger.debug("%d vs (fileAgeMax setting) %d seconds. Too Old!" % (age,self.o.fileAgeMax) ) + return (True, []) + + # post it + + if event == 'mkdir' and 'mkdir' in self.o.fileEvents: + return (True, self.post1file(src, lstat, is_directory=True)) + elif self.o.create_modify: + return (True, self.post1file(src, lstat)) + return (True, [])
+ + + def set_blocksize(self, bssetting, fsiz): + + tfactor = 50 * 1024 * 1024 + + if bssetting == 0: ## default blocksize + return tfactor + + elif bssetting == 1: ## send file as one piece. + return fsiz + + else: ## partstr=i + return bssetting + + def wakeup(self): + #logger.debug("wakeup") + + # FIXME: Tiny potential for events to be dropped during copy. + # these lists might need to be replaced with watchdog event queues. + # left for later work. PS-20170105 + # more details: https://github.com/gorakhargosh/watchdog/issues/392 + + # pile up left events to process + + self.left_events.update(self.new_events) + self.new_events = OrderedDict() + + # work with a copy events and keep done events (to delete them) + + self.cur_events = OrderedDict() + self.cur_events.update(self.left_events) + + # loop on all events + + messages = [] + for key in self.cur_events: + event_done=False + event, src, dst = self.cur_events[key] + try: + (event_done, new_messages) = self.process_event(event, src, dst) + messages.extend(new_messages) + except OSError as err: + """ + This message is reduced to debug priority because it often happens when files + are too transitory (they disappear before we have a chance to post them) + not sure if it should be an error message or not. + + """ + logger.debug("skipping event that could not be processed: ({}): {}".format( + event, err)) + logger.debug("Exception details:", exc_info=True) + event_done=True + if event_done: + self.left_events.pop(key) + return messages + +
+[docs] + def walk(self, src): + """ + walk directory tree returning 1 message for each file in it. + """ + logger.debug("walk %s" % src) + + # how to proceed with symlink + + if os.path.islink(src) and self.o.realpathPost: + src = os.path.realpath(src) + if sys.platform == 'win32': + src = src.replace('\\', '/') + + messages = [] + + # need to post root of tree first, so mode bits get propagated on creation. + if src == self.o.post_baseDir : + logger.debug("skip posting of post_baseDir {src}") + else: + messages.extend(self.post1file(src, sarracenia.stat(src), is_directory=True)) + + # walk src directory, this walk is depth first... there could be a lot of time + # between *listdir* run, and when a file is visited, if there are subdirectories before you get there. + # hence the existence check after listdir (crashed in flow_tests of > 20,000) + + if self.o.recursive: + for x in os.listdir(src): + path = src + '/' + x + # add path created + if os.path.isdir(path): + messages.extend(self.walk(path)) + continue + + if os.path.exists(path): + messages.extend(self.post1file(path, sarracenia.stat(path))) + + + return messages
+ + +
+[docs] + def walk_priming(self, p): + """ + Find all the subdirectories of the given path, start watches on them. + deal with symbolically linked directories correctly + """ + if os.path.islink(p): + realp = os.path.realpath(p) + if sys.platform == 'win32': + realp = realp.replace('\\', '/') + + logger.info("sr_watch %s is a link to directory %s" % (p, realp)) + if self.o.realpathPost: + d = realp + else: + d = p + '/' + '.' + else: + d = p + + try: + fs = sarracenia.stat(d) + dir_dev_id = '%s,%s' % (fs.st_dev, fs.st_ino) + if dir_dev_id in self.inl: + return True + except OSError as err: + logger.warning("could not stat file ({}): {}".format(d, err)) + logger.debug("Exception details:", exc_info=True) + + if os.access(d, os.R_OK | os.X_OK): + try: + ow = self.observer.schedule(self.watch_handler, + d, + recursive=True) + self.obs_watched.append(ow) + self.inl[dir_dev_id] = (ow, d) + logger.info( + "sr_watch priming watch (instance=%d) scheduled for: %s " % + (len(self.obs_watched), d)) + except: + logger.warning("sr_watch priming watch: %s failed, deferred." % + d) + logger.debug('Exception details:', exc_info=True) + + # add path created + self.on_add('create', p, None) + return True + + else: + logger.warning( + "sr_watch could not schedule priming watch of: %s (EPERM) deferred." + % d) + logger.debug('Exception details:', exc_info=True) + + # add path created + self.on_add('create', p, None) + return True + + return True
+ + + def watch_dir(self, sld): + logger.debug("watch_dir %s" % sld) + + if not features['watch']['present']: + logger.critical("sr_watch needs the python watchdog library to be installed.") + return [] + + if self.o.force_polling: + logger.info( + "sr_watch polling observer overriding default (slower but more reliable.)" + ) + self.observer = PollingObserver() + else: + logger.info( + "sr_watch optimal observer for platform selected (best when it works)." + ) + self.observer = Observer() + + self.obs_watched = [] + + self.watch_handler = SimpleEventHandler(self) + self.walk_priming(sld) + + logger.info( + "sr_watch priming walk done, but not yet active. Starting...") + self.observer.start() + logger.info("sr_watch now active on %s posting to exchange: %s" % + (sld, self.o.post_exchange)) + + if self.o.post_on_start: + return self.walk(sld) + else: + return [] + + def on_start(self): + self.queued_messages = [] + self.primed = False + +
+[docs] + def gather(self, messageCountMax): + """ + from sr_post.py/run + + FIXME: really bad performance with large trees: It scans an entire tree + before emitting any messages. Need to re-factor with iterator style so produce + result in batch sized chunks incrementally. + """ + #logger.debug("%s run partflg=%s, sum=%s, nodupe_ttl=%s basis=%s pbd=%s" % \ + # ( self.o.component, self.o.partflg, self.o.sumflg, self.o.nodupe_ttl, + # self.o.nodupe_basis, self.o.post_baseDir )) + #logger.debug("%s realpathPost=%s follow_links=%s force_polling=%s batch=%s" % \ + # ( self.o.component, self.o.realpathPost, self.o.follow_symlinks, \ + # self.o.force_polling, self.o.batch ) ) + #logger.info("%s len(self.queued_messages)=%d" % \ + # ( self.o.component, len(self.queued_messages) ) ) + + pbd = self.o.post_baseDir + + if len(self.queued_messages) > self.o.batch: + messages = self.queued_messages[0:self.o.batch] + self.queued_messages = self.queued_messages[self.o.batch:] + return (True, messages) + + elif len(self.queued_messages) > 0: + messages = self.queued_messages + self.queued_messages = [] + + if self.o.sleep < 0: + return (True, messages) + else: + messages = [] + + if self.primed: + return (True, self.wakeup()) + + cwd = os.getcwd() + + for d in self.o.postpath: + + # convert relative path to absolute. + if d[0] != os.sep: d = cwd + os.sep + d + + d=self.o.variableExpansion(d) + logger.debug("postpath = %s" % d) + + if self.o.sleep > 0: + if features['watch']['present']: + messages.extend(self.watch_dir(d)) + else: + logger.critical("python watchdog package missing! Cannot watch directory") + continue + + if os.path.isdir(d): + logger.debug("postpath = %s" % d) + messages.extend(self.walk(d)) + elif os.path.islink(d): + messages.extend(self.post1file(d, None)) + elif os.path.isfile(d): + messages.extend(self.post1file(d, sarracenia.stat(d))) + else: + logger.error("could not post %s (exists %s)" % + (d, os.path.exists(d))) + + if len(messages) > self.o.batch: + self.queued_messages = messages[self.o.batch:] + logger.info("len(queued_messages)=%d" % len(self.queued_messages)) + messages = messages[0:self.o.batch] + + self.primed = True + return (True, messages)
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/gather/message.html b/_modules/sarracenia/flowcb/gather/message.html new file mode 100644 index 000000000..ddf0c5262 --- /dev/null +++ b/_modules/sarracenia/flowcb/gather/message.html @@ -0,0 +1,190 @@ + + + + + + sarracenia.flowcb.gather.message — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.gather.message

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2020
+#
+
+import logging
+
+from sarracenia import naturalSize
+import sarracenia.moth
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Message(FlowCB): + """ + gather messages from a sarracenia.moth message queuing protocol source. + """ +
+[docs] + def __init__(self, options) -> None: + + super().__init__(options,logger) + + self.od = sarracenia.moth.default_options + self.od.update(self.o.dictify()) + + if hasattr(self.o, 'broker') and self.o.broker: + self.consumer = sarracenia.moth.Moth.subFactory(self.od) + else: + logger.critical('missing required broker specification')
+ + +
+[docs] + def gather(self, messageCountMax) -> list: + """ + return: + True ... you can gather from other sources. and: + a list of messages obtained from this source. + """ + if hasattr(self,'consumer') and hasattr(self.consumer,'newMessages'): + return (True, self.consumer.newMessages()) + else: + logger.warning( f'not connected. Trying to connect to {self.o.broker}') + self.consumer = sarracenia.moth.Moth.subFactory(self.od) + return (True, [])
+ + + def ack(self, mlist) -> None: + + if not hasattr(self,'consumer'): + return + + for m in mlist: + # messages being re-downloaded should not be re-acked, but they won't have an ack_id (see issue #466) + self.consumer.ack(m) + + def metricsReport(self) -> dict: + if hasattr(self,'consumer') and hasattr(self.consumer,'metricsReport'): + return self.consumer.metricsReport() + else: + return {} + + def on_housekeeping(self) -> None: + + if not hasattr(self,'consumer'): + return + + if hasattr(self.consumer, 'metricsReport'): + m = self.consumer.metricsReport() + average = (m['rxByteCount'] / + m['rxGoodCount'] if m['rxGoodCount'] != 0 else 0) + logger.info( f"messages: good: {m['rxGoodCount']} bad: {m['rxBadCount']} " +\ + f"bytes: {naturalSize(m['rxByteCount'])} " +\ + f"average: {naturalSize(average)}" ) + self.consumer.metricsReset() + + def on_stop(self) -> None: + if hasattr(self,'consumer') and hasattr(self.consumer, 'close'): + self.consumer.close() + logger.info('closing')
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/housekeeping/resources.html b/_modules/sarracenia/flowcb/housekeeping/resources.html new file mode 100644 index 000000000..10d7e113f --- /dev/null +++ b/_modules/sarracenia/flowcb/housekeeping/resources.html @@ -0,0 +1,257 @@ + + + + + + sarracenia.flowcb.housekeeping.resources — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.housekeeping.resources

+"""
+Default on_housekeeping handler that:
+- Logs Memory and CPU usage.
+- Restarts components to deal with memory leaks.
+
+If `MemoryMax` is not in the config, it is automatically calculated with the following procedure:
+
+1. The plugin processes the first `MemoryBaseLineFile` items to reach a steady state.
+   - Subscribers process messages-in
+   - Posting programs process messages-posted
+2. Set `MemoryMax` threshold to `MemoryMultiplier` * (memory usage at time steady state)
+
+If memory use ever exceeds the `MemoryMax` threshold, then the plugin triggers a restart, reducing memory consumption.
+
+Parameters:
+
+MemoryMax : size (default: none)
+    Hard coded maximum for tolerable memory consumption.
+    Must be suffixed with k/m/g for Kilo/Mega/Giga byte values.
+    If not set then the following options will have an effect:
+
+MemoryBaseLineFile : int, optional (default: 100)
+    How many files to process before measuring to establish the baseline memory usage.
+    (how many files are expected to process before a steady state is reached)
+
+MemoryMultiplier : int, optional (default: 3)
+    How many times past the steady state memory footprint you want to allow the component to grow before restarting.
+    It could be normal for memory usage to grow, especially if plugins store data in memory.
+
+
+Returns:
+    Nothing, restarts components if memory usage is outside of configured thresholds.
+"""
+
+import logging
+
+import os
+from sarracenia.flowcb import FlowCB
+from sarracenia import naturalSize, naturalTime
+from sarracenia.featuredetection import features
+
+if features['process']['present']:
+    import psutil
+
+import sys
+
+logger = logging.getLogger(__name__)
+
+
+[docs] +class Resources(FlowCB): +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + # Set option to neg value to determine if user set in config + self.o.add_option('MemoryMax', 'size', '0') + self.o.add_option('MemoryBaseLineFile', 'count', 100) + self.o.add_option('MemoryMultiplier', 'float', 3) + + self.threshold = None + ''' Per-process maximum memory footprint that is considered too large, forcing a process restart.''' + self.transferCount = 0 + self.msgCount = 0
+ + + def on_housekeeping(self): + if features['process']['present']: + mem = psutil.Process().memory_info().vms + else: + mem = 0 + + ost = os.times() + logger.info(f"Current cpu_times: user={ost.user} system={ost.system}") + + # We must set a threshold **after** the config file has been parsed. + if self.threshold is None: + # If the config set something, use it. + if self.o.MemoryMax != 0: + self.threshold = self.o.MemoryMax + + if self.threshold is None: + # No user input set, now to figure out what our baseline memory usage is at a steady state + # Process MemoryBaseLineFile(s)+ then get a memory reading before setting memory restart threshold. + if (self.transferCount < self.o.MemoryBaseLineFile) and ( + self.msgCount < self.o.MemoryBaseLineFile): + # Not enough files processed for steady state, continue to wait.. + logger.info( + f"Current mem usage: {naturalSize(mem)}, accumulating count " + f"({self.transferCount} or {self.msgCount}/{self.o.MemoryBaseLineFile} so far) " + f"before self-setting threshold") + return True + + self.threshold = int(self.o.MemoryMultiplier * mem) + + logger.info(f"Memory threshold set to: {naturalSize(self.threshold)}") + + logger.info( + f"Current Memory usage: {naturalSize(mem)} / " + f"{naturalSize(self.threshold)} = {(mem/self.threshold):.2%}" + ) + + if mem > self.threshold: + self.restart() + # self.restart() + + return True + +
+[docs] + def restart(self): + """ + Do an in-place restart of the current process (keeps pid). + Gets a new memory stack/heap, keeps all file descriptors but replaces the buffers. + """ + logger.info( + f"Memory threshold surpassed! Triggering a restart for '{sys.argv}' via '{sys.executable}'" + ) + # First arg must be the program to be run (absolute path to program) + # Second arg has to be python for windows, see how this affects the linux side of things.. + # Third arg is the name of the program you wish to run (should be full path to script) plus all the args. + # The star unpacks the sys.argv list into the remaining function args + + if sys.platform.startswith(('linux', 'cygwin', 'darwin', 'aix')): + # Unix* (Linux / Windows/Cygwin / MacOS / AIX) Specific restart + os.execl(sys.executable, sys.executable, *sys.argv) + elif sys.platform.startswith('win32'): + # Windows Specific restart + os.execl(sys.executable, 'python', *sys.argv) + else: + logger.error( + f'Unknown platform type: "{sys.platform}", attempting default unix process restart..' + ) + os.execl(sys.executable, sys.executable, *sys.argv) + + # Scream out in agony and die + logger.critical( + f'Plugin resources.py:restart() "execl" failed, this should never be logged.' + ) + exit(1)
+ + + def after_work(self, worklist): + self.transferCount += len(worklist.ok) + # if self.threshold is not None: + # TODO: Remove this callback when issue #444 is implemented + + def after_accept(self, worklist): + self.msgCount += len(worklist.incoming)
+ + # if self.threshold is not None: + # TODO: Remove this callback when issue #444 is implemented +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/log.html b/_modules/sarracenia/flowcb/log.html new file mode 100644 index 000000000..3df2db9d4 --- /dev/null +++ b/_modules/sarracenia/flowcb/log.html @@ -0,0 +1,375 @@ + + + + + + sarracenia.flowcb.log — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.log

+
+
+import logging
+from sarracenia import nowflt, timeflt2str, timestr2flt, __version__, naturalSize, naturalTime
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Log(FlowCB): + """ + The logging flow callback class. + logs message at the indicated time. Controlled by: + + logEvents - which entry points to emit log messages at. + + logMessageDump - print literal messages when printing log messages. + + every housekeeping interval, prints: + + * how many messages were received, rejected, %accepted. + * number of files transferred, their size, and rate in files/s and bytes/s + * lag: some information about how old the messages are when processed + + """ +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.o.add_option('logEvents', 'set', + ['after_accept', 'on_housekeeping']) + self.o.add_option('logMessageDump', 'flag', False) + logger.info(f'{self.o.component} initialized with: logEvents: {self.o.logEvents}, logMessageDump: {self.o.logMessageDump}') + if self.o.component in ['sender']: + self.action_verb = 'sent' + elif self.o.component in ['subscribe', 'sarra' ]: + self.action_verb = 'downloaded' + elif self.o.component in ['post', 'poll', 'watch']: + self.action_verb = 'noticed' + elif self.o.component in [ 'flow', 'shovel', 'winnow']: + self.action_verb = self.o.component + 'ed' + else: + self.action_verb = 'done' + self.started = nowflt() + + self.rxTopicSeparator='.' + if hasattr(options,'broker') and options.broker and options.broker.url.scheme.startswith('mqtt'): + self.rxTopicSeparator='/' + + self.__reset()
+ + + def __reset(self): + self.last_housekeeping = nowflt() + self.fileBytes = 0 + self.lagTotal = 0 + self.lagMax = 0 + self.msgCount = 0 + self.rejectCount = 0 + self.transferCount = 0 + + def metricsReport(self): + return { 'lagMax': self.lagMax, 'lagTotal':self.lagTotal, 'lagMessageCount':self.msgCount, 'rejectCount':self.rejectCount } + + def gather(self, messageCountMax): + if set(['gather']) & self.o.logEvents: + logger.info( f' messageCountMax: {messageCountMax} ') + + return (True, []) + + def _messageStr(self, msg): + if self.o.logMessageDump: + return msg.dumps() + else: + s = '' + if 'baseUrl' in msg: + s+= msg['baseUrl'] + ' ' + if 'relPath' in msg: + s+= msg['relPath'] + elif 'retrievePath' in msg: + s+= msg['retrievePath'] + else: + s+= 'badMessage' + return s + + def _messageAcceptStr(self,msg): + if self.o.logMessageDump: + return msg.dumps() + + s = " " + if 'exchange' in msg: + s+= f"exchange: {msg['exchange']} " + if 'subtopic' in msg: + s+= f"subtopic: {self.rxTopicSeparator.join(msg['subtopic'])} " + if 'fileOp' in msg: + op=','.join(msg['fileOp'].keys()) + + if op in ['link']: + s+= f"a link to {msg['fileOp']['link']} with baseUrl: {msg['baseUrl']} " + elif op in ['rename']: + s+= f"a rename {msg['fileOp']['rename']} with baseUrl: {msg['baseUrl']} " + else: + s+= f"a {op} with baseUrl: {msg['baseUrl']} " + else: + s+= f"a file with baseUrl: {msg['baseUrl']} " + if 'relPath' in msg: + s+= f"relPath: {msg['relPath']} " + if 'retrievePath' in msg: + s+= f"retrievePath: {msg['retrievePath']} " + if 'rename' in msg: + s+= f"rename: {msg['rename']} " + return s + + def _messagePostStr(self,msg): + if self.o.logMessageDump: + return msg.dumps() + + s = "to " + if 'post_exchange' in msg and ('post_topic' in msg) and \ + not msg['post_topic'].startswith(msg['post_exchange']) : + s+= f"exchange: {msg['post_exchange']} " + if 'post_topic' in msg: + s+= f"topic: {msg['post_topic']} " + + if 'fileOp' in msg: + op=','.join(msg['fileOp'].keys()) + + if op in ['link']: + s+= f"a link to {msg['fileOp']['link']} " + elif op in ['rename']: + s+= f"a rename {msg['fileOp']['rename']} " + else: + s+= f"a {op} " + else: + s+= f"a file " + + if 'baseUrl' in msg: + s+= f"with baseUrl: {msg['baseUrl']} " + + if 'relPath' in msg: + s+= f"relPath: {msg['relPath']} " + if 'retrievePath' in msg: + s+= f"retrievePath: {msg['retrievePath']} " + if 'rename' in msg: + s+= f"rename: {msg['rename']} " + return s + + def after_accept(self, worklist): + + self.rejectCount += len(worklist.rejected) + self.msgCount += len(worklist.incoming) + now = nowflt() + + if set(['reject']) & self.o.logEvents: + for msg in worklist.rejected: + if 'report' in msg: + logger.info( + "%s rejected: %d %s " % + (msg['relPath'], msg['report']['code'], msg['report']['message'])) + else: + logger.info("rejected: %s " % self._messageAcceptStr(msg)) + + for msg in worklist.incoming: + + lag = now - timestr2flt(msg['pubTime']) + self.lagTotal += lag + if lag > self.lagMax: + self.lagMax = lag + if set(['after_accept']) & self.o.logEvents: + logger.info( f"accepted: (lag: {lag:.2f} ) {self._messageStr(msg)}" ) + + def after_post(self, worklist): + if set(['after_post']) & self.o.logEvents: + for msg in worklist.ok: + logger.info("posted %s" % self._messagePostStr(msg)) + for msg in worklist.failed: + logger.info("failed to post, queued to retry %s" % self._messagePostStr(msg)) + + def after_work(self, worklist): + self.rejectCount += len(worklist.rejected) + self.transferCount += len(worklist.ok) + if set(['reject']) & self.o.logEvents: + for msg in worklist.rejected: + if 'report' in msg: + logger.info( + "rejected: %d %s " % + (msg['report']['code'], msg['report']['message'])) + else: + logger.info("rejected: %s " % self._messageStr(msg)) + + for msg in worklist.ok: + if 'size' in msg: + self.fileBytes += msg['size'] + + if not self.o.download: + continue + + if set(['after_work']) & self.o.logEvents: + if 'fileOp' in msg : + if 'link' in msg['fileOp']: + verb = 'linked' + elif 'remove' in msg['fileOp']: + verb = 'removed' + elif 'rename' in msg['fileOp']: + verb = 'renamed' + else: + verb = ','.join(msg['fileOp'].keys()) + elif self.action_verb in ['downloaded'] and 'content' in msg: + verb = 'written from message' + else: + verb = self.action_verb + + if ('new_dir' in msg) and ('new_file' in msg): + logger.info("%s ok: %s " % + (verb, msg['new_dir'] + '/' + msg['new_file'])) + elif 'relPath' in msg: + logger.info("%s ok: relPath: %s " % (verb, msg['relPath'] )) + + if self.o.logMessageDump: + logger.info('message: %s' % msg.dumps()) + + def stats(self): + tot = self.msgCount + self.rejectCount + how_long = nowflt() - self.last_housekeeping + if tot > 0: + apc = 100 * self.msgCount / tot + rate = self.msgCount / how_long + else: + apc = 0 + rate = 0 + + logger.info( + f"version: {__version__}, started: {naturalTime(nowflt()-self.started)}, last_housekeeping: {how_long:4.1f} seconds ago " + ) + logger.info( + "messages received: %d, accepted: %d, rejected: %d rate accepted: %3.1f%% or %3.1f m/s" + % (self.msgCount + self.rejectCount, self.msgCount, + self.rejectCount, apc, rate)) + logger.info( f"files transferred: {self.transferCount} " +\ + f"bytes: {naturalSize(self.fileBytes)} " +\ + f"rate: {naturalSize(self.fileBytes/how_long)}/sec" ) + if self.msgCount > 0: + logger.info("lag: average: %.2f, maximum: %.2f " % + (self.lagTotal / self.msgCount, self.lagMax)) + + def on_cleanup(self): + logger.info("hello") + + def on_declare(self): + logger.info("hello") + + def on_stop(self): + if set(['on_stop']) & self.o.logEvents: + self.stats() + logger.info("stopping") + + def on_start(self): + if set(['on_start']) & self.o.logEvents: + self.stats() + logger.info("starting") + + def on_housekeeping(self): + if set(['on_housekeeping']) & self.o.logEvents: + self.stats() + logger.debug("housekeeping") + self.__reset()
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/mdelaylatest.html b/_modules/sarracenia/flowcb/mdelaylatest.html new file mode 100644 index 000000000..25e46f401 --- /dev/null +++ b/_modules/sarracenia/flowcb/mdelaylatest.html @@ -0,0 +1,214 @@ + + + + + + sarracenia.flowcb.mdelaylatest — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.mdelaylatest

+
+import logging
+
+from sarracenia.flowcb import FlowCB
+from sarracenia import timestr2flt, nowflt
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class MDelayLatest(FlowCB): + """ + This plugin delays processing of messages by *message_delay* seconds + If multiple versions of a file are published within the interval, only the latest one + will be published. + + + mdelay 30 + flowcb sarracenia.flowcb.mdelaylatest.MDelayLatest + + every message will be at least 30 seconds old before it is forwarded by this plugin. + In the meantime, the message is placed on the retry queue by marking it as failed. + + """ +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.stop_requested = False + self.suppressions = 0 + self.ok_delay = [] + logging.basicConfig(format=self.o.logFormat, + level=getattr(logging, self.o.logLevel.upper())) + + self.o.add_option('mdelay', 'duration', 30) + + logger.info(f'mdelay set to {self.o.mdelay}')
+ + + def after_accept(self, worklist): + + # Check message in ok list + # get time at beginning of loop, less system calls. + now = nowflt() + + new_incoming = [] + for m1 in worklist.incoming: + #logger.info('1 relPath=%s' % m1['relPath']) + #logger.info('1 pubTime=%s' % m1['pubTime']) + elapsedtime = now - timestr2flt(m1['pubTime']) + #logger.info('1 Time=%s' % str(elapsedtime)) + wait = False + + # If same message found in the delay list, replaced it with the one in ok list. + new_ok_delay = [] + for m2 in self.ok_delay: + if m1['relPath'] == m2['relPath']: + logger.info( + f"intermediate version suppressed: {m1['relPath']}") + self.suppressions += 1 + new_ok_delay.append(m1) + worklist.rejected.append(m2) + wait = True + else: + new_ok_delay.append(m2) + #new_incoming.append(m1) + self.ok_delay = new_ok_delay + + # If it's new, put it in delay list too. + # else if replacing a message in delay, do nothing + # else put it back to incoming + if not wait and elapsedtime < self.o.mdelay: + self.ok_delay.append(m1) + elif wait: + pass + else: + new_incoming.append(m1) + + worklist.incoming = new_incoming + + # Check message in the delay list + new_ok_delay = [] + for m1 in self.ok_delay: + #logger.info('2 relPath=%s' % m1['relPath']) + #logger.info('2 pubTime=%s' % m1['pubTime']) + elapsedtime = nowflt() - timestr2flt(m1['pubTime']) + #logger.info('Time=%s' % str(elapsedtime)) + # if it's time, the message is putting back to the ok list to publish + if elapsedtime >= self.o.mdelay: + #logger.info('OK') + worklist.incoming.append(m1) + else: + new_ok_delay.append(m1) + self.ok_delay = new_ok_delay + + if self.stop_requested: + worklist.incoming.extend(self.ok_delay) + self.ok_delay = [] + logger.info('stop requested, ok_delay queue drained') + + def on_housekeeping(self): + logger.info( + f'suppressions={self.suppressions} currently delay queue length:{len(self.ok_delay)}' + ) + self.suppressions = 0
+ + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/nodupe.html b/_modules/sarracenia/flowcb/nodupe.html new file mode 100644 index 000000000..13a70f1c8 --- /dev/null +++ b/_modules/sarracenia/flowcb/nodupe.html @@ -0,0 +1,164 @@ + + + + + + sarracenia.flowcb.nodupe — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.nodupe

+
+
+import logging
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class NoDupe(FlowCB): + """ + duplicate suppression family of modules. + + invoked with: + + callback sarracenia.flowcb.nodupe.disk + + or: + callback sarracenia.flowcb.nodupe.redis + + with default being loaded depdending on the presence of a + + nodupe_driver "redis" + + setting (defaults to disk.) + + """ + + + def deriveKey(self, msg) -> str: + + key=None + if ('nodupe_override' in msg) and ('key' in msg['nodupe_override']): + key = msg['nodupe_override']['key'] + elif 'fileOp' in msg : + if 'link' in msg['fileOp']: + key = msg['fileOp']['link'] + elif 'directory' in msg['fileOp']: + if 'remove' not in msg['fileOp']: + key = msg['relPath'] + elif ('identity' in msg) and not (msg['identity']['method'] in ['cod']): + key = msg['identity']['method'] + ',' + msg['identity']['value'].replace('\n', '') + + if not key: + if 'mtime' in msg: + t = msg['mtime'] + else: + t = msg['pubTime'] + if 'size' in msg: + key = f"{msg['relPath']},{t},{msg['size']}" + else: + key = f"{msg['relPath']},{t}" + + return key
+ + + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/nodupe/data.html b/_modules/sarracenia/flowcb/nodupe/data.html new file mode 100644 index 000000000..1ec33749e --- /dev/null +++ b/_modules/sarracenia/flowcb/nodupe/data.html @@ -0,0 +1,127 @@ + + + + + + sarracenia.flowcb.nodupe.data — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.nodupe.data

+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Data(FlowCB): + """ + duplicate suppression based on data alone. Overrides the path used for lookups + in the cache so that all files have the same name, and so if the checksum + is the same, regardless of file name, it is considered a duplicate. + """ + def after_accept(self, worklist): + for m in worklist.incoming: + if not 'nodupe_override' in m: + m['_deleteOnPost'] |= set(['nodupe_override']) + m['nodupe_override'] = {} + m['nodupe_override']['path'] = 'data'
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/nodupe/name.html b/_modules/sarracenia/flowcb/nodupe/name.html new file mode 100644 index 000000000..502c1e64d --- /dev/null +++ b/_modules/sarracenia/flowcb/nodupe/name.html @@ -0,0 +1,130 @@ + + + + + + sarracenia.flowcb.nodupe.name — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.nodupe.name

+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Name(FlowCB): + """ + Override the the comparison so that files with the same name, + regardless of what directory they are in, are considered the same. + This is useful when receiving data from two different sources (two different trees) + and winnowing between them. + """ + def after_accept(self, worklist): + for m in worklist.incoming: + if not 'nodupe_override' in m: + m['_deleteOnPost'] |= set(['nodupe_override']) + m['nodupe_override'] = {} + + m['nodupe_override']['path'] = m['relPath'].split('/')[-1] + m['nodupe_override']['key'] = m['relPath'].split('/')[-1]
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/pclean.html b/_modules/sarracenia/flowcb/pclean.html new file mode 100644 index 000000000..e0af07c61 --- /dev/null +++ b/_modules/sarracenia/flowcb/pclean.html @@ -0,0 +1,185 @@ + + + + + + sarracenia.flowcb.pclean — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.pclean

+""" msg_pclean module: base module for propagation tests and cleanup for Sarracenia components (in flow test)
+    Used by https://github.com/MetPX/sr_insects test suite for CI/CD.
+"""
+
+import logging
+
+from sarracenia import timestr2flt, nowflt
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PClean(FlowCB): + """ Base plugin class that is used in shovel pclean_f9x: + + - it checks if the propagation was ok. + - it randomly set a test in the watch f40.conf for propagation + - it posts the product again (more test in shovel clean_f91) which is propagated too + - it remove the original product + + It also uses a file delay to tolerate a maximum lag for the test + + The posted message contains a tag in the header for the test performed which is the extension used for the test + """ +
+[docs] + def __init__(self, options): + super().__init__(options,logger) + self.test_extension_list = ['.slink', '.hlink', '.moved'] + self.ext_key = 'pclean_ext' + self.ext_count = 0 + self.all_fxx_dirs = [ + '', # sarra f20 + 'downloaded_by_sub_amqp', # subscribe amqp f30 + 'downloaded_by_sub_rabbitmqtt', # subscribe mqtt f30 + # f40 is watch... no file + 'sent_by_tsource2send', # sender f50 + 'downloaded_by_sub_u', # subscribe sub_u f60 + 'downloaded_by_sub_cp', # subscribe sub_cp f61 + 'posted_by_shim', # shim f63 + 'posted_by_srpost_test2', # subscribe ftp_f70 + 'recd_by_srpoll_test1' + ] # subscribe q_f71
+ + +
+[docs] + def build_path_dict(self, fxx_dirs, relpath, ext=''): + """ This build paths necessary to pclean tests + + It is a subset of all flow test path based on fxx download directory provided. + + :param root: usually the sarra dev doc root directory + :param fxx_dirs: a list of the flow test directory needed + :param relpath: the relative path of the file (starting with the date) without the forward slash + :param ext: the extension from the extension test (optional) + :return: a dictionnary of all paths built + """ + results = {} + for fxx_dir in fxx_dirs: + results["{}{}".format(fxx_dir, ext)] = relpath.replace( + self.all_fxx_dirs[1], fxx_dir) + return results
+ + +
+[docs] + def get_extension(self, relpath): + """ Check whether the extension is in the header + + :param msg: the msg used for the test + :return: the value corresponding to the extension key in the msg header + """ + from pathlib import Path + + return Path(relpath).suffix
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll.html b/_modules/sarracenia/flowcb/poll.html new file mode 100644 index 000000000..2b577c1dc --- /dev/null +++ b/_modules/sarracenia/flowcb/poll.html @@ -0,0 +1,712 @@ + + + + + + sarracenia.flowcb.poll — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll

+#
+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2022
+#
+
+import sarracenia.moth
+import copy
+
+
+import datetime
+import html.parser
+import logging
+import os
+import paramiko
+
+import sarracenia
+from sarracenia.featuredetection import features
+
+if features['ftppoll']['present']:
+    import dateparser
+    import pytz
+
+import sarracenia.config
+from sarracenia.flowcb import FlowCB
+import sarracenia.transfer
+import stat
+import sys, time
+
+logger = logging.getLogger(__name__)
+
+
+def file_size_fix(str_value) -> int:
+    try:
+
+        factor = 1
+        if str_value[-1] in 'bB': str_value = str_value[:-1]
+        elif str_value[-1] in 'kK': factor = 1024
+        elif str_value[-1] in 'mM': factor = 1024 * 1024
+        elif str_value[-1] in 'gG': factor = 1024 * 1024 * 1024
+        elif str_value[-1] in 'tT': factor = 1024 * 1024 * 1024 * 1024
+        if str_value[-1].isalpha(): str_value = str_value[:-1]
+
+        fsize = float(str_value) * factor
+        isize = int(fsize)
+
+    except:
+        logger.debug("bad size %s" % str_value)
+        return -1
+
+    return isize
+
+
+file_type_dict = {
+    'l': 0o120000,  # symbolic link
+    's': 0o140000,  # socket file
+    '-': 0o100000,  # regular file
+    'b': 0o060000,  # block device
+    'd': 0o040000,  # directory
+    'c': 0o020000,  # character device
+    'p': 0o010000  # fifo (named pipe)
+}
+
+
+def modstr2num(m) -> int:
+    mode = 0
+    if (m[0] == 'r'): mode += 4
+    if (m[1] == 'w'): mode += 2
+    if (m[2] == 'x'): mode += 1
+    return mode
+
+
+def filemode(self, modstr) -> int:
+    mode = 0
+    mode += file_type_dict[modstr[0]]
+    mode += modstr2num(modstr[1:4]) << 6
+    mode += modstr2num(modstr[4:7]) << 3
+    mode += modstr2num(modstr[7:10])
+    return mode
+
+
+def fileid(self, id) -> int:
+    if id.isnumeric():
+        return int(id)
+    else:
+        return None
+
+
+
+[docs] +class Poll(FlowCB): + """ + The Poll flow callback class implements the main logic for polling remote resources. + the *poll* routine returns a list of messages for new files to be filtered. + + when instantiated with options, the options honoured include: + + * pollUrl - the URL of the server to be polled. + + * post_baseURL - parameter for messages to be returned. Also used to look + up credentials to help subscribers with retrieval. + + * masks - These are the directories at the pollUrl to poll. + derived from the accept/reject clauses, but filtering should happen later. + entire directories are listed at this point. + + * timezone - interpret listings from an FTP server as being in the given timezone + (as per `pytz <pypi.org/project/pytz>`_ + + * chmod - used to identify the minimum permissions to accept for a file to + be included in a polling result. + + * identity_method - parameter for how to build identity checksum for messages. + as these are usually remote files, the default is typically "cod" (calculate on download) + + * rename - parameter used to to put in messages built to specify the rename field contents. + + * options are passed to sarracenia.Transfer classes for their use as well. + + Poll uses sarracenia.transfer (ftp, sftp, https, etc... )classes to + requests lists of files using those protocols using built-in logic. + + Internally, Poll normalizes the listings received by placing them into paramiko.SFTPAttributes + metadata records (similar to stat records) and builds a Sarracenia.Message from them. + The *poll* routine does one pass of this, returning a list of Sarracenia.Messages. + + To customize: + + * one can add new sarracenia.transfer protocols, each implementing the *ls* entry point + to be compatible with this polling routine, ideally the entry point would return a + list of paramiko.SFTPAttributes for each file in a directory listing. + This can be used to implement polling of structured remote resources such as S3 or webdav. + + * one can deal with different formats of HTTP pages by overriding the handle_data entry point, + as done in `nasa_mls_nrt.py <nasa_mls_nrt.py>`_ plugin + + * for traditional file servers, the listing format should be decypherable with the built-in processing. + + * sftp file servers provide paramiko.SFTPAttributes naturally which are timezone agnostic. + + * for some FTP servers, one may need to specify the *timezone* option to override the UTC default. + + * If there are problems with date or line formats, one can sub-class poll, and override only the on_line + routine to deal with that. + + + """ + def handle_starttag(self, tag, attrs): + if tag == "table": + self.tabular_format=True + elif tag == "tr": + self.table_column=0 + elif tag == "td": + self.table_column +=1 + else: + for attr in attrs: + c, n = attr + if c == "href": + self.myfname = n.strip().strip('\t') + +
+[docs] + def handle_data(self, data): + """ + routine called from html.parser to deal with a single line. + if the line is about a file, then create a new entry for it + with a metadata available from SFTPAttributes. + + example lines: + + from hpfx.collab.science.gc.ca: + 20230113T00Z_MSC_REPS_HGT_ISBL-0850_RLatLon0.09x0.09_PT000H.grib2 2023-01-13 03:49 5.2M + from https://data.cosmic.ucar.edu/suominet/nrt/ncConus/y2023/ + CsuPWVh_2023.011.22.00.0060_nc 11-Jan-2023 23:58 47K + + this can be overridden by subclassing to deal with new web sites. + + Other web servers put their file indices in a tabular format, where there is a number + of cells per row: + <tr><td></td><td href=filename>filename</td><td>yyyy-mm-dd hh:mm</td><td>size</td> + This handle_data supports both formats... + the tabular format is provided by a vanilla apache2 on a debian derived system. + + """ + logger.debug( f"handling_data {data} column={self.table_column}" ) + + if self.tabular_format: + if self.table_column == 2: + self.myfname=data + return + elif self.table_column != 3: + return + sdate=data.strip() + else: + if self.myfname == None: return + if self.myfname == data: return + + words = data.split() + + if len(words) != 3: + self.myfname = None + return + + sdate = words[0] + ' ' + words[1] + + if len(sdate) < 10: + return + + entry = paramiko.SFTPAttributes() + + t=None + for f in [ '%d-%b-%Y %H:%M', '%Y-%m-%d %H:%M' ]: + logger.debug( f" try parsing +{sdate}+ using {f}" ) + try: + t = time.strptime(sdate, f) + break + except Exception as Ex: + pass + + if t: + mydate = time.strftime('%b %d %H:%M', t) + entry.st_mtime = time.mktime(t) + + # size is rounded, need a way to be more precise. + #entry.st_size = file_size_fix(words[-1]) + + if self.myfname[-1] != '/': + entry.st_mode = 0o755 + else: + entry.st_mode = stat.S_IFDIR | 0o755 + + self.entries[self.myfname] = entry + self.myfname = None
+ + +
+[docs] + def on_html_page(self, data) -> dict: + """ + called once per directory or page of HTML, invokes html.parser, returns + a dictionary of file entries. + """ + self.entries = {} + self.myfname = None + self.tabular_format=False + self.table_column=0 + + self.parser.feed(data) + self.parser.close() + + return self.entries
+ + + def on_html_parser_init(self): + # HTML Parsing stuff. + self.parser = html.parser.HTMLParser() + self.parser.handle_starttag = self.handle_starttag + self.parser.handle_data = self.handle_data + + """ + HTML Parsing begine + + """ + +
+[docs] + def __init__(self, options,class_logger=None): + + super().__init__(options,class_logger) + + # check pollUrl + + self.details = None + if self.o.pollUrl is not None: + ok, self.details = sarracenia.config.Config.credentials.get( + self.o.pollUrl) + + if self.o.pollUrl is None or self.details == None: + logger.error("pollUrl option incorrect or missing\n") + sys.exit(1) + + if self.o.post_baseUrl is None: + self.o.post_baseUrl = self.details.url.geturl() + if self.o.post_baseUrl[-1] != '/': self.o.post_baseUrl += '/' + if self.o.post_baseUrl.startswith('file:'): + self.o.post_baseUrl = 'file:' + if self.details.url.password: + self.o.post_baseUrl = self.o.post_baseUrl.replace( + ':' + self.details.url.password, '') + + self.o.sendTo = self.o.pollUrl + + self.dest = sarracenia.transfer.Transfer.factory( + self.details.url.scheme, self.o) + + if self.dest is None: + logger.critical("unsupported polling protocol") + + # rebuild mask as pulls instructions + # pulls[directory] = [mask1,mask2...] + + #self.pulls = {} + #for mask in self.o.masks: + # pattern, maskDir, maskFileOption, mask_regexp, accepting, mirror, strip, pstrip, flatten = mask + # logger.debug(mask) + # if not maskDir in self.pulls: + # self.pulls[maskDir] = [] + # self.pulls[maskDir].append(mask) + + self.metricsReset() + self.on_html_parser_init()
+ + + def metricsReset(self) -> None: + self.metrics = { 'transferRxBytes': 0 } + + def metricsReport(self) -> dict: + return self.metrics + + def cd(self, path): + try: + self.dest.cd(path) + return True + except: + logger.warning("sr_poll/cd: could not cd to directory %s" % path) + return False + + def filedate(self, line): + + if not features['ftppoll']['present']: + logger.error('need dateparser library to deal with polling of ftp servers, no date parsed') + return 0 + + line_split = line.split() + file_date = line_split[5] + " " + line_split[6] + " " + line_split[7] + current_date = datetime.datetime.now(pytz.utc) + # case 1: the date contains '-' implies the date is in 1 string not 3 seperate ones, and H:M is also provided + if "-" in file_date: file_date = line_split[5] + " " + line_split[6] + standard_date_format = dateparser.parse( + file_date, + settings={ + 'RELATIVE_BASE': datetime.datetime(current_date.year, 1, 1), + 'TIMEZONE': self.o.timezone, #turn this into an option - should be EST for mtl + 'TO_TIMEZONE': 'UTC' + }) + if standard_date_format is not None: + # case 2: the year was not given, it is defaulted to 1900. Must find which year (this one or last one). + if standard_date_format.month - current_date.month >= 6: + standard_date_format = standard_date_format.replace( + year=(current_date.year - 1)) + timestamp = datetime.datetime.timestamp(standard_date_format) + return timestamp + +
+[docs] + def on_line(self, line) -> paramiko.SFTPAttributes: + """ + default line processing, converts a file listing into an SFTPAttributes. + does nothing if input is already an SFTPAttributes item, returning it unchanged. + verifies that file is accessible (based on self.o.permDefault pattern to establish minimum permissions.) + """ + if type(line) is paramiko.SFTPAttributes: + sftp_obj = line + elif type(line) is str and len(line.split()) < 7: + # assume windows... + parts = line.split() + sftp_obj = paramiko.SFTPAttributes() + ldate = dateparser.parse( ' '.join(parts[0:2]), settings={ 'TIMEZONE': self.o.timezone, 'TO_TIMEZONE':'UTC' } ) + sftp_obj.st_mtime = ldate.timestamp() + sftp_obj.st_size = file_size_fix(parts[2]) + sftp_obj.longname = ' '.join(line[3:]) + sftp_obj.st_mode = 0o644 # just make it work... no permission info provided. + #logger.info( f"windows line parsing result: {sftp_obj}") + elif type(line) is str and len(line.split()) > 7: + + parts = line.split() + sftp_obj = paramiko.SFTPAttributes() + sftp_obj.st_mode = filemode(self,parts[0]) + sftp_obj.st_uid = fileid(self,parts[2]) + sftp_obj.st_gid = fileid(self,parts[3]) + + if file_size_fix(parts[4]) >= 0: # normal linux/unix ftp server case. + sftp_obj.st_size = file_size_fix(parts[4]) + sftp_obj.filename = line[8:] + sftp_obj.st_mtime = self.filedate(line) + else: # university of wisconsin (some special file system? has third ownship field before size) + sftp_obj.st_size = file_size_fix(parts[5]) + sftp_obj.filename = line[9:] + sftp_obj.st_mtime = self.filedate(line[1:]) + + sftp_obj.longname = sftp_obj.filename + + + # assert at this point we have an sftp_obj... + # filter out files we don't have the necessary permissions for. + if 'sftp_obj' in locals() and ((sftp_obj.st_mode + & self.o.permDefault) == self.o.permDefault): + return sftp_obj + else: + return None
+ + + def lsdir(self): + + try: + ls = self.dest.ls() + + if type(ls) is bytes: + self.metrics["transferRxBytes"] += len(ls) + ls = self.on_html_page(ls.decode('utf-8')) + + new_ls = {} + new_dir = {} + # del ls[''] # For some reason with FTP the first line of the ls causes an index out of bounds error becuase it contains only "total ..." in line_mode.py + + # apply selection on the list + + for f in ls: + logger.debug( f"line to parse: {f}" ) + matched = False + line = ls[f] + + line = self.on_line(line) + if (line is None) or (line == ""): + continue + if stat.S_ISDIR(line.st_mode): + new_dir[f] = line + else: + new_ls[f] = line + + return True, new_ls, new_dir + except Exception as e: + logger.warning("dest.lsdir: Could not ls directory") + logger.debug("Exception details:", exc_info=True) + + return False, {}, {} + + def poll_directory(self, pdir): + + #logger.debug("poll_directory %s %s" % (pdir)) + msgs = [] + + # cd to that directory + logger.debug(" cd %s" % pdir) + ok = self.cd(pdir) + if not ok: return [] + + # ls that directory + + ok, file_dict, dir_dict = self.lsdir() + if not ok: return [] + + filelst = file_dict.keys() + desclst = file_dict + + logger.debug("poll_directory: new files found %d" % len(filelst)) + + # post poll list + + msgs.extend(self.poll_list_post(pdir, dir_dict, dir_dict.keys())) + + msgs.extend(self.poll_list_post(pdir, desclst, filelst)) + + # poll in children directory + + sdir = sorted(dir_dict.keys()) + for d in sdir: + if d == '.' or d == '..': continue + + #d_lspath = lspath + '_' + d + d_pdir = pdir + os.sep + d + + msgs.extend(self.poll_directory(d_pdir)) + + return msgs + + def poll_file_post(self, desc, destDir, remote_file): + + path = destDir + '/' + remote_file + + # posting a localfile + if self.o.post_baseUrl.startswith('file:'): + if os.path.isfile(path) or os.path.islink(path): + try: + lstat = sarracenia.stat(path) + except: + lstat = None + + ok = sarracenia.Message.fromFileInfo(path, self.o, lstat) + if os.path.islink(path): + if 'size' in msg: + del msg['size'] + if not self.o.follow_symlinks: + try: + ok['fileOp'] = { 'link': os.readlink(path) } + if 'Identity' in msg: + del ok['Identity'] + except: + logger.error("cannot read link %s message dropped" % path) + logger.debug('Exception details: ', exc_info=True) + ok=None + return ok + + post_relPath = destDir + '/' + remote_file + + logger.debug('desc: type: %s, value: %s' % (type(desc), desc)) + + if type(desc) == str: + line = desc.split() + st = paramiko.SFTPAttributes() + st.st_size = file_size_fix(line[4]) + # actionally only need to convert normalized time to number here... + # just being lazy... + lstime = dateparser.parse(line[5] + " " + line[6]).timestamp() + st.st_mtime = lstime + st.st_atime = lstime + + desc = st + + msg = sarracenia.Message.fromFileInfo(post_relPath, self.o, desc) + + if stat.S_ISDIR(desc.st_mode): + if 'mkdir' not in self.o.fileEvents: + return None + + msg['fileOp'] = { 'directory':'' } + + elif stat.S_ISLNK(desc.st_mode): + if 'link' not in self.o.fileEvents: + return None + + if not self.o.follow_symlinks: + try: + msg['fileOp'] = { 'link': self.dest.readlink(path) } + except: + logger.error("cannot read link %s message dropped" % post_relPath) + logger.debug('Exception details: ', exc_info=True) + return None + + if 'create' not in self.o.fileEvents and 'modify' not in self.o.fileEvents: + return None + + if self.o.identity_method and (',' in self.o.identity_method): + m, v = self.o.identity_method.split(',') + msg['identity'] = {'method': m, 'value': v} + + # If there is a file operation, and it isn't a rename, then some fields are irrelevant/wrong. + if 'fileOp' in msg and 'rename' not in msg['fileOp']: + if 'identity' in msg: + del msg['identity'] + if 'size' in msg: + del msg['size'] + + return [msg] + + def poll_list_post(self, destDir, desclst, filelst): + + n = 0 + msgs = [] + + for idx, remote_file in enumerate(filelst): + desc = desclst[remote_file] + + new_msgs = self.poll_file_post(desc, destDir, remote_file) + if new_msgs: + msgs.extend(new_msgs) + return msgs + + # ============= + # for all directories, get urls to post + # if True is returned it means : no sleep, retry on return + # False means, go to sleep and retry after sleep seconds + # ============= + + def poll(self) -> list: + + msgs = [] + + try: + self.dest.connect() + except: + # connection did not work + logger.error("sr_poll/post_new_url: unable to connect to %s" % + self.o.pollUrl) + logger.debug('Exception details: ', exc_info=True) + nap=15 + logger.error("Sleeping {nap} secs and retry") + time.sleep(nap) + return [] + + for destDir in self.o.path: + + currentDir = self.o.variableExpansion(destDir) + + if currentDir == '': currentDir = destDir + msgs.extend(self.poll_directory(currentDir)) + logger.debug('poll_directory returned: %s' % len(msgs)) + + # close connection + + try: + self.dest.close() + except: + pass + + return msgs
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll/airnow.html b/_modules/sarracenia/flowcb/poll/airnow.html new file mode 100644 index 000000000..f72c75bc7 --- /dev/null +++ b/_modules/sarracenia/flowcb/poll/airnow.html @@ -0,0 +1,173 @@ + + + + + + sarracenia.flowcb.poll.airnow — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll.airnow

+"""
+Posts updated files of airnowtech. 
+Compatible with Python 3.5+.
+
+usage:
+	in an sr3 poll configuration file:
+
+	pollUrl http://files.airnowtech.org/?prefix=airnow/today/
+
+	callback airnow
+
+STATUS: unknown... need some authentication, or perhaps the method has changed.
+        does not seem to work out of the box.
+"""
+
+import datetime
+import logging
+import paramiko
+import requests
+import sarracenia
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Airnow(FlowCB): + + def poll(self): + + sleep = self.o.scheduled_interval + + gathered_messages = [] + for Hours in range(1, 3): + last_hour_date_time = datetime.datetime.now() - datetime.timedelta( + hours=Hours) + Filename = 'HourlyData_%s.dat' % last_hour_date_time.strftime( + '%Y%m%d%H') + logger.debug("poll_airnow_http Filename: %s" % Filename) + URL = self.o.pollUrl + '/' + Filename + logger.info('INFO %s ' % URL) + #resp = requests.get(self.o.pollUrl + '/' + Filename) + resp = requests.get(URL) + if resp.ok: + mtime = datetime.datetime.strptime(resp.headers['last-modified'],\ + '%a, %d %b %Y %H:%M:%S %Z') + last_poll = datetime.datetime.utcnow() + datetime.timedelta( + seconds=-sleep) + logger.info(mtime) + logger.info(last_poll) + + fakeStat = paramiko.SFTPAttributes() + fakeStat.st_size = int(resp.headers['content-length']) + + # convert datetime to numeric timestamp from beginning of POSIX epoch. + fakeStat.st_mtime = mtime.timestamp() + fakeStat.st_atime = mtime.timestamp() + fakeStat.st_mode = 0o644 + + m = sarracenia.Message.fromFileInfo(Filename, self.o, fakeStat) + gathered_messages.append(m) + + logger.info('mtime: %s last_pollL %s' % (mtime, last_poll)) + + return gathered_messages
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll/mail.html b/_modules/sarracenia/flowcb/poll/mail.html new file mode 100644 index 000000000..11e241824 --- /dev/null +++ b/_modules/sarracenia/flowcb/poll/mail.html @@ -0,0 +1,272 @@ + + + + + + sarracenia.flowcb.poll.mail — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll.mail

+"""
+Posts any new emails from an email server, connected to using 
+the specified protocol, either pop3 or imap. The imaplib/poplib 
+implementations in Python use the most secure SSL settings by 
+default: PROTOCOL_TLS, OP_NO_SSLv2, and OP_NO_SSLv3.
+Compatible with Python 2.7+.
+
+A sample do_poll option for sr_poll.
+connects to an email server with the provided
+credentials and posts all new messages by their msg ID.
+
+usage:
+        in an sr_poll configuration file:
+
+        pollUrl [imap|imaps|pop|pops]://[user[:password]@]host[:port]/
+
+        IMAP over SSL uses 993, POP3 over SSL uses 995
+        IMAP unsecured uses 143, POP3 unsecured uses 110
+
+        Full credentials must be in credentials.conf.
+        If port is not specified it'll default to the ones above based on protocol/ssl setting.
+
+This posts what messages are available. A separate component is needed to 
+download the message, which would need:
+
+     callback download.mail_ingest
+ 
+to process these posts.
+
+
+"""
+
+import datetime
+import email
+import imaplib
+import logging
+import poplib
+import sarracenia
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Mail(Poll): +
+[docs] + def __init__(self, options): + + self.o = options + logger.info("poll_email_ingest init")
+ + + def poll(self): + + logger.debug("start") + + ok, details = self.o.credentials.get(self.o.pollUrl) + if ok: + setting = details.url + user = setting.username + password = setting.password + server = setting.hostname + protocol = setting.scheme.lower() + port = setting.port + logger.debug("pollUrl valid") + else: + logger.error("pollUrl: invalid credentials") + return + + if not port: + if protocol == "imaps": + port = 993 + elif protocol == "pops": + port = 995 + elif protocol == "imap": + port = 143 + else: + port = 110 + + gathered_messages = [] + if "imap" in protocol: + if protocol == "imaps": + try: + mailman = imaplib.IMAP4_SSL(server, port=port) + mailman.login(user, password) + except imaplib.IMAP4.error as e: + logger.error( + "poll_email_ingest imaplib connection error: {}". + format(e)) + return + + elif protocol == "imap": + try: + mailman = imaplib.IMAP4(server, port=port) + mailman.login(user, password) + except imaplib.IMAP4.error as e: + logger.error( + "poll_email_ingest imaplib connection error: {}". + format(e)) + return + else: + return + # only retrieves unread mail from inbox, change these values as to your preference + mailman.select(mailbox='INBOX') + resp, data = mailman.search(None, '(UNSEEN)') + self.metrics['transferRxBytes'] += len(data) + for index in data[0].split(): + r, d = mailman.fetch(index, '(RFC822)') + msg = d[0][1].decode("utf-8", "ignore") + "\n" + msg_subject = email.message_from_string(msg).get('Subject') + msg_filename = msg_subject + datetime.datetime.now().strftime( + '%Y%m%d_%H%M%s_%f') + m = sarracenia.Message.fromFileInfo(msg_filename, self.o) + gathered_messages.append(m) + + mailman.close() + mailman.logout() + + elif "pop" in protocol: + if protocol == "pops": + try: + mailman = poplib.POP3_SSL(server, port=port) + mailman.user(user) + mailman.pass_(password) + logger.debug("poll_email_ingest connection started") + except poplib.error_proto as e: + logger.error( + "poll_email_ingest pop3 connection error: {}".format( + e)) + return + + elif protocol == "pop": + try: + mailman = poplib.POP3(server, port=port) + mailman.user(user) + mailman.pass_(password) + except poplib.error_proto as e: + logger.error( + "poll_email_ingest pop3 connection error: {}".format( + e)) + return + else: + return + # only retrieves msgs that haven't triggered internal pop3 'read' flag + numMsgs = len(mailman.list()[1]) + for index in range(numMsgs): + msg = "" + for line in mailman.retr(index + 1)[1]: + self.metrics['transferRxBytes'] += len(line) + msg += line.decode("utf-8", "ignore") + "\n" + msg_subject = email.message_from_string(msg).get('Subject') + msg_filename = msg_subject + datetime.datetime.now().strftime( + '%Y%m%d_%H%M%s_%f') + m = sarracenia.Message.fromFileInfo(msg_filename, self.o) + gathered_messages.append(m) + + mailman.quit() + + else: + logger.error( + "poll_email_ingest pollUrl protocol must be one of 'imap/imaps' or 'pop/pops'." + ) + return gathered_messages
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll/nasa_mls_nrt.html b/_modules/sarracenia/flowcb/poll/nasa_mls_nrt.html new file mode 100644 index 000000000..e0d5f8b56 --- /dev/null +++ b/_modules/sarracenia/flowcb/poll/nasa_mls_nrt.html @@ -0,0 +1,139 @@ + + + + + + sarracenia.flowcb.poll.nasa_mls_nrt — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll.nasa_mls_nrt

+import logging
+import paramiko
+import sarracenia
+from sarracenia import nowflt, timestr2flt
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Nasa_mls_nrt(Poll): +
+[docs] + def handle_data(self, data): + """ + decode some HTML into an SFTPAttributes record for a file. + """ + + st = paramiko.SFTPAttributes() + st.st_mtime = 0 + st.st_mode = 0o775 + st.filename = data + + if 'MLS-Aura' in data: + logger.debug("data %s" % data) + self.entries[data] = st + + logger.info("(%s) = %s" % (self.myfname, st)) + if self.myfname == None: return + if self.myfname == data: return
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll/noaa_hydrometric.html b/_modules/sarracenia/flowcb/poll/noaa_hydrometric.html new file mode 100644 index 000000000..9ae68cbde --- /dev/null +++ b/_modules/sarracenia/flowcb/poll/noaa_hydrometric.html @@ -0,0 +1,233 @@ + + + + + + sarracenia.flowcb.poll.noaa_hydrometric — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll.noaa_hydrometric

+"""
+Posts updated files of NOAA water level/temperature hydrometric data. Station site IDs provided
+in the poll_noaa_stn_file. Compatible with Python 3.5+.
+
+usage:
+sample url: https://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=9450460&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv
+
+in an sr_poll configuration file::
+
+	pollUrl http://tidesandcurrents.noaa.gov/api
+        retrievePathPattern /datagetter?range=1&station={0:}&product={1:}&units=metric&time_zone=gmt&application=web_services&format=csv
+
+	poll_noaa_stn_file [path/to/stn/file]
+	callback noaa_hydrometric
+
+sample station file::
+
+        7|70678|9751639|Charlotte Amalie|US|VI|-4.0
+        7|70614|9440083|Vancouver|US|WA|-8.0
+
+The poll:
+If poll_noaa_stn_file isn't set, it'll grab an up-to-date version of all station site code data from the 
+NOAA website. The station list file is in the following format:
+SourceID | SiteID | SiteCode | SiteName | CountryID | StateID | UTCOffset
+Each station on its own line.
+Posts the file on the exchange if the request returns a valid URL. 
+
+in v2, one needed a matching downloader plugin, but in sr3 we can leverage the retrievePath feature
+so that normalk downloader works, so only the poll one needed.
+
+"""
+
+import copy
+import datetime
+import logging
+
+import os
+import sarracenia
+from sarracenia.flowcb import FlowCB
+import urllib.request
+import xml.etree.ElementTree as ET
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Noaa_hydrometric(FlowCB): +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + + # these options are only for the poll. + self.o.add_option(option='poll_noaa_stn_file', kind='str') + self.o.add_option( option='retrievePathPattern', kind='str', \ + default_value='datagetter?range=1&station={0:}&product={1:}&units=metric&time_zone=gmt&application=web_services&format=csv' ) + + if self.o.identity_method.startswith('cod,'): + m, v = self.o.identity_method.split(',') + self.identity = {'method': m, 'value': v}
+ + + def poll(self) -> list: + + # Make list of site codes to pass to http get request + sitecodes = [] + if hasattr(self.o, 'poll_noaa_stn_file'): + stn_file = self.o.poll_noaa_stn_file + + # Parse file to make list of all site codes + try: + with open(stn_file) as f: + for line in f: + items = line.split('|') + sitecodes.append(items[2]) + logger.info("poll_noaa used stn_file %s" % stn_file) + + except IOError as e: + logger.error("poll_noaa couldn't open stn file: %s" % stn_file) + + else: + # Grab station site codes from https://opendap.co-ops.nos.noaa.gov/stations/stationsXML.jsp + tree = ET.parse(urllib.request.urlopen\ + ('https://opendap.co-ops.nos.noaa.gov/stations/stationsXML.jsp')) + root = tree.getroot() + for child in root: + sitecodes.append(child.attrib['ID']) + + incoming_message_list = [] + # Every hour, form the link of water level/temp data to post + for site in sitecodes: + + retrievePath = self.o.retrievePathPattern.format(site, 'water_temperature') + url = self.o.pollUrl + retrievePath + logger.info(f'polling {site}, polling: {url}') + # Water temp request + resp = urllib.request.urlopen(url).getcode() + logger.info(f"poll_noaa file posted: {url} %s") + mtime = datetime.datetime.utcnow().strftime('%Y%m%d_%H%M') + + fname = f'noaa_{mtime}_{site}_WT.csv' + m = sarracenia.Message.fromFileInfo(fname, self.o) + m['identity'] = self.identity + m['retrievePath'] = retrievePath + m['new_file'] = fname + + incoming_message_list.append(m) + + # Water level request + retrievePath = self.o.retrievePathPattern.format( + site, 'water_level') + '&datum=STND' + url = self.o.pollUrl + retrievePath + resp = urllib.request.urlopen(url).getcode() + logger.info(f"poll_noaa file posted: {url}") + + fname = f'noaa_{mtime}_{site}_WL.csv' + m = sarracenia.Message.fromFileInfo(fname, self.o) + m['identity'] = self.identity + m['retrievePath'] = retrievePath + m['new_file'] = fname + + incoming_message_list.append(m) + + return incoming_message_list
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/poll/usgs.html b/_modules/sarracenia/flowcb/poll/usgs.html new file mode 100644 index 000000000..b6c29172f --- /dev/null +++ b/_modules/sarracenia/flowcb/poll/usgs.html @@ -0,0 +1,246 @@ + + + + + + sarracenia.flowcb.poll.usgs — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.poll.usgs

+"""
+Posts updated files of USGS hydrometric data. Station site IDs provided
+in the poll_usgs_stn_file. Compatible with Python 3.5+.
+
+Status: UNTESTED... don't have a working basis (even on v2) to compare against.
+        was updated for v3. but don't have credentials to confirm it now works.
+
+usage:
+	in an sr3 poll configuration file:
+	pollUrl http://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site={0:}&period=PT3H&parameterCd=00060,00065,00011
+
+	batch [station_chunk]
+	poll_usgs_station  station-declaration
+	callback gather/usgs
+
+	If multiple usgs stations need to be fetched in one call, station_chunk should specify how big the station
+	blocks should be. If not set it'll individually download station data.
+	If poll_usgs_stn_file isn't set, it'll default to pulling the siteIDs from: 
+	https://water.usgs.gov/osw/hcdn-2009/HCDN-2009_Station_Info.xlsx
+	directory. each station declaration is in the following format:
+	SourceID | SiteID | SiteCode | SiteName | CountryID | StateID | UTCOffset
+	Each station on its own line.
+
+        to reformat into a parseable configuration option, declare poll_usgs_station as a list option,
+        so each line looks like::
+
+             poll_usgs_station 7|70026|9014087|Dry Dock, MI|US|MI|-5.0
+
+        then this file can be an *include* directive, and include supports both local and remote url's
+        so the db can be admined in some other http accessible git repository.
+
+	More info on the http rest parameters at: https://waterservices.usgs.gov/rest/IV-Service.html
+	For writing fault-resistant code that polls from usgs: https://waterservices.usgs.gov/docs/portable_code.html
+	Sign up for updates involving if/how the format changes: http://waterdata.usgs.gov/nwis/subscribe?form=email
+	Parameter codes to tailor the data you want: 
+	https://help.waterdata.usgs.gov/code/parameter_cd_query?fmt=rdb&inline=true&group_cd=%
+	Currently the parametercd is set to:
+	00060	Physical	Discharge, cubic feet per second
+	00065	Physical	Gage height, feet
+	00011	Physical	Temperature, water, degrees Fahrenheit	
+"""
+
+import datetime
+import logging
+import pandas as pd
+import paramiko
+import sarracenia
+from sarracenia.flowcb import FlowCB
+import urllib.request
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Usgs(FlowCB): +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.o.add_option('poll_usgs_station', 'list') + + # Parse sitecodes from file if provided, or the usgs website (turns excel spreadsheet into pandas + # dataframe, parses from there) + self.sitecodes = [] + if hasattr(self.o, 'poll_usgs_station'): + for s in self.o.poll_usgs_station: + items = s.split('|') + self.sitecodes.append(items[2]) + logger.info('%d stations declared' % len(self.sitecodes)) + else: + df = pd.read_excel( + 'https://water.usgs.gov/osw/hcdn-2009/HCDN-2009_Station_Info.xlsx' + ) + for row in df.iterrows(): + self.sitecodes.append(str(row[1]['STATION ID']).zfill(8)) + + if hasattr(self.o, 'batch'): + mult = True + chunk_size = int(self.o.batch)
+ + + def poll(self): + + run_time = datetime.datetime.utcnow().strftime('%Y%m%d_%H%M') + + gathered_messages = [] + if self.o.batch > 1: + file_cnt = 0 + for sites in [ + self.sitecodes[i:i + self.o.batch] + for i in range(0, len(self.sitecodes), self.o.batch) + ]: + stns = ','.join([s for s in sites]) + file_cnt += 1 + logger.debug('getting: %s' % self.o.pollUrl.format(stns)) + + status_code = urllib.request.urlopen( + self.o.pollUrl.format(stns)).getcode() + if status_code == 200: + logger.info("poll_usgs file updated %s" % + self.o.pollUrl.format(stns)) + + self.o.msg.new_baseurl = self.o.pollUrl.format(stns) + + m = sarracenia.Message.fromFileInfo( + 'usgs_{0}_sites{1}.xml'.format(run_time, file_cnt), + self.o) + gathered_messages.append(m) + elif status_code == 403: + logger.error( + '''poll_usgs: USGS has determined your usage is excessive and \ + blocked your IP. Use the contact form on their site to be \ + unblocked.''') + else: + logger.debug("poll_usgs file not found: %s" % + self.o.pollUrl.format(stns)) + else: # Get stations one at a time + for site in self.sitecodes: + logger.debug('getting: %s' % self.o.pollUrl.format(site)) + status_code = urllib.request.urlopen( + self.o.pollUrl.format(site)).getcode() + if status_code == 200: + logger.info("poll_usgs file updated %s" % + self.o.pollUrl.format(site)) + self.o.msg.new_baseurl = self.o.pollUrl.format(site) + m = sarracenia.Message.fromFileInfo( + 'usgs_{0}_{1}.xml'.format(run_time, site), self.o) + gathered_messages.append(m) + elif status_code == 403: + logger.error( + '''poll_usgs: USGS has determined your usage is excessive and \ + blocked your IP. Use the contact form on their site to be \ + unblocked.''') + else: + logger.debug("poll_usgs file not found: %s" % + self.o.pollUrl.format(site)) + return gathered_messages
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/post/message.html b/_modules/sarracenia/flowcb/post/message.html new file mode 100644 index 000000000..953cba54f --- /dev/null +++ b/_modules/sarracenia/flowcb/post/message.html @@ -0,0 +1,182 @@ + + + + + + sarracenia.flowcb.post.message — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.post.message

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2020
+#
+
+import logging
+
+import sarracenia.moth
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Message(FlowCB): + """ + post messages to sarracenia.moth message queuing protocol destination. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + + if hasattr(self.o, 'post_broker'): + props = sarracenia.moth.default_options + props.update(self.o.dictify()) + + # adjust settings post_xxx to be xxx, as Moth does not use post_ ones. + for k in [ 'broker', 'exchange', 'topicPrefix', 'exchangeSplit', 'topic' ]: + post_one='post_'+k + if hasattr( self.o, post_one ): + #props.update({ k: getattr(self.o,post_one) } ) + props[ k ] = getattr(self.o,post_one) + + self.poster = sarracenia.moth.Moth.pubFactory(props)
+ + + def post(self, worklist): + + still_ok = [] + all_good=True + for m in worklist.ok: + if all_good and hasattr(self.poster,'putNewMessage') and self.poster.putNewMessage(m): + still_ok.append(m) + else: + all_good=False + worklist.failed.append(m) + worklist.ok = still_ok + + def metricsReport(self) -> dict: + if hasattr(self,'poster') and self.poster: + return self.poster.metricsReport() + else: + return {} + + def on_housekeeping(self): + if hasattr(self,'poster') and self.poster: + m = self.poster.metricsReport() + logger.debug( + f"messages: good: {m['txGoodCount']} bad: {m['txBadCount']} bytes: {m['txByteCount']}" + ) + self.poster.metricsReset() + else: + logger.debug( "no metrics available" ) + + def on_start(self): + if hasattr(self,'poster') and self.poster: + self.poster.putSetup() + logger.info('starting') + + def on_stop(self): + if hasattr(self,'poster') and self.poster: + self.poster.close() + logger.info('closing')
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/retry.html b/_modules/sarracenia/flowcb/retry.html new file mode 100644 index 000000000..276d4df6d --- /dev/null +++ b/_modules/sarracenia/flowcb/retry.html @@ -0,0 +1,313 @@ + + + + + + sarracenia.flowcb.retry — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.retry

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# more info: https://github.com/MetPX/sarracenia
+#
+
+import os, json, sys, time
+from _codecs import decode, encode
+
+from sarracenia import nowflt, timestr2flt
+
+import logging
+
+from sarracenia.flowcb import FlowCB
+from sarracenia.featuredetection import features
+
+
+# class sarra/retry
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Retry(FlowCB): + """ + overall goal: + + * When file transfers fail, write the messages to a queue to be retried later. + There is also a second retry queue for failed posts. + + how it works: + + * the after_accept checks how many incoming messages we received. + If there is a full batch to process, don't try to retry any. + + * if there is room, then fill in the batch with some retry requests. + + * when after_work is called, the worklist.failed list of messages + is the files where the transfer failed. write those messages to + a retry queue. + + * the DiskQueue or RedisQueue classes are used to store the retries, and it handles + expiry on each housekeeping event. + + """ +
+[docs] + def __init__(self, options) -> None: + + logger.debug("sr_retry __init__") + + super().__init__(options,logger) + + if not features['retry']['present'] : + logger.critical( f"missing retry pre-requsites, module disabled") + return + + self.o.add_option( 'retry_driver', 'str', 'disk') + + # retry_refilter False -- rety to send with existing processing. + # retry_refilter True -- re-ingest and re-apply processing (if it has changed.) + self.o.add_option( 'retry_refilter', 'flag', False) + + #queuedriver = os.getenv('SR3_QUEUEDRIVER', 'disk') + + logger.debug('logLevel=%s' % self.o.logLevel)
+ + + +
+[docs] + def gather(self, qty) -> None: + """ + If there are only a few new messages, get some from the download retry queue and put them into + `worklist.incoming`. + + Do this in the gather() entry point if retry_refilter is True. + + """ + if not features['retry']['present'] or not self.o.retry_refilter: + return (True, []) + + if qty <= 0: return (True, []) + + message_list = self.download_retry.get(qty) + + # eliminate calculated values so it is refiltered from scratch. + for m in message_list: + for k in m: + if k in m['_deleteOnPost'] or k.startswith('new_'): + del m[k] + m['_isRetry'] = True + m['_deleteOnPost'] = set( [ '_isRetry' ] ) + + + return (True, message_list)
+ + + +
+[docs] + def after_accept(self, worklist) -> None: + """ + If there are only a few new messages, get some from the download retry queue and put them into + `worklist.incoming`. + + Do this in the after_accept() entry point if retry_refilter is False. + + """ + if not features['retry']['present'] or self.o.retry_refilter: + return + + qty = (self.o.batch / 2) - len(worklist.incoming) + #logger.info('qty: %d len(worklist.incoming) %d' % ( qty, len(worklist.incoming) ) ) + + if qty <= 0: return + + mlist = self.download_retry.get(qty) + + #logger.debug("loading from %s: qty=%d ... got: %d " % (self.download_retry_name, qty, len(mlist))) + if len(mlist) > 0: + worklist.incoming.extend(mlist)
+ + +
+[docs] + def after_work(self, worklist) -> None: + """ + Messages in `worklist.failed` should be put in the download retry queue. If there are only a few new + messages, get some from the post retry queue and put them into `worklist.ok`. + """ + if not features['retry']['present'] : + return + + if len(worklist.failed) != 0: + #logger.debug("putting %d messages into %s" % (len(worklist.failed),self.download_retry_name) ) + self.download_retry.put(worklist.failed) + worklist.failed = [] + + # retry posting... + qty = (self.o.batch / 2) - len(worklist.ok) + if qty <= 0: return + + mlist = self.post_retry.get(qty) + + #logger.debug("loading from %s: qty=%d ... got: %d " % (self.post_retry_name, qty, len(mlist))) + if len(mlist) > 0: + worklist.ok.extend(mlist)
+ + +
+[docs] + def after_post(self, worklist) -> None: + """ + Messages in `worklist.failed` should be put in the post retry queue. + """ + if not features['retry']['present'] : + return + + self.post_retry.put(worklist.failed) + worklist.failed=[]
+ + +
+[docs] + def metricsReport(self) -> dict: + """Returns the number of messages in the download_retry and post_retry queues. + + Returns: + dict: containing metrics: ``{'msgs_in_download_retry': (int), 'msgs_in_post_retry': (int)}`` + """ + return {'msgs_in_download_retry': len(self.download_retry), 'msgs_in_post_retry': len(self.post_retry)}
+ + + def on_cleanup(self) -> None: + logger.debug('starting retry cleanup') + + if not hasattr(self,'download_retry'): + self.on_start() + + self.download_retry.cleanup() + self.post_retry.cleanup() + + def on_housekeeping(self) -> None: + logger.debug("on_housekeeping") + + self.download_retry.on_housekeeping() + self.post_retry.on_housekeeping() + + def on_start(self) -> None: + + if self.o.retry_driver == 'redis': + from sarracenia.redisqueue import RedisQueue + self.download_retry = RedisQueue(self.o, 'work_retry') + self.post_retry = RedisQueue(self.o, 'post_retry') + else: + from sarracenia.diskqueue import DiskQueue + self.download_retry_name = 'work_retry_%02d' % self.o.no + self.download_retry = DiskQueue(self.o, self.download_retry_name) + self.post_retry_name = 'post_retry_%03d' % self.o.no + self.post_retry = DiskQueue(self.o, self.post_retry_name) + + def on_stop(self) -> None: + self.download_retry.close() + self.post_retry.close()
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/sample.html b/_modules/sarracenia/flowcb/sample.html new file mode 100644 index 000000000..47a7e9af6 --- /dev/null +++ b/_modules/sarracenia/flowcb/sample.html @@ -0,0 +1,128 @@ + + + + + + sarracenia.flowcb.sample — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.sample

+import logging
+import sarracenia.flowcb
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Sample(sarracenia.flowcb.FlowCB): +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + + # declare a module specific setting. + options.add_option('announce_list', 'list')
+ + + def on_start(self): + + logger.info('announce_list: %s' % self.o.announce_list)
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/scheduled.html b/_modules/sarracenia/flowcb/scheduled.html new file mode 100644 index 000000000..1aa08cf49 --- /dev/null +++ b/_modules/sarracenia/flowcb/scheduled.html @@ -0,0 +1,368 @@ + + + + + + sarracenia.flowcb.scheduled — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.scheduled

+import logging
+import paramiko
+import re
+
+import sarracenia
+from sarracenia.filemetadata import FmdStat
+from sarracenia.flowcb import FlowCB
+
+import datetime
+import json
+import time
+
+
+logger = logging.getLogger(__name__)
+
+
+[docs] +class Scheduled(FlowCB): + + """ + Scheduled flow callback plugin arranges to post url's + at scheduled times. + + usage: + + In the configuration file, need:: + + callback scheduled + + and the schedule can be a specified as: + + * scheduled_interval 1m (once a minute) a duration + * scheduled_hour 4,9 at 4Z and 9Z every day. + * scheduled_minute 33,45 within scheduled hours which minutes. + + Scheduled_interval takes precedence over the others, making it + easier to specify an interval for testing/debugging purposes. + + use in code (for subclassing): + + from sarracenia.scheduled import Scheduled + + class hoho(Scheduled): + replace the gather() routine... + keep the top lines "until time to run" + replace whatever is below. + will only run when it should. + + """ + +
+[docs] + def update_appointments(self,when): + """ + # make a flat list from values where comma separated on a single or multiple lines. + + set self.appointments to a list of when something needs to be run during the current day. + """ + self.appointments=[] + for h in self.hours: + for m in self.minutes: + if ( h > when.hour ) or ((h == when.hour) and ( m >= when.minute )): + appointment = datetime.time(h, m, tzinfo=datetime.timezone.utc ) + next_time = datetime.datetime.combine(when,appointment) + self.appointments.append(next_time) + else: + pass # that time is passed for today. + + logger.info( f"for {when}: {json.dumps(list(map( lambda x: str(x), self.appointments))) } ")
+ + + +
+[docs] + def __init__(self,options,logger=logger): + super().__init__(options,logger) + self.o.add_option( 'scheduled_interval', 'duration', 0 ) + self.o.add_option( 'scheduled_hour', 'list', [] ) + self.o.add_option( 'scheduled_minute', 'list', [] ) + + self.housekeeping_needed=False + self.interrupted=None + + sched_hours = sum([ x.split(',') for x in self.o.scheduled_hour],[]) + self.hours = list(map( lambda x: int(x), sched_hours )) + self.hours.sort() + logger.debug( f"hours {self.hours}" ) + + sched_min = sum([ x.split(',') for x in self.o.scheduled_minute ],[]) + self.minutes = list(map( lambda x: int(x), sched_min)) + self.minutes.sort() + + self.default_wait=300 + + logger.debug( f'minutes: {self.minutes}') + + now=datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + self.update_appointments(now) + self.first_interval=True + + if self.o.scheduled_interval <= 0 and not self.appointments: + logger.info( f"no scheduled_interval or appointments (combination of scheduled_hour and scheduled_minute) set defaulting to every {self.default_wait} seconds" )
+ + + def gather(self,messageCountMax): + + # for next expected post + self.wait_until_next() + + if self.stop_requested or self.housekeeping_needed: + return (False, []) + + logger.info('time to run') + + # always post the same file at different time + gathered_messages = [] + + for relPath in self.o.path: + st = FmdStat() + m = sarracenia.Message.fromFileInfo(relPath, self.o, st) + gathered_messages.append(m) + + return (True, gathered_messages) + + def on_housekeeping(self): + + self.housekeeping_needed = False + + +
+[docs] + def wait_seconds(self,sleepfor): + """ + sleep for the given number of seconds, like time.sleep() but broken into + shorter naps to be able to honour stop_requested, or when housekeeping is needed. + + """ + + housekeeping=datetime.timedelta(seconds=self.o.housekeeping) + nap=datetime.timedelta(seconds=10) + + if self.interrupted: + sleepfor = self.interrupted + now = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + + # update sleep remaining based on how long other processing took. + interruption_duration= now-self.interrupted_when + sleepfor -= interruption_duration + + if sleepfor < nap: + nap=sleepfor + + sleptfor=datetime.timedelta(seconds=0) + + while sleepfor > datetime.timedelta(seconds=0): + time.sleep(nap.total_seconds()) + if self.stop_requested: + return + + # how long is left to sleep. + sleepfor -= nap + self.interrupted=sleepfor + self.interrupted_when = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + + sleptfor += nap + if sleptfor > housekeeping: + self.housekeeping_needed=True + return + + # got to the end of the interval... + self.interrupted=None
+ + + def wait_until( self, appointment ): + + now = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + + sleepfor=appointment-now + + logger.info( f"appointment at: {appointment}, need to wait: {sleepfor})" ) + self.wait_seconds( sleepfor ) + + + def wait_until_next( self ): + + if self.o.scheduled_interval > 0: + if self.first_interval: + self.first_interval=False + return + + self.wait_seconds(datetime.timedelta(seconds=self.o.scheduled_interval)) + return + + if ( len(self.o.scheduled_hour) > 0 ) or ( len(self.o.scheduled_minute) > 0 ): + now = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + next_appointment=None + missed_appointments=[] + for t in self.appointments: + if now < t: + next_appointment=t + break + else: + logger.info( f'already too late to {t} skipping' ) + missed_appointments.append(t) + + if missed_appointments: + for ma in missed_appointments: + self.appointments.remove(ma) + + if next_appointment is None: + # done for the day... + tomorrow = datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc)+datetime.timedelta(days=1) + midnight = datetime.time(0,0,tzinfo=datetime.timezone.utc) + midnight = datetime.datetime.combine(tomorrow,midnight) + self.update_appointments(midnight) + next_appointment=self.appointments[0] + + self.wait_until(next_appointment) + if self.interrupted: + logger.info( f"sleep interrupted, returning for housekeeping." ) + else: + self.appointments.remove(next_appointment) + logger.info( f"ok {len(self.appointments)} appointments left today" ) + return + + # default wait... + + if self.first_interval: + self.first_interval=False + return + + self.wait_seconds(self.default_wait)
+ + +if __name__ == '__main__': + + import sarracenia.config + import types + import sarracenia.flow + options = sarracenia.config.default_config() + flow = sarracenia.flow.Flow(options) + flow.o.scheduled_hour= [ '1','3','5',' 7',' 9',' 13','21','23'] + flow.o.scheduled_minute= [ '1,3,5',' 7',' 9',' 13',' 15',' 51','53' ] + logging.basicConfig(level=logging.DEBUG) + + when=datetime.datetime.fromtimestamp(time.time(),datetime.timezone.utc) + + me = Scheduled(flow.o) + me.update_appointments(when) + + flow.o.scheduled_hour= [ '1' ] + me = Scheduled(flow.o) + me.update_appointments(when) + + """ + for unit testing should be able to change when, and self.o.scheduled_x to cover + many different test cases. + """ + + while True: + logger.info("hoho!") + me.wait_until_next() + logger.info("Do Something!") +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/send/email.html b/_modules/sarracenia/flowcb/send/email.html new file mode 100644 index 000000000..0a772d82d --- /dev/null +++ b/_modules/sarracenia/flowcb/send/email.html @@ -0,0 +1,203 @@ + + + + + + sarracenia.flowcb.send.email — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.send.email

+"""
+    sarracenia.flowcb.send.email.Email is an sr3 sender plugin. Once a file is 
+    posted, the plugin matches the topic(what the filename begins with) to the
+    file name and sends the appropriate emails.
+
+    Usage:
+      1. Need the following variables in an sr_sender config defined: file_email_to, file_email_relay
+         Optionally, you can also provide a sender name/email as file_email_form:
+
+            file_email_to AACN27 muhammad.taseer@canada.ca, test@test.com
+            file_email_relay email.relay.server.ca
+            file_email_from santa@canada.ca
+
+      2. In the config file, include the following line:
+
+            callback send.email
+
+      3. sr_sender foreground emails.conf
+
+    Original Author: Wahaj Taseer - June, 2019
+"""
+
+from email.message import EmailMessage
+import logging
+import os.path
+import re
+from sarracenia.flowcb import FlowCB
+import smtplib
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Email(FlowCB): +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.o.add_option('file_email_command', 'str', '/usr/bin/mail') + self.o.add_option('file_email_to', 'list') + self.o.add_option('file_email_from', 'str') + self.o.add_option('file_email_relay', 'str')
+ + + def send(self, msg): + + # have a list of email destinations... + logger.debug("email: %s" % self.o.file_email_to) + ipath = os.path.normpath(msg['relPath']) + + # loop over all the variables from config file, if files match, send via email + for header in self.o.file_email_to: + file_type, emails = header.split(' ', 1) + emails = [x.strip(' ') for x in emails.split(',')] + + # check if the file arrived matches any email rules + if re.search('^' + file_type + '.*', msg['new_file']): + + for recipient in emails: + logger.debug('sending file %s to %s' % (ipath, recipient)) + + with open(ipath) as fp: + emsg = EmailMessage() + emsg.set_content(fp.read()) + + try: + sender = self.o.file_email_from + if not sender: + sender = 'sarracenia-emailer' + except AttributeError: + sender = 'sarracenia-emailer' + + logger.debug("Using sender email: " + sender) + + emsg['Subject'] = msg['new_file'] + emsg['From'] = sender + emsg['To'] = recipient + + try: + email_relay = self.o.file_email_relay + if not email_relay: + raise AttributeError() + except AttributeError: + logger.error( + 'file_email_relay config NOT defined, please define an SMTP (relay) server' + ) + + logger.debug("Using email relay server: " + email_relay) + s = smtplib.SMTP(email_relay) + s.send_message(emsg) + s.quit() + + logger.info('sent file %s to %s' % (ipath, recipient)) + + return True
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/shiftdir2baseurl.html b/_modules/sarracenia/flowcb/shiftdir2baseurl.html new file mode 100644 index 000000000..18efec423 --- /dev/null +++ b/_modules/sarracenia/flowcb/shiftdir2baseurl.html @@ -0,0 +1,150 @@ + + + + + + sarracenia.flowcb.shiftdir2baseurl — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.shiftdir2baseurl

+import os
+
+import urllib.parse
+
+import logging
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class ShiftDir2baseUrl(FlowCB): + """ + modify message to shift directories from relPath to baseUrl: + + given the setting shiftDir2baseUrl == 2 and given message with + baseDir=https://a relPath=b/c/d/e subtopic=b/c/d --> baseDir=https://a/b/c relPath=d/e subtopic=d + + """ +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.o.add_option('shiftDir2baseUrl', 'count', 1)
+ + + def after_work(self, worklist): + for m in worklist.ok: + logger.debug("before: base_url=%s, subtopic=%s relPath=%s" % + (m['baseUrl'], m['subtopic'], m['relPath'])) + + dirs2shift = '/'.join(m['subtopic'][0:self.o.shiftDir2baseUrl]) + m['subtopic'] = m['subtopic'][self.o.shiftDir2baseUrl:] + m['baseUrl'] = m['baseUrl'] + '/' + dirs2shift + m['relPath'] = '/'.join( + m['relPath'].split('/')[self.o.shiftDir2baseUrl:]) + + logger.info( + "shifted %d done: base_url=%s, subtopic=%s relPath=%s" % + (self.o.shiftDir2baseUrl, m['baseUrl'], m['subtopic'], + m['relPath']))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/v2wrapper.html b/_modules/sarracenia/flowcb/v2wrapper.html new file mode 100644 index 000000000..d1b9efbb2 --- /dev/null +++ b/_modules/sarracenia/flowcb/v2wrapper.html @@ -0,0 +1,589 @@ + + + + + + sarracenia.flowcb.v2wrapper — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.v2wrapper

+from base64 import b64decode, b64encode
+from codecs import decode, encode
+
+import copy
+import logging
+from hashlib import md5
+from hashlib import sha512
+import os
+import sarracenia.config
+import time
+import types
+import urllib
+
+import sarracenia
+from sarracenia.flowcb import FlowCB
+
+from sarracenia import nowflt, timestr2flt, timev2tov3str
+
+logger = logging.getLogger(__name__)
+
+sum_algo_v3tov2 = {
+                "arbitrary": "a",
+                "md5": "d",
+                "sha512": "s",
+                "md5name": "n",
+                "random": "0",
+                "link": "L",
+                "remove": "R",
+                "cod": "z"
+}
+
+sum_algo_v2tov3 = { v: k for k,v in sum_algo_v3tov2.items() }
+
+
+[docs] +def sumstrFromMessage( msg ) -> str: + """ + accepts a v3 message as argument msg. returns the corresponding sum string for a v2 'sum' header. + """ + + if 'identity' in msg: + if msg['identity']['method'] in sum_algo_v3tov2: + sa = sum_algo_v3tov2[msg["identity"]["method"]] + else: # FIXME ... 1st md5name case... default when unknown... + logger.error('identity method unknown to v2: %s, replacing with md5name' % msg['identity']['method'] ) + sa = 'n' + sv = md5(bytes(os.path.basename(msg['relPath']),'utf-8')).hexdigest() + + # transform sum value + if sa in ['0', 'a']: + sv = msg["identity"]["value"] + elif sa in ['z']: + sv = sum_algo_v3tov2[msg["identity"]["value"]] + else: + sv = encode( + decode(msg["identity"]["value"].encode('utf-8'), "base64"), + 'hex').decode('utf-8') + sumstr = sa + ',' + sv + else: + # FIXME ... 2nd md5name case. + sumstr = 'n,%s' % md5(bytes(os.path.basename(msg['relPath']),'utf-8')).hexdigest() + + if 'fileOp' in msg: + if 'rename' in msg['fileOp']: + msg['oldname'] = msg['fileOp']['rename'] + + if 'link' in msg['fileOp']: + hash = sha512() + hash.update( bytes( msg['fileOp']['link'], encoding='utf-8' ) ) + sumstr = 'L,%s' % hash.hexdigest() + elif 'remove' in msg['fileOp']: + hash = sha512() + hash.update(bytes(os.path.basename(msg['relPath']), encoding='utf-8')) + sumstr = 'R,%s' % hash.hexdigest() + elif 'directory' in msg['fileOp']: + hash = sha512() + hash.update(bytes(os.path.basename(msg['relPath']), encoding='utf-8')) + + if 'remove' in msg['fileOp']: + sumstr = 'r,%s' % hash.hexdigest() + else: + sumstr = 'm,%s' % hash.hexdigest() + else: + logger.error('unknown fileOp: %s' % msg['fileOp'] ) + return sumstr
+ + +class Message: + def __init__(self, h): + """ + builds the in-memory representation of a message as expected by v2 plugins. + In v3, a message is just a dictionary. in v2 it is an object. + + assign everything, except topic... because the topic is stored outside the body in v02. + """ + + self.pubtime = h['pubTime'].replace("T", "") + self.baseurl = h['baseUrl'] + self.relpath = h['relPath'] + + if 'new_dir' in h: + self.new_dir = h['new_dir'] + self.new_file = h['new_file'] + + if 'new_relPath' in h: + self.new_relpath = h['new_relPath'] + + self.urlstr = self.baseurl + self.relpath + self.url = urllib.parse.urlparse(self.urlstr) + + self.notice = self.pubtime + ' ' + h["baseUrl"] + ' ' + h[ + "relPath"].replace(' ', '%20').replace('#', '%23') + + #FIXME: ensure headers are < 255 chars. + for k in ['mtime', 'atime']: + if k in h: + h[k] = h[k].replace("T", "") + + #FIXME: sum header encoding. + if 'size' in h: + if type(h['size']) is str: + h['size'] = int(h['size']) + h['parts'] = '1,%d,1,0,0' % h['size'] + + if 'blocks' in h: + if h['blocks']['method'] == 'inplace': + m = 'i' + else: + m = 'p' + p = h['blocks'] + if 'number' in p: + remainder = p['manifest'][p['number']]['size'] + number = p['number'] + else: + number=0 + if 'manifest' in p: + remainder = p['manifest'][len(p['manifest'])-1]['size'] + else: + remainder = 0 + h['parts'] = '%s,%d,%d,%d,%d' % (m, p['size'], len(p['manifest']), + remainder, number) + + h['topic'] = [ 'v02', 'post' ] + self.relpath.split('/')[0:-1] + + if 'parts' in h: + self.partstr = h['parts'] + #else: + # self.partstr = None + + self.sumstr = sumstrFromMessage( h ) + self.sumflg = self.sumstr[0] + h['sum'] = self.sumstr + + if 'fileOp' in h and 'rename' in h['fileOp'] : + h['oldname'] = h['fileOp']['rename'] + + self.headers = h + self.hdrstr = str(h) + self.isRetry = False + + # from sr_message/sr_new ... + self.local_offset = 0 + self.in_partfile = False + self.local_checksum = None + + self.target_file = None + # does not cover partitioned files. + + def set_hdrstr(self): + logger.info("set_hdrstr not implemented") + pass + + def get_elapse(self): + return nowflt() - timestr2flt(self.pubtime) + + def set_parts(self): + logger.info("set_parts not implemented") + pass + + +
+[docs] +class V2Wrapper(FlowCB): +
+[docs] + def __init__(self, o): + """ + A wrapper class to run v02 plugins. + us run_entry(entry_point,module) + + entry_point is a string like 'on_message', and module being the one to add. + + weird v2 stuff: + when calling init, self is a config/subscriber... + when calling on_message, self is a message... + that is kind of blown away for each message... + parent is the config/subscriber in both cases. + so v2 state variables are always stored in parent. + + """ + global logger + + logging.basicConfig(format=o.logFormat, + level=getattr(logging, o.logLevel.upper())) + + logger.setLevel(getattr(logging, o.logLevel.upper())) + + #logger.info('logging: fmt=%s, level=%s' % ( o.logFormat, o.logLevel ) ) + + # FIXME, insert parent fields for v2 plugins to use here. + self.logger = logger + #logger.info('v2wrapper init start') + + self.state_vars = [] + + if o.statehost: + hostdir = o.hostdir + else: + hostdir = None + + self.user_cache_dir = sarracenia.config.get_user_cache_dir(hostdir) + + if hasattr(o, 'no'): + self.instance = o.no + else: + self.instance = 0 + + self.o = o + + self.v2plugins = {} + self.consumer = types.SimpleNamespace() + self.consumer.sleep_min = 0.01 + + for ep in sarracenia.config.Config.v2entry_points: + self.v2plugins[ep] = [] + + unsupported_v2_events = ['do_download', 'do_get', 'do_put', 'do_send'] + for e in o.v2plugins: + #logger.info('resolving: %s' % e) + for v in o.v2plugins[e]: + if e in unsupported_v2_events: + logger.error( + 'v2 plugin conversion required, %s too different in v3' + % e) + continue + self.add(e, v) + + #propagate options back to self.o for on_timing calls. + #for v2o in self.o.v2plugin_options: + # setattr( self.o, v2o, getattr(self,v2o ) ) + + # backward compat... + self.o.user_cache_dir = self.o.cfg_run_dir + self.o.instance = self.instance + self.o.logger = self.logger + if hasattr(self.o, 'post_baseDir'): + self.o.post_base_dir = self.o.post_baseDir
+ + + #logger.info('v2wrapper init done') + + def declare_option(self, option): + logger.info('v2plugin option: %s declared' % option) + + self.state_vars.append(option) + + self.o.add_option(option) + if not hasattr(self.o, option): + logger.info('value of %s not set' % option) + return + + if type(getattr(self.o, option)) is not list: + setattr(self.o, option, [getattr(self.o, option)]) + + def add(self, opname, path): + + setattr(self, opname, None) + + if path == 'None' or path == 'none' or path == 'off': + logger.info("Reset plugin %s to None" % opname) + exec('self.' + opname + '_list = [ ]') + return True + + ok, script = sarracenia.config.config_path('plugins', + path, + mandatory=True, + ctype='py') + if not ok: + logger.error("installing %s %s failed: not found " % + (opname, path)) + return False + + #logger.info('installing: %s %s' % ( opname, path ) ) + + c1 = set(vars(self)) + + try: + with open(script) as f: + exec( + compile(f.read().replace('self.plugin', 'self.v2plugin'), + script, 'exec')) + except: + logger.error( + "sr_config/execfile 2 failed for option '%s' and plugin '%s'" % + (opname, path)) + logger.debug('Exception details: ', exc_info=True) + return False + + if opname == 'plugin': + if getattr(self, 'v2plugin') is None: + logger.error("%s plugin %s incorrect: does not set self.%s" % + ('v2plugin', path, 'v2plugin')) + return False + + # pci plugin-class-instance... parent is self (a v2wrapper) + pci = self.v2plugin.lower() + s = pci + ' = ' + self.v2plugin + '(self)' + exec(pci + ' = ' + self.v2plugin + '(self)') + s = 'vars(' + self.v2plugin + ')' + pcv = eval('vars(' + self.v2plugin + ')') + for when in sarracenia.config.Config.v2entry_points: + if when in pcv: + #logger.info("v2 registering %s from %s" % ( when, path ) ) + + # 2020/05/22. I think the commented exec can be removed. + #FIXME: this breaks things horrible in v3. I do not see the usefulness even in v2. + # everything is done with the lists, so value of setting individual value is nil. + # self.on_start... vs. + # self.v2plugins['on_start'].append( thing. ) + #exec( 'self.' + when + '=' + pci + '.' + when ) + eval('self.v2plugins["' + when + '"].append(' + pci + '.' + + when + ')') + else: + if getattr(self, opname) is None: + logger.error("%s plugin %s incorrect: does not set self.%s" % + (opname, path, opname)) + return False + + #eval( 'self.' + opname + '_list.append(self.' + opname + ')' ) + eval('self.v2plugins["' + opname + '"].append( self.' + opname + + ')') + + c2 = set(vars(self)) + c12diff = list(c2 - c1) + #logger.error('init added: +%s+ to %s' % (c12diff, self.state_vars) ) + if len(c12diff) > 0: + self.state_vars.extend(c12diff) + + for opt in self.state_vars: + if hasattr(self, opt): + setattr(self.o, opt, getattr(self, opt)) + + return True + + def after_work(self, worklist): + ok_to_post = [] + for m in worklist.ok: + if self.run_entry('on_file', m): + ok_to_post.append(m) + else: + #worklist.failed.append(m) + pass + # FIXME: what should we do on failure of on_file plugin? + # download worked, but on_file failed... hmm... + + worklist.ok = ok_to_post + + outgoing = [] + for m in worklist.ok: + if self.run_entry('on_post', m): + outgoing.append(m) + else: + worklist.rejected.append(m) + # set incoming for future steps. + worklist.ok = outgoing + + def after_accept(self, worklist): + + outgoing = [] + for m in worklist.incoming: + try: + if self.run_entry('on_message', m): + outgoing.append(m) + else: + worklist.rejected.append(m) + except Exception as Ex: + logger.error( f"plugin {m} died: {Ex}" ); + logger.debug( 'details: ', exc_info=True) + worklist.rejected.append(m) + # set incoming for future steps. + worklist.incoming = outgoing + +
+[docs] + def on_time(self, time): + """ + run plugins for a given entry point. + """ + logger.info('v2 run %s' % time) + for plugin in self.v2plugins[time]: + plugin(self.o)
+ + + def on_housekeeping(self): + self.on_time('on_heartbeat') + + def on_start(self): + self.on_time('on_start') + + def on_stop(self): + self.on_time('on_stop') + + def restoreMsg(self, m, v2msg): + + if 'topic' in m: + if m['topic'][0:2] == ['v02', 'post' ]: + m['topic'] = self.o.post_topicPrefix + m['topic'][2:] + + if ('link' in v2msg.headers): + if not 'fileOp' in m: + m['fileOp'] = {} + + if m['fileOp']['link'] != v2msg.headers['link']: + m['fileOp']['link'] = v2msg.headers['link'] + + for h in ['oldname', 'newname' ]: + if (h in v2msg.headers) and ((h not in m) or + (v2msg.headers[h] != m[h])): + m[h] = v2msg.headers[h] + + if v2msg.new_dir != m['new_dir']: + m['new_dir'] = v2msg.new_dir + relpath = m['new_dir'] + '/' + v2msg.new_file + if self.o.post_baseDir: + relpath = relpath.replace(self.o.post_baseDir, '', 1) + m['new_relPath'] = relpath + + if v2msg.baseurl != m['baseUrl']: + m['baseUrl'] = v2msg.baseurl + + if hasattr(v2msg, 'new_file'): + if ('new_file' not in m) or (m['new_file'] != v2msg.new_file): + m['new_file'] = v2msg.new_file + + if hasattr( + v2msg, + 'post_base_dir') and (v2msg.post_base_dir != m['new_baseDir']): + m['post_baseDir'] = v2msg.post_base_dir + +
+[docs] + def run_entry(self, ep, m): + """ + run plugins for a given entry point. + """ + self.msg = Message(m) + self.msg.topic = '.'.join(self.o.topicPrefix + m['subtopic']) + self.o.msg = self.msg + if hasattr(self.msg, 'partstr'): + self.o.partstr = self.msg.partstr + self.o.sumstr = self.msg.sumstr + + varsb4 = set(vars(self.msg)) + + for opt in self.state_vars: + if hasattr(self.o, opt): + setattr(self.msg, opt, getattr(self.o, opt)) + + ok = True + for plugin in self.v2plugins[ep]: + ok = plugin(self.o) + if not ok: break + + vars_after = set(vars(self.msg)) + + self.restoreMsg(m, self.msg) + + diff = list(vars_after - varsb4) + if len(diff) > 0: + self.state_vars.extend(diff) + + return ok
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/work/age.html b/_modules/sarracenia/flowcb/work/age.html new file mode 100644 index 000000000..b124de937 --- /dev/null +++ b/_modules/sarracenia/flowcb/work/age.html @@ -0,0 +1,179 @@ + + + + + + sarracenia.flowcb.work.age — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.work.age

+"""
+   print the age of files written (compare current time to mtime of message.)
+   usage:
+
+   flowcb work.age
+
+"""
+
+import os, stat, time
+import datetime
+import json
+import logging
+import sarracenia 
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Age(FlowCB): + + def reset_metrics(self) -> None: + self.metrics={} + self.metrics['ageTotal'] = 0 + self.metrics['ageCount'] = 0 + self.metrics['ageMax'] = 0 + self.metrics['copyTotal'] = 0 + self.metrics['copyMax'] = 0 + + def on_start(self) -> None: + self.reset_metrics() + + def metricsReport(self) -> dict: + self.metrics['copyCount'] = self.metrics['ageCount'] + if self.metrics['ageCount'] > 0: + self.metrics['ageMean'] = self.metrics['ageTotal']/self.metrics['ageCount'] + self.metrics['copyMean'] = self.metrics['copyTotal']/self.metrics['ageCount'] + else: + self.metrics['ageMean'] = 0 + self.metrics['copyMean'] = 0 + + return self.metrics + + def on_housekeeping(self) -> None: + #logger.info( f" maximum Age: {datetime.timedelta(seconds=self.metrics['ageMax'])} Average Age: {datetime.timedelta(seconds=self.metrics['ageMean'])} files: {self.metrics['ageCount']}" ) + logger.info( "Age of files (in seconds) when transfer complete, maximum: %.2g Average: %.2g file count: %d" % + ( self.metrics['ageMax'], self.metrics['ageMean'], self.metrics['ageCount'] ) ) + + logger.info( "Copy time for files (in seconds) when transfer complete, maximum: %.2g Average: %.2g file count: %d" % + ( self.metrics['copyMax'], self.metrics['copyMean'], self.metrics['ageCount'] ) ) + + + def after_work(self, worklist) -> None: + for m in worklist.ok: + if not 'mtime' in m: + return None + completed = sarracenia.timestr2flt(m['report']['timeCompleted']) + mtime = sarracenia.timestr2flt(m['mtime']) + pubtime = sarracenia.timestr2flt(m['pubTime']) + age = completed - mtime + copy = completed - pubtime + self.metrics['ageTotal'] += age + self.metrics['copyTotal'] += copy + self.metrics['ageCount'] += 1 + if copy > self.metrics['copyMax']: + self.metrics['copyMax'] = copy + if age > self.metrics['ageMax']: + self.metrics['ageMax'] = age + + logger.info( f"file {m['new_dir']+os.sep+m['new_file']} took {copy} seconds to copy and is {age} seconds old" )
+ + + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/flowcb/work/rxpipe.html b/_modules/sarracenia/flowcb/work/rxpipe.html new file mode 100644 index 000000000..b32a1bef5 --- /dev/null +++ b/_modules/sarracenia/flowcb/work/rxpipe.html @@ -0,0 +1,144 @@ + + + + + + sarracenia.flowcb.work.rxpipe — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.flowcb.work.rxpipe

+import logging
+import os
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Rxpipe(FlowCB): + """ + After each file is transferred, write it's name to a named_pipe. + + parameter: + rxpipe_name -- the path for the named pipe to write the file names to. + """ +
+[docs] + def __init__(self, options): + + super().__init__(options,logger) + self.o.add_option(option='rxpipe_name', kind='str')
+ + + def on_start(self): + if not hasattr(self.o, 'rxpipe_name') and self.o.file_rxpipe_name: + logger.error("Missing rxpipe_name parameter") + return + self.rxpipe = open(self.o.rxpipe_name, "w") + logger.info(f'opened rxpipe_name: {self.o.rxpipe_name} for append') + + def after_work(self, worklist): + + for msg in worklist.ok: + fname = msg['new_dir'] + os.sep + msg['new_file'] + self.rxpipe.write(fname + '\n') + self.rxpipe.flush() + return None
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/identity.html b/_modules/sarracenia/identity.html new file mode 100644 index 000000000..799e38ba5 --- /dev/null +++ b/_modules/sarracenia/identity.html @@ -0,0 +1,199 @@ + + + + + + sarracenia.identity — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.identity

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2020
+#
+# sarracenia repository: git://git.code.sf.net/p/metpx/git
+# Documentation: http://metpx.sourceforge.net/#SarraDocumentation
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+import os
+import logging
+
+import functools
+
+from base64 import b64encode
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Identity: + """ + A class for algorithms to get a fingerprint for a file being announced. + Appropriate fingerprinting algorithms vary according to file type. + + required methods in subclasses: + + def registered_as(self): + return a one letter string identifying the algorithm (mostly for v2.) + in v3, the registration comes from the identity sub-class name in lower case. + + def set_path(self,path): + start a checksum for the given path... initialize. + + def update(self,chunk): + update the checksum based on the given bytes from the file (sequential access assumed.) + """ + @staticmethod + def factory(method='sha512'): + + for sc in Identity.__subclasses__(): + if method == sc.__name__.lower(): + return sc() + return None + + def get_method(self): + return type(self).__name__.lower() + +
+[docs] + def update_file(self, path): + """ + read the entire file, check sum it. + this is kind of last resort as it cost an extra file read. + It is better to call update( as the file is being read for other reasons. + """ + self.set_path(path) + with open(path, 'rb') as f: + for data in iter(functools.partial(f.read, 1024 * 1024), b''): + self.update(data)
+ + + @property + def value(self): + """ + return the current value of the checksum calculation. + """ + return b64encode(self.filehash.digest()).decode('utf-8')
+ + + +import sarracenia.identity.arbitrary +import sarracenia.identity.md5 +import sarracenia.identity.random +import sarracenia.identity.sha512 + +known_methods = [] +for sc in Identity.__subclasses__(): + known_methods.append(sc.__name__.lower()) +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/identity/arbitrary.html b/_modules/sarracenia/identity/arbitrary.html new file mode 100644 index 000000000..cb1cf1196 --- /dev/null +++ b/_modules/sarracenia/identity/arbitrary.html @@ -0,0 +1,148 @@ + + + + + + sarracenia.identity.arbitrary — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.identity.arbitrary

+from sarracenia.identity import Identity
+
+default_value = "None"
+
+
+def set_default_value(value):
+    global default_value
+    default_value = value
+
+
+[docs] +class Arbitrary(Identity): + """ + For applications where there is no known way of determining equivalence, allow them to supply + an arbitrary tag, that can be used to compare products for duplicate suppression purposes. + + use setter to set the value... some sort of external checksum algorithm that cannot be reproduced. + """ +
+[docs] + def __init__(self): + global default_value + self._value = default_value
+ + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = value + + @staticmethod + def registered_as(): + return 'a' + + def set_path(self, path): + pass + + def update(self, chunk): + pass
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/identity/md5.html b/_modules/sarracenia/identity/md5.html new file mode 100644 index 000000000..f97e3ed38 --- /dev/null +++ b/_modules/sarracenia/identity/md5.html @@ -0,0 +1,135 @@ + + + + + + sarracenia.identity.md5 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.identity.md5

+from hashlib import md5
+
+from sarracenia.identity import Identity
+
+
+
+[docs] +class Md5(Identity): + """ + use the (obsolete) Message Digest 5 (MD5) algorithm, applied on the content + of a file, to generate an identity signature. + """ + +
+[docs] + @staticmethod + def registered_as(): + """ + v2name. + """ + return 'd'
+ + + def set_path(self, path): + self.filehash = md5() + + def update(self, chunk): + if type(chunk) == bytes: self.filehash.update(chunk) + else: self.filehash.update(bytes(chunk, 'utf-8'))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/identity/random.html b/_modules/sarracenia/identity/random.html new file mode 100644 index 000000000..aa7a26b9c --- /dev/null +++ b/_modules/sarracenia/identity/random.html @@ -0,0 +1,127 @@ + + + + + + sarracenia.identity.random — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.identity.random

+import random
+
+from sarracenia.identity import Identity
+
+
+
+[docs] +class Random(Identity): + """ + Trivial minimalist checksumming algorithm, returns random number for any file. + """ + @property + def value(self): + return '%.4d' % random.randint(0, 9999) + + def set_path(self, path): + pass + + @staticmethod + def registered_as(): + return '0'
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/identity/sha512.html b/_modules/sarracenia/identity/sha512.html new file mode 100644 index 000000000..9a8fe8b29 --- /dev/null +++ b/_modules/sarracenia/identity/sha512.html @@ -0,0 +1,132 @@ + + + + + + sarracenia.identity.sha512 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.identity.sha512

+from hashlib import sha512
+
+from sarracenia.identity import Identity
+
+# ===================================
+# checksum_s class
+# ===================================
+
+
+
+[docs] +class Sha512(Identity): + """ + The SHA512 algorithm to checksum the entire file, which is called 's'. + """ + + @staticmethod + def registered_as(): + return 's' + + def set_path(self, path): + self.filehash = sha512() + + def update(self, chunk): + if type(chunk) == bytes: self.filehash.update(chunk) + else: self.filehash.update(bytes(chunk, 'utf-8'))
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/instance.html b/_modules/sarracenia/instance.html new file mode 100644 index 000000000..b4edb8f28 --- /dev/null +++ b/_modules/sarracenia/instance.html @@ -0,0 +1,367 @@ + + + + + + sarracenia.instance — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.instance

+#!/usr/bin/env python3
+
+# This file is part of Sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2020
+#
+
+import copy
+import logging
+import logging.handlers
+import os
+import pathlib
+from sarracenia.moth import Moth
+import signal
+import sys
+import threading
+import time
+import traceback
+
+from sarracenia import user_config_dir
+import sarracenia.config
+from sarracenia.flow import Flow
+
+from urllib.parse import urlparse, urlunparse
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class RedirectedTimedRotatingFileHandler( + logging.handlers.TimedRotatingFileHandler): +
+[docs] + def doRollover(self): + super().doRollover() + + if sys.platform != 'win32': + os.dup2(self.stream.fileno(), 1) + os.dup2(self.stream.fileno(), 2)
+
+ + + +
+[docs] +class instance: + """ + Process management for a single flow instance. + start and stop instances. + + this is the main entry point launched from the sr3 cli, with arguments for it to turn into a specific configuration. + """ +
+[docs] + def __init__(self): + self.running_instance = None + original_sigint = signal.getsignal(signal.SIGINT)
+ + + def stop_signal(self, signum, stack): + logging.info('signal %d received' % signum) + + # stack trace dump from: https://stackoverflow.com/questions/132058/showing-the-stack-trace-from-a-running-python-application + if self.o.debug: + logger.debug("when debug is on, we generate stack trace below to help debugging, but note that nothing has failed") + id2name = dict([(th.ident, th.name) for th in threading.enumerate()]) + code = [] + for threadId, stack in sys._current_frames().items(): + code.append("\n# Thread: %s(%d)" % (id2name.get(threadId,""), threadId)) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + logging.debug('\n'.join(code)) + self.running_instance.please_stop() + +
+[docs] + def start(self): + """ + Main element to run a single flow instance. It parses the command line arguments twice. + the first pass, is to initialize the log file and debug level, and select the configuration file to parse. + Once the log file is set, and output & error re-direction is in place, the second pass begins: + + The configuration files are parsed, and then the options are parsed a second time to act + as overrides to the configuration file content. + + As all process management is handled by sr.py, the *action* here is not parsed, but always either + *start* (daemon) or *foreground* (interactive) + + """ + global logger + + logging.basicConfig( + format= + '%(asctime)s [%(levelname)s] %(process)d %(name)s %(funcName)s %(message)s', + level=logging.INFO) + + # FIXME: honour SR_ variable for moving preferences... + default_cfg_dir = sarracenia.user_config_dir( + sarracenia.config.Config.appdir_stuff['appname'], + sarracenia.config.Config.appdir_stuff['appauthor']) + + cfg_preparse=sarracenia.config.Config( \ + { + 'acceptUnmatched':True, 'exchange':None, 'inline':False, 'inlineEncoding':'guess', 'logStdout': False, + } ) + + defconfig = default_cfg_dir + os.sep + "default.conf" + if os.path.exists(defconfig): + cfg_preparse.parse_file(defconfig) + cfg_preparse.parse_args() + + #cfg_preparse.dump() + + if cfg_preparse.action not in ['foreground', 'start']: + logger.error('action must be one of: foreground or start') + return + + if cfg_preparse.debug: + logLevel = logging.DEBUG + elif hasattr(cfg_preparse, 'logLevel'): + logLevel = getattr(logging, cfg_preparse.logLevel.upper()) + else: + logLevel = logging.INFO + + logger.setLevel(logLevel) + + if not hasattr(cfg_preparse, + 'no') and not (cfg_preparse.action == 'foreground'): + logger.critical('need an instance number to run.') + return + + if (len(cfg_preparse.configurations) > 1 ) and \ + ( cfg_preparse.configurations[0].split(os.sep)[0] != 'post' ): + logger.critical("can only run one configuration in an instance") + return + + if (not os.sep in cfg_preparse.configurations[0]): + component = 'flow' + config = cfg_preparse.configurations[0] + else: + component, config = cfg_preparse.configurations[0].split(os.sep) + + cfg_preparse = sarracenia.config.one_config(component, config, cfg_preparse.action) + + if cfg_preparse.logRotateInterval < (24*60*60): + logRotateInterval=int(cfg_preparse.logRotateInterval) + lr_when='s' + else: + logRotateInterval = int(cfg_preparse.logRotateInterval/(24*60*60)) + lr_when='midnight' + + + # init logs here. need to know instance number and configuration and component before here. + if cfg_preparse.action in ['start','run'] : + if cfg_preparse.statehost: + hostdir = cfg_preparse.hostdir + else: + hostdir = None + + metricsfilename = sarracenia.config.get_metrics_filename( hostdir, component, config, cfg_preparse.no) + + dir_not_there = not os.path.exists(os.path.dirname(metricsfilename)) + while dir_not_there: + try: + os.makedirs(os.path.dirname(metricsfilename), exist_ok=True) + dir_not_there = False + except FileExistsError: + dir_not_there = False + except Exception as ex: + logging.error( "makedirs {} failed err={}".format(os.path.dirname(metricsfilename),ex)) + logging.debug("Exception details:", exc_info=True) + + cfg_preparse.metricsFilename = metricsfilename + + if not cfg_preparse.logStdout: + + logfilename = sarracenia.config.get_log_filename( hostdir, component, config, cfg_preparse.no) + + dir_not_there = not os.path.exists(os.path.dirname(logfilename)) + while dir_not_there: + try: + os.makedirs(os.path.dirname(logfilename), exist_ok=True) + dir_not_there = False + except FileExistsError: + dir_not_there = False + except Exception as ex: + logging.error( "makedirs {} failed err={}".format(os.path.dirname(logfilename),ex)) + logging.debug("Exception details:", exc_info=True) + time.sleep(0.1) + + log_format = '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s' + if logging.getLogger().hasHandlers(): + for h in logging.getLogger().handlers: + h.close() + logging.getLogger().removeHandler(h) + logger = logging.getLogger() + logger.setLevel(logLevel) + + handler = RedirectedTimedRotatingFileHandler( + logfilename, + when=lr_when, + interval=logRotateInterval, + backupCount=cfg_preparse.logRotateCount) + handler.setFormatter(logging.Formatter(log_format)) + + logger.addHandler(handler) + + if hasattr(cfg_preparse, 'permLog'): + os.chmod(logfilename, cfg_preparse.permLog) + + # FIXME: https://docs.python.org/3/library/contextlib.html portable redirection... + if sys.platform != 'win32': + os.dup2(handler.stream.fileno(), 1) + os.dup2(handler.stream.fileno(), 2) + + else: + logger.setLevel(logLevel) + + signal.signal(signal.SIGTERM, self.stop_signal) + signal.signal(signal.SIGINT, self.stop_signal) + + if cfg_preparse.statehost: + hostdir = cfg_preparse.hostdir + else: + hostdir = None + + pidfilename = sarracenia.config.get_pid_filename( hostdir, component, config, cfg_preparse.no) + + if not os.path.isdir(os.path.dirname(pidfilename)): + pathlib.Path(os.path.dirname(pidfilename)).mkdir(parents=True, exist_ok=True) + + with open(pidfilename, 'w') as pfn: + pfn.write('%d' % os.getpid()) + + cfg = sarracenia.config.one_config(component, config, cfg_preparse.action) + + cfg.novipFilename = pidfilename.replace(".pid", ".noVip") + + if not hasattr(cfg, 'env_declared'): + sys.exit(0) + + for n in cfg.env_declared: + os.environ[n] = cfg.env[n] + os.putenv(n, cfg.env[n]) + + self.o = cfg + self.running_instance = Flow.factory(cfg) + + self.running_instance.run() + + if os.path.isfile(pidfilename): + os.unlink(pidfilename) + sys.exit(0)
+
+ + + +if __name__ == '__main__': + i = instance() + i.start() +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/moth.html b/_modules/sarracenia/moth.html new file mode 100644 index 000000000..acf43dc19 --- /dev/null +++ b/_modules/sarracenia/moth.html @@ -0,0 +1,546 @@ + + + + + + sarracenia.moth — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.moth

+import copy
+import json
+import logging
+import sys
+import time
+from sarracenia.featuredetection import features
+
+import sarracenia
+
+logger = logging.getLogger(__name__)
+
+default_options = {
+    'acceptUnmatched': True,
+    'batch': 100,
+    'bindings': [],
+    'broker': None,
+    'dry_run': False,
+    'exchange': 'xpublic',
+    'expire': 300,
+    'inline': False,
+    'inlineEncoding': 'guess',
+    'inlineByteMax': 4096,
+    'logFormat':
+    '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s',
+    'logLevel': 'info',
+    'messageDebugDump': False,
+    'message_strategy': {
+        'reset': True,
+        'stubborn': True,
+        'failure_duration': '5m'
+    },
+    'message_ttl': 0,
+    'topicPrefix': ['v03'],
+    'tlsRigour': 'normal'
+}
+
+def ProtocolPresent(p) -> bool:
+    if ( p[0:4] in ['amqp'] ) and sarracenia.features['amqp']['present']:
+       return True
+    if ( p[0:4] in ['mqtt'] ) and sarracenia.features['mqtt']['present']:
+       return True
+    if p in sarracenia.features:
+        logger.critical( f"support for {p} missing, please install python packages: {' '.join(sarracenia.features[p]['modules_needed'])}" )
+    else:
+        logger.critical( f"Protocol scheme {p} unsupported for communications with message brokers" )
+
+    return False
+
+
+
+[docs] +class Moth(): + r""" + Moth ... Messages Organized by Topic Headers + (en français: Messages organisés par thème hierarchique. ) + + A multi-protocol library for use by hierarchical message passing implementations, + (messages which have a 'topic' header that is used for routing by brokers.) + + - regardless of protocol, the message format returned should be the same. + - the message is turned into a sarracenia.Message object, which acts like a python + dictionary, corresponding to key-value pairs in the message body, and properties. + - topic is a special key that may end up in the message body, or some sort of property + or metadata. + - the protocol should support acknowledgement under user control. Such control indicated + by the presence of an entry_point called "ack". The entry point accepts "ack_id" as + a message identifier to be passed to the broker. Whatever protocol symbol is used + by the protocol, it is passed through this message property. Examples: + in rabbitmq/amqp ack takes a "delivery_tag" as an argument, in MQTT, it takes a "message-id" + so when receiving an AMQP message, the m['ack_id'] is assigned the delivery_tag from the message. + - There is a special dict item: "_DeleteOnPost", + to identify keys which are added only for local use. + they will be removed from the message when publishing. + examples: topic (sent outside body), message-id (used for acknowledgements.) + new_basedir, ack_id, new\_... (settings...) + + Intent is to be specialized for topic based data distribution (MQTT style.) + API to allow pass-through of protocol specific properties, but apply templates for genericity. + + Target protocols (and corresponding libraries.): AMQP, MQTT, ? + + Things to specify: + + * broker + + * topicPrefix + + * subTopic + + * queueName (for amqp, used as client-id for mqtt) + + this library knows nothing about Sarracenia, the only code used from sarracenia is to interpret + duration properties, from the root sarracenia/__init__.py, the broker argument from sarracenia.credentials + + usage:: + + import sarracenia.moth + import sarracenia.credentials + + + props = sarracenia.moth.default_options + props['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca') + props['expire'] = 300 + props['batch'] = 1 + is_subscriber=True + + c= Moth( props, is_subscriber ) + + messages = c.newMessages() + + # if there are new messages from a publisher, return them, otherwise return + # an empty list []]. + + p=Moth( { 'batch':1 }, False ) + + p.putNewMessage() + + p.close() + # tear down connection. + + Initialize a broker connection. Connections are unidirectional. + either for subscribing (with subFactory) or publishing (with pubFactory.) + + The factories return objects subclassed to match the protocol required + by the broker argument. + + arguments to the factories are: + + * broker ... the url of the broker to connect to. + + * props is a dictionary or properties/parameters. + + * supplied as overrides to the default properties listed above. + + Some may vary among protocols:: + + Protocol library implementing URL to select + -------- -------------------- ------------- + + AMQPv0.9 --> amqplib from Celery --> amqp, amqps + + AMQPv0.9 --> pika --> pika, pikas + + MQTTv3 --> paho --> mqtt, mqtts + + AMQPv1.0 --> qpid-proton --> amq1, amq1s + + + + **messaging_strategy** + + how to manage the connection. Covers whether to treat the connection + as new or assume it is set up. Also, If something goes wrong. + What should be done. + + * reset: on startup... erase any state, and re-initialize. + + * stubborn: If set to True, loop forever if something bad happens. + Never give up. This sort of setting is desired in operations, especially unattended. + if set to False, may give up more easily. + + * failure_duration is to advise library how to structure connection service level. + + * 5m - make a connection that will recover from transient errors of a few minutes, + but not tax the broker too much for prolonged outages. + + * 5d - duration outage to striving to survive connection for five days. + + Changing recovery_strategy setting, might result in having to destroy and re-create + consumer queues (AMQP.) + + **Options** + + **both** + + * 'topicPrefix' : [ 'v03' ] + + * 'messageDebugDump': False, --> enable printing of raw messages. + + * 'inline': False, - Are we inlining content within messages? + + * 'inlineEncoding': 'guess', - what encoding should we use for inlined content? + + * 'inlineByteMax': 4096, - Maximum size of messages to inline. + + **for get** + + * 'batch' : 100 # how many messages to get at once + + * 'broker' : an sr_broker ? + + * 'queueName' : Mandatory, name of a queue. (only in AMQP... hmm...) + + * 'bindings' : [ list of bindings ] + + * 'loop' + + **optional:** + + * 'message_ttl' + + **for put:** + + * 'exchange' (only in AMQP... hmm...) + + + """ + @staticmethod + def subFactory(props) -> 'Moth': + + if not props['broker'] : + logger.error('no broker specified') + return None + + if not hasattr(props['broker'],'url'): + logger.error('invalid broker url') + return None + + if not ProtocolPresent(props['broker'].url.scheme): + logger.error('unknown broker scheme/protocol specified') + return None + + for sc in Moth.findAllSubclasses(Moth): + driver=sc.__name__.lower() + # when amqp_consumer option is True, use the moth.AMQPConsumer class, not normal moth.AMQP + if "amqp_consumer" in props and props["amqp_consumer"]: + if driver == 'amqp': + continue + if driver == 'amqpconsumer': + # driver needs to be amqp to match with the broker URL's scheme + driver = 'amqp' + scheme=props['broker'].url.scheme + if (scheme == driver) or \ + ( (scheme[0:-1] == driver) and (scheme[-1] in [ 's', 'w' ])) or \ + ( (scheme[0:-2] == driver) and (scheme[-2] == 'ws')): + return sc(props, True) + logger.error('broker intialization failure') + return None + + @staticmethod + def pubFactory(props) -> 'Moth': + if not props['broker']: + logger.error('no broker specified') + return None + + if not hasattr(props['broker'],'url'): + logger.error('invalid broker url') + return None + + if not ProtocolPresent(props['broker'].url.scheme): + logger.error('unknown broker scheme/protocol specified') + return None + + for sc in Moth.__subclasses__(): + driver=sc.__name__.lower() + scheme=props['broker'].url.scheme + if (scheme == driver) or \ + ( (scheme[0:-1] == driver) and (scheme[-1] in [ 's', 'w' ])) or \ + ( (scheme[0:-2] == driver) and (scheme[-2] == 'ws')): + return sc(props, False) + + # ProtocolPresent test should ensure that we never get here... + logger.error('broker intialization failure') + return None + +
+[docs] + @staticmethod + def findAllSubclasses(cls) -> set: + """Recursively finds all subclasses of a class. __subclasses__() only gives direct subclasses. + """ + cls_subclasses = set(cls.__subclasses__()) + for sc in cls_subclasses: + cls_subclasses = cls_subclasses.union(Moth.findAllSubclasses(sc)) + return cls_subclasses
+ + +
+[docs] + def __init__(self, props=None, is_subscriber=True) -> None: + """ + If is_subscriber=True, then this is a consuming instance. + expect calls to get* routines. + + if is_subscriber=False, then expect/permit only calls to put* + """ + + self.is_subscriber = is_subscriber + self.connected=False + self.please_stop = False + self.metrics = { 'connected': False } + self.metricsReset() + + if (sys.version_info.major == 3) and (sys.version_info.minor < 7): + self.o = {} + for k in default_options: + if k == 'masks': + self.o[k] = default_options[k] + else: + self.o[k] = copy.deepcopy(default_options[k]) + else: + self.o = copy.deepcopy(default_options) + + if props is not None: + self.o.update(props) + + me = 'sarracenia.moth.Moth' + + # apply settings from props. + if 'settings' in self.o: + if me in self.o['settings']: + for s in self.o['settings'][me]: + self.o[s] = self.o['settings'][me][s] + + logging.basicConfig(format=self.o['logFormat'], + level=getattr(logging, self.o['logLevel'].upper()))
+ + +
+[docs] + def ack(self, message: sarracenia.Message ) -> None: + """ + tell broker that a given message has been received. + + ack uses the 'ack_id' property to send an acknowledgement back to the broker. + """ + logger.error("ack unimplemented")
+ + + @property + def default_options(self) -> dict: + """ + get default properties to override, used by client for validation. + + """ + return Moth.__default_options + +
+[docs] + def getNewMessage(self) -> sarracenia.Message: + """ + If there is one new message available, return it. Otherwise return None. Do not block. + + side effects: + metrics. + self.metrics['RxByteCount'] should be incremented by size of payload. + self.metrics['RxGoodCount'] should be incremented by 1 if a good message is received. + self.metrics['RxBadCount'] should be incremented by 1 if an invalid message is received (&discarded.) + """ + logger.error("getNewMessage unimplemented") + return None
+ + +
+[docs] + def newMessages(self) -> list: + """ + If there are new messages available from the broker, return them, otherwise return None. + + On Success, this routine returns immediately (non-blocking) with either None, or a list of messages. + + On failure, this routine blocks, and loops reconnecting to broker, until interaction with broker is successful. + + """ + logger.error("NewMessages unimplemented") + return []
+ + +
+[docs] + def putNewMessage(self, message:sarracenia.Message, content_type: str ='application/json', exchange: str = None) -> bool: + """ + publish a message as set up to the given topic. + + return True is succeeded, False otherwise. + + side effect + self.metrics['TxByteCount'] should be incremented by size of payload. + self.metrics['TxGoodCount'] should be incremented by 1 if a good message is received. + self.metrics['TxBadCount'] should be incremented by 1 if an invalid message is received (&discarded.) + """ + logger.error("implementation missing!") + return False
+ + + def metricsReset(self) -> None: + self.metrics['disconnectLast'] = 0 + self.metrics['disconnectTime'] = 0 + self.metrics['disconnectCount'] = 0 + self.metrics['rxByteCount'] = 0 + self.metrics['rxGoodCount'] = 0 + self.metrics['rxBadCount'] = 0 + self.metrics['txByteCount'] = 0 + self.metrics['txGoodCount'] = 0 + self.metrics['txBadCount'] = 0 + + def metricsReport(self) -> tuple: + if not self.metrics['connected'] and (self.metrics['disconnectLast'] > 0): + down_time = time.time() - self.metrics['disconnectLast'] + self.metrics['disconnectTime'] += down_time + + return self.metrics + + def metricsConnect(self) -> None: + self.metrics['connected']=True + if self.metrics['disconnectLast'] > 0 : + down_time = time.time() - self.metrics['disconnectLast'] + self.metrics['disconnectTime'] += down_time + +
+[docs] + def metricsDisconnect(self) -> None: + """ + tear down an existing connection. + """ + self.metrics['connected']=False + self.metrics['disconnectCount'] += 1 + self.metrics['disconnectLast'] = time.time()
+ + + def close(self) -> None: + logger.error("implementation missing!") + +
+[docs] + def cleanup(self) -> None: + """ + get rid of server-side resources associated with a client. (queues/id's, etc...) + """ + if self.is_subscriber: + self.getCleanUp() + else: + self.putCleanUp()
+
+ + +if features['amqp']['present']: + import sarracenia.moth.amqp + import sarracenia.moth.amqpconsumer + +if features['mqtt']['present']: + import sarracenia.moth.mqtt +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/moth/amq1.html b/_modules/sarracenia/moth/amq1.html new file mode 100644 index 000000000..d37a20cb0 --- /dev/null +++ b/_modules/sarracenia/moth/amq1.html @@ -0,0 +1,130 @@ + + + + + + sarracenia.moth.amq1 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.moth.amq1

+from sarracenia.moth import Moth
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class AMQ1(Moth): +
+[docs] + def __init__(self, broker, props, is_subscriber): + """ + AMQP 1.0 library to be built with libqpid-proton (the only free amqp 1.0 library around.) + + stub, not implemented + """ + super().__init__(broker, props, is_subscriber) + + logger.error( + "__init__ AMQP 1.0 Moth using qpid-proton library: not implemented" + )
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/moth/amqp.html b/_modules/sarracenia/moth/amqp.html new file mode 100644 index 000000000..2fcfcbc06 --- /dev/null +++ b/_modules/sarracenia/moth/amqp.html @@ -0,0 +1,933 @@ + + + + + + sarracenia.moth.amqp — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.moth.amqp

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2020
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+# the real AMQP library... not this one...
+import amqp
+import copy
+import json
+
+import logging
+
+import re
+
+import sarracenia
+from sarracenia.postformat import PostFormat
+from sarracenia.moth import Moth
+import signal
+import os
+
+import time
+from urllib.parse import unquote
+
+logger = logging.getLogger(__name__)
+"""
+amqp_ss_maxlen 
+
+the maximum length of a "short string", as per AMQP protocol, in bytes.
+
+"""
+amqp_ss_maxlen = 255
+
+default_options = {
+    'auto_delete': False,
+    'batch': 25,
+    'durable': True,
+    'exchange': None,
+    'exchangeDeclare': True,
+    'expire': None,
+    'logLevel': 'info',
+    'persistent': True,
+    'prefetch': 25,
+    'queueName': None,
+    'queueBind': True,
+    'queueDeclare': True,
+    'reset': False,
+    'subtopic': [],
+    'messageDebugDump': False,
+    'topicPrefix': ['v03'],
+    'vhost': '/',
+}
+
+
+
+[docs] +class AMQP(Moth): + """ + implementation of the Moth API for the amqp library, which is built to talk to rabbitmq brokers in 0.8 and 0.9 + AMQP dialects. + + to allow acknowledgements we map: AMQP' 'delivery_tag' to the 'ack_id' + + additional AMQP specific options: + + exchangeDeclare - declare exchanges before use. + queueBind - bind queue to exchange before use. + queueDeclare - declare queue before use. + + """ + + def _msgRawToDict(self, raw_msg) -> sarracenia.Message: + if raw_msg is not None: + body = raw_msg.body + + if self.o['messageDebugDump']: + logger.info('raw message start') + if not ('content_type' in raw_msg.properties): + logger.warning('message is missing content-type header') + if body: + logger.info('body: type: %s (%d bytes) %s' % + (type(body), len(body), body)) + else: + logger.info('had no body') + if raw_msg.headers: + logger.info('headers: type: %s (%d elements) %s' % + (type(raw_msg.headers), len(raw_msg.headers), raw_msg.headers)) + else: + logger.info('had no headers') + if raw_msg.properties: + logger.info('properties:' % raw_msg.properties) + else: + logger.info('had no properties') + if raw_msg.delivery_info: + logger.info( f"delivery info: {raw_msg.delivery_info}" ) + else: + logger.info('had no delivery info') + logger.info('raw message end') + + + + if type(body) is bytes: + try: + body = raw_msg.body.decode("utf8") + except Exception as ex: + logger.error( + 'ignoring message. UTF8 encoding expected. raw message received: %s' % ex) + logger.debug('Exception details: ', exc_info=True) + self.channel.basic_ack( raw_msg.delivery_info['delivery_tag']) + return None + + if 'content_type' in raw_msg.properties: + content_type = raw_msg.properties['content_type'] + else: + content_type = None + + msg = PostFormat.importAny( body, raw_msg.headers, content_type, self.o ) + if not msg: + logger.error('Decode failed, discarding message') + self.channel.basic_ack( raw_msg.delivery_info['delivery_tag']) + return None + + topic = raw_msg.delivery_info['routing_key'].replace( + '%23', '#').replace('%22', '*') + msg['exchange'] = raw_msg.delivery_info['exchange'] + source=None + if 'source' in self.o: + source = self.o['source'] + elif 'sourceFromExchange' in self.o and self.o['sourceFromExchange']: + itisthere = re.match( "xs_([^_]+)_.*", msg['exchange'] ) + if itisthere: + source = itisthere[1] + else: + itisthere = re.match( "xs_([^_]+)", msg['exchange'] ) + if itisthere: + source = itisthere[1] + if 'source' in msg and 'sourceFromMessage' in self.o and self.o['sourceFromMessage']: + pass + elif source: + msg['source'] = source + msg['_deleteOnPost'] |= set(['source']) + + msg_topic = topic.split('.') + + # topic validation... deal with DMS topic scheme. https://github.com/MetPX/sarracenia/issues/1017 + if 'topicCopy' in self.o and self.o['topicCopy']: + topicOverride=True + else: + topicOverride=False + if 'relPath' in msg: + path_topic = self.o['topicPrefix'] + os.path.dirname(msg['relPath']).split('/') + + if msg_topic != path_topic: + topicOverride=True + + # set subtopic if possible. + if msg_topic[0:len(self.o['topicPrefix'])] == self.o['topicPrefix']: + msg['subtopic'] = msg_topic[len(self.o['topicPrefix']):] + else: + topicOverride=True + + if topicOverride: + msg['topic'] = topic + msg['_deleteOnPost'] |= set( ['topic'] ) + + msg['ack_id'] = raw_msg.delivery_info['delivery_tag'] + msg['local_offset'] = 0 + msg['_deleteOnPost'] |= set( ['ack_id', 'exchange', 'local_offset', 'subtopic']) + if not msg.validate(): + if hasattr(self,'channel'): + self.channel.basic_ack(msg['ack_id']) + logger.error('message acknowledged and discarded: %s' % msg) + msg = None + else: + msg = None + + return msg + + # length of an AMQP short string (used for headers and many properties) + amqp_ss_maxlen = 255 + +
+[docs] + def __init__(self, props, is_subscriber) -> None: + """ + connect to broker, depending on message_strategy stubborness, remain connected. + + """ + + super().__init__(props, is_subscriber) + + self.last_qDeclare = time.time() + + logging.basicConfig( + format= + '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s') + + self.o = copy.deepcopy(default_options) + self.o.update(props) + + self.first_setup = True + self.please_stop = False + + me = "%s.%s" % (__class__.__module__, __class__.__name__) + + if ('settings' in self.o) and (me in self.o['settings']): + for s in self.o['settings'][me]: + self.o[s] = self.o['settings'][me][s] + + if 'logLevel' in self.o['settings'][me]: + logger.setLevel(self.o['logLevel'].upper()) + + self.connection = None
+ + def __connect(self, broker) -> bool: + """ + connect to broker. + returns True if connected, false otherwise. + * side effect: self.channel set to a new channel. + + Expect caller to handle errors. + """ + if broker.url.hostname: + host = broker.url.hostname + if broker.url.port is None: + if (broker.url.scheme[-1] == 's'): + host += ':5671' + else: + host += ':5672' + else: + host += ':{}'.format(broker.url.port) + else: + logger.critical( f"invalid broker specification: {broker} " ) + return False + + # if needed, set the vhost using the broker URL's path + vhost = self.o['vhost'] + # if the URL path is '/' or '', no vhost is specified and the default vhost from self.o + # will be used. Otherwise, strip off leading or trailing slashes. + if broker.url.path != '/' and broker.url.path != '': + vhost = broker.url.path.strip('/') + + self.connection = amqp.Connection(host=host, + userid=broker.url.username, + password=unquote( + broker.url.password), + login_method=broker.login_method, + virtual_host=vhost, + ssl=(broker.url.scheme[-1] == 's')) + if hasattr(self.connection, 'connect'): + # check for amqp 1.3.3 and 1.4.9 because connect doesn't exist in those older versions + self.connection.connect() + + self.management_channel = self.connection.channel(1) + self.channel = self.connection.channel(2) + return True + + def _amqp_setup_signal_handler(self, signum, stack): + logger.info("ok, asked to stop") + self.please_stop=True + + def metricsReport(self): + + if 'no' in self.o and self.o['no'] < 2 and self.is_subscriber and self.connection and self.connection.connected: + # control frequency of checks for queue size. hardcoded for now. + next_time = self.last_qDeclare + 30 + now=time.time() + if next_time <= now: + #self._queueDeclare(passive=True) + self.last_qDeclare=now + + super().metricsReport() + + return self.metrics + + def _queueDeclare(self,passive=False) -> int: + + try: + # from sr_consumer.build_connection... + if not self.__connect(self.o['broker']): + logger.critical('could not connect') + if hasattr(self,'metrics'): + self.metrics['brokerQueuedMessageCount'] = -2 + return -2 + + #FIXME: test self.first_setup and props['reset']... delete queue... + broker_str = self.o['broker'].url.geturl().replace( + ':' + self.o['broker'].url.password + '@', '@') + + if self.o['queueDeclare'] and self.o['queueName']: + + args = {} + if self.o['expire']: + x = int(self.o['expire'] * 1000) + if x > 0: args['x-expires'] = x + if self.o['message_ttl']: + x = int(self.o['message_ttl'] * 1000) + if x > 0: args['x-message-ttl'] = x + + #FIXME: conver expire, message_ttl to proper units. + if self.o['dry_run']: + logger.info('queue declare (dry run) %s (as: %s) ' % + (self.o['queueName'], broker_str)) + msg_count=0 + else: + qname, msg_count, consumer_count = self.management_channel.queue_declare( + self.o['queueName'], + passive=passive, + durable=self.o['durable'], + exclusive=False, + auto_delete=self.o['auto_delete'], + nowait=False, + arguments=args) + if not passive: + logger.info( f"queue declared {self.o['queueName']} (as: {broker_str}), (messages waiting: {msg_count})" ) + + if hasattr(self,'metrics'): + self.metrics['brokerQueuedMessageCount'] = msg_count + return msg_count + + except Exception as err: + logger.error( + f'connecting to: {self.o["queueName"]}, durable: {self.o["durable"]}, expire: {self.o["expire"]}, auto_delete={self.o["auto_delete"]}' + ) + logger.error("AMQP getSetup failed to {} with {}".format( + self.o['broker'].url.hostname, err)) + logger.debug('Exception details: ', exc_info=True) + + if hasattr(self,'metrics'): + self.metrics['brokerQueuedMessageCount'] = -1 + return -1 + + +
+[docs] + def getSetup(self) -> None: + """ + Setup so we can get messages. + + if message_strategy is stubborn, will loop here forever. + connect, declare queue, apply bindings. + """ + + ebo = 1 + original_sigint = signal.getsignal(signal.SIGINT) + original_sigterm = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, self._amqp_setup_signal_handler) + signal.signal(signal.SIGTERM, self._amqp_setup_signal_handler) + + while True: + + if self.please_stop: + break + + if 'broker' not in self.o or self.o['broker'] is None: + logger.critical( f"no broker given" ) + break + + # It does not really matter how it fails, the recovery approach is always the same: + # tear the whole thing down, and start over. + try: + # from sr_consumer.build_connection... + if not self.__connect(self.o['broker']): + logger.critical('could not connect') + break + + # only first/lead instance needs to declare a queue and bindings. + if 'no' in self.o and self.o['no'] >= 2: + self.metricsConnect() + return + + #logger.info('getSetup connected to {}'.format(self.o['broker'].url.hostname) ) + + if self.o['prefetch'] != 0: + self.channel.basic_qos(0, self.o['prefetch'], True) + + #FIXME: test self.first_setup and props['reset']... delete queue... + broker_str = self.o['broker'].url.geturl().replace( + ':' + self.o['broker'].url.password + '@', '@') + + # from Queue declare + msg_count = self._queueDeclare() + + if msg_count == -2: break + + if self.o['queueBind'] and self.o['queueName']: + for tup in self.o['bindings']: + exchange, prefix, subtopic = tup + topic = '.'.join(prefix + subtopic) + if self.o['dry_run']: + logger.info('binding (dry run) %s with %s to %s (as: %s)' % \ + ( self.o['queueName'], topic, exchange, broker_str ) ) + else: + logger.info('binding %s with %s to %s (as: %s)' % \ + ( self.o['queueName'], topic, exchange, broker_str ) ) + if exchange: + self.management_channel.queue_bind(self.o['queueName'], exchange, + topic) + + # Setup Successfully Complete! + self.metricsConnect() + logger.debug('getSetup ... Done!') + break + + except Exception as err: + logger.error( + f'connecting to: {self.o["queueName"]}, durable: {self.o["durable"]}, expire: {self.o["expire"]}, auto_delete={self.o["auto_delete"]}' + ) + logger.error("AMQP getSetup failed to {} with {}".format( + self.o['broker'].url.hostname, err)) + logger.debug('Exception details: ', exc_info=True) + + if not self.o['message_strategy']['stubborn']: return + + if ebo < 60: ebo *= 2 + + logger.info("Sleeping {} seconds ...".format(ebo)) + time.sleep(ebo) + + signal.signal(signal.SIGINT, original_sigint) + signal.signal(signal.SIGTERM, original_sigterm) + if self.please_stop: + os.kill(os.getpid(), signal.SIGINT)
+ + + def putSetup(self) -> None: + + ebo = 1 + original_sigint = signal.getsignal(signal.SIGINT) + original_sigterm = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, self._amqp_setup_signal_handler) + signal.signal(signal.SIGTERM, self._amqp_setup_signal_handler) + + while True: + + # It does not really matter how it fails, the recovery approach is always the same: + # tear the whole thing down, and start over. + try: + if self.please_stop: + break + + if self.o['broker'] is None: + logger.critical( f"no broker given" ) + break + + if not self.__connect(self.o['broker']): + logger.critical('could not connect') + break + + # transaction mode... confirms would be better... + self.channel.tx_select() + broker_str = self.o['broker'].url.geturl().replace( + ':' + self.o['broker'].url.password + '@', '@') + + #logger.debug('putSetup ... 1. connected to {}'.format(broker_str ) ) + + if self.o['exchangeDeclare']: + logger.debug('putSetup ... 1. declaring {}'.format( + self.o['exchange'])) + if type(self.o['exchange']) is not list: + self.o['exchange'] = [self.o['exchange']] + for x in self.o['exchange']: + if self.o['dry_run']: + logger.info('exchange declare (dry run): %s (as: %s)' % + (x, broker_str)) + else: + self.channel.exchange_declare( + x, + 'topic', + auto_delete=self.o['auto_delete'], + durable=self.o['durable']) + logger.info('exchange declared: %s (as: %s)' % + (x, broker_str)) + + # Setup Successfully Complete! + self.metricsConnect() + logger.debug('putSetup ... Done!') + break + + except Exception as err: + logger.error( + "AMQP putSetup failed to connect or declare exchanges {}@{} on {}: {}" + .format(self.o['exchange'], self.o['broker'].url.username, + self.o['broker'].url.hostname, err)) + logger.debug('Exception details: ', exc_info=True) + + if not self.o['message_strategy']['stubborn']: return + + if ebo < 60: ebo *= 2 + + self.close() + logger.info("Sleeping {} seconds ...".format(ebo)) + time.sleep(ebo) + + signal.signal(signal.SIGINT, original_sigint) + signal.signal(signal.SIGTERM, original_sigterm) + if self.please_stop: + os.kill(os.getpid(), signal.SIGINT) + + + def putCleanUp(self) -> None: + + try: + for x in self.o['exchange']: + try: + if self.o['dry_run']: + logger.info("deleted exchange (dry run): %s (if unused)" % x) + else: + if hasattr(self,'channel'): + self.channel.exchange_delete(x, if_unused=True) + logger.info("deleted exchange: %s" % x) + except amqp.exceptions.PreconditionFailed as err: + err_msg = str(err).replace("Exchange.delete: (406) PRECONDITION_FAILED - exchange ", "") + logger.warning("failed to delete exchange: %s" % err_msg) + except Exception as err: + logger.error("failed on {} with {}".format( + self.o['broker'].url.hostname, err)) + logger.debug('Exception details: ', exc_info=True) + + def getCleanUp(self) -> None: + + try: + if self.o['dry_run']: + logger.info("deleting queue (dry run) %s" % self.o['queueName'] ) + else: + logger.info("deleting queue %s" % self.o['queueName'] ) + if hasattr(self,'channel'): + self.channel.queue_delete(self.o['queueName']) + except Exception as err: + logger.error("failed to {} with {}".format( + self.o['broker'].url.hostname, err)) + logger.debug('Exception details: ', exc_info=True) + +
+[docs] + def newMessages(self) -> list: + + if not self.is_subscriber: #build_consumer + logger.error("getting from a publisher") + return [] + + ml = [] + m = self.getNewMessage() + if m is not None: + fetched = 1 + ml.append(m) + while fetched < self.o['batch']: + m = self.getNewMessage() + if m is None: + break + ml.append(m) + fetched += 1 + + return ml
+ + +
+[docs] + def getNewMessage(self) -> sarracenia.Message: + + if not self.is_subscriber: #build_consumer + logger.error("getting from a publisher") + return None + + try: + if not self.connection: + self.getSetup() + + raw_msg = self.channel.basic_get(self.o['queueName']) + if (raw_msg is None) and (self.connection.connected): + return None + else: + self.metrics['rxByteCount'] += len(raw_msg.body) + try: + msg = self._msgRawToDict(raw_msg) + except Exception as err: + logger.error("message decode failed. raw message: %s" % raw_msg.body ) + logger.debug('Exception details: ', exc_info=True) + msg = None + if msg is None: + self.metrics['rxBadCount'] += 1 + return None + else: + self.metrics['rxGoodCount'] += 1 + self.metrics['rxLast'] = sarracenia.nowstr() + if hasattr(self.o, 'fixed_headers'): + for k in self.o.fixed_headers: + msg[k] = self.o.fixed_headers[k] + + logger.debug("new msg: %s" % msg) + return msg + except Exception as err: + logger.warning("failed %s: %s" % + (self.o['queueName'], err)) + logger.debug('Exception details: ', exc_info=True) + + if not self.o['message_strategy']['stubborn']: + return None + + logger.warning('lost connection to broker') + self.close() + time.sleep(1) + return None
+ + +
+[docs] + def ack(self, m: sarracenia.Message) -> None: + """ + do what you need to acknowledge that processing of a message is done. + """ + if not self.is_subscriber: #build_consumer + logger.error("getting from a publisher") + return + + + # silent success. retry messages will not have an ack_id, and so will not require acknowledgement. + if not 'ack_id' in m: + #logger.warning( f"no ackid present" ) + return + + #logger.info( f"acknowledging {m['ack_id']}" ) + ebo = 1 + while True: + try: + if hasattr(self, 'channel'): + self.channel.basic_ack(m['ack_id']) + del m['ack_id'] + m['_deleteOnPost'].remove('ack_id') + # Break loop if no exceptions encountered + return + + except Exception as err: + logger.warning("failed for tag: %s: %s" % (m['ack_id'], err)) + logger.debug('Exception details: ', exc_info=True) + + # Cleanly close partially broken connection and restablish + self.close() + self.getSetup() + + if ebo < 60: ebo *= 2 + + logger.info( + "Sleeping {} seconds before re-trying ack...".format(ebo)) + time.sleep(ebo)
+ + +
+[docs] + def putNewMessage(self, + message: sarracenia.Message, + content_type: str = 'application/json', + exchange: str = None ) -> bool: + """ + put a new message out, to the configured exchange by default. + """ + + if self.is_subscriber: #build_consumer + logger.error("publishing from a consumer") + return False + + # Check connection and channel status, try to reconnect if not connected + if (not self.connection) or (not self.connection.connected) or (not self.channel.is_open): + try: + self.close() + self.putSetup() + except Exception as err: + logger.warning(f"failed, connection was closed/broken and could not be re-opened {exchange}: {err}") + logger.debug('Exception details: ', exc_info=True) + return False + + # The caller probably doesn't expect the message to get modified by this method, so use a copy of the message + body = copy.deepcopy(message) + + version = body['_format'] + + + if '_deleteOnPost' in body: + # FIXME: need to delete because building entire JSON object at once. + # makes this routine alter the message. Ideally, would use incremental + # method to build json and _deleteOnPost would be a guide of what to skip. + # library for that is jsonfile, but not present in repos so far. + for k in body['_deleteOnPost']: + if k in body: + del body[k] + del body['_deleteOnPost'] + + if not exchange: + if (type(self.o['exchange']) is list): + if (len(self.o['exchange']) > 1): + if ( 'exchangeSplit' in self.o) and self.o['exchangeSplit'] > 1: + # FIXME: assert ( len(self.o['exchange']) == self.o['post_exchangeSplit'] ) + # if that isn't true... then there is something wrong... should we check ? + if 'exchangeSplitOverride' in message: + idx = int(message['exchangeSplitOverride'])%len(self.o['exchange']) + else: + idx = sum( bytearray(body['identity']['value'], + 'ascii')) % len(self.o['exchange']) + + exchange = self.o['exchange'][idx] + else: + logger.error( + 'do not know which exchange to publish to: %s' % + self.o['exchange']) + return False + else: + exchange = self.o['exchange'][0] + else: + exchange = self.o['exchange'] + + if self.o['message_ttl']: + ttl = "%d" * int( + sarracenia.durationToSeconds(self.o['message_ttl']) * 1000) + else: + ttl = "0" + + if 'persistent' in self.o: + deliv_mode = 2 if self.o['persistent'] else 1 + else: + deliv_mode = 2 + + raw_body, headers, content_type = PostFormat.exportAny( body, version, self.o['topicPrefix'], self.o ) + + topic = '.'.join(headers['topic']) + topic = topic.replace('#', '%23') + topic = topic.replace('*', '%22') + + if len(topic) >= 255: # ensure topic is <= 255 characters + logger.error("message topic too long, truncating") + mxlen = amqp_ss_maxlen + while (topic.encode("utf8")[mxlen - 1] & 0xc0 == 0xc0): + mxlen -= 1 + topic = topic.encode("utf8")[0:mxlen].decode("utf8") + + if self.o['messageDebugDump']: + logger.info('raw message body: version: %s type: %s %s' % + (version, type(raw_body), raw_body)) + logger.info('raw message headers: type: %s value: %s' % (type(headers), headers)) + + message['post_topic'] = topic + message['post_exchange'] = exchange + message['_deleteOnPost'] |= set( ['post_exchange', 'post_topic'] ) + del headers['topic'] + + if headers : + for k in headers: + if (type(headers[k]) is str) and (len(headers[k]) >= + amqp_ss_maxlen): + logger.error("message header %s too long, dropping" % k) + return False + + AMQP_Message = amqp.Message(raw_body, + content_type=content_type, + application_headers=headers, + expire=ttl, + delivery_mode=deliv_mode) + self.metrics['txByteCount'] += len(raw_body) + if headers: + self.metrics['txByteCount'] += len(''.join(str(headers))) + self.metrics['txLast'] = sarracenia.nowstr() + + # timeout option is a float and default is 0.0. basic_publish wants int or None for no timeout + try: + if self.o['timeout']: + pub_timeout = int(self.o['timeout']) + else: + pub_timeout = None + except Exception as err: + logger.debug('Set pub_timeout to None. Exception details: ', exc_info=True) + pub_timeout = None + + body=raw_body + ebo = 1 + try: + logger.debug( f"trying to publish body: {body} headers: {headers} to {exchange} under: {topic} " ) + self.channel.basic_publish(AMQP_Message, exchange, topic, timeout=pub_timeout) + # Issue #732: tx_commit can get stuck forever + self.channel.tx_commit() + logger.debug("published body: {} headers: {} to {} under: {} ".format( + body, headers, exchange, topic)) + self.metrics['txGoodCount'] += 1 + return True # no failure == success :-) + + except Exception as err: + logger.warning("failed %s: %s" % (exchange, err)) + logger.debug('Exception details: ', exc_info=True) + + self.metrics['txBadCount'] += 1 + # Issue #466: commenting this out until message_strategy stubborn is working correctly (Issue #537) + # Always return False when an error occurs and use the DiskQueues to retry, instead of looping. This should + # eventually be configurable with message_strategy stubborn + # if True or not self.o['message_strategy']['stubborn']: + # return False + + self.close() + return False # instead of looping
+ + + def close(self) -> None: + try: + if self.connection: + self.connection.collect() + self.connection.close() + + except Exception as err: + logger.error("sr_amqp/close 2: {}".format(err)) + logger.debug("sr_amqp/close 2 Exception details:", exc_info=True) + # FIXME toclose not useful as we don't close channels anymore + self.metricsDisconnect() + self.connection = None
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/moth/pika.html b/_modules/sarracenia/moth/pika.html new file mode 100644 index 000000000..4864ef422 --- /dev/null +++ b/_modules/sarracenia/moth/pika.html @@ -0,0 +1,130 @@ + + + + + + sarracenia.moth.pika — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.moth.pika

+from sarracenia.moth import Moth
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class PIKA(Moth): + """ + moth subclass based on the pika AMQP/rabbitmq client library. + + stub: not implemented. + + """ +
+[docs] + def __init__(self, broker): + super().__init__(broker, props, is_subscriber) #FIXME unresolved reference to props and is_subscriber + + logger.error( + "__init__ AMQP 0.x (rabbitmq) using pika: not implemented ")
+
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/rabbitmq_admin.html b/_modules/sarracenia/rabbitmq_admin.html new file mode 100644 index 000000000..3d1e34a58 --- /dev/null +++ b/_modules/sarracenia/rabbitmq_admin.html @@ -0,0 +1,477 @@ + + + + + + sarracenia.rabbitmq_admin — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.rabbitmq_admin

+#!/usr/bin/env python3
+"""
+   rabbitmq administration bindings, to allow sr to invoke broker management functions.
+
+"""
+import sys
+import urllib, urllib.parse
+import base64
+import logging
+import os
+import re
+import socket
+import subprocess
+
+#rabbitmqadmin = '.' + os.sep + 'rabbitmqadmin'
+rabbitmqadmin = 'rabbitmqadmin'
+
+logger = logging.getLogger(__name__)
+
+#logger.setLevel( logging.DEBUG )
+
+#===========================
+# rabbitmqadmin
+#===========================
+
+
+
+[docs] +def exec_rabbitmqadmin(url, options, simulate=False): + """ + invoke rabbitmqadmin using a sub-process, with the given options. + """ + + try: + command = rabbitmqadmin + command += ' --host \'' + url.hostname + command += '\' --user \'' + url.username + command += '\' -p \'' + urllib.parse.unquote(url.password) + command += '\' --format raw_json ' + if url.scheme == 'amqps': + command += ' --ssl --port=15671 ' + command += ' ' + options + + logger.debug("command = %s" % command) + if sys.version_info.major < 3 or (sys.version_info.major == 3 + and sys.version_info.minor < 5): + if logger: logger.debug("using subprocess.getstatusoutput") + + if simulate: + print("dry_run: %s" % ' '.join(command)) + return 0, None + + return subprocess.getstatusoutput(command) + else: + cmdlin = command.replace("'", '') + cmdlst = cmdlin.split() + if logger: + logger.debug("using subprocess.run cmdlst=%s" % + ' '.join(cmdlst)) + + if simulate: + print("dry_run: %s" % cmdlin) + return 0, None + + rclass = subprocess.run(cmdlst, stdout=subprocess.PIPE) + if rclass.returncode == 0: + output = rclass.stdout + if type(output) == bytes: output = output.decode("utf-8") + return rclass.returncode, output + return rclass.returncode, None + except: + if sys.version_info.major < 3 or (sys.version_info.major == 3 + and sys.version_info.minor < 5): + if logger: logger.error("trying run command %s %s" % command) + else: + if logger: + logger.error("trying run command %s %s" % ' '.join(cmdlst)) + if logger: logger.debug('Exception details:', exc_info=True) + + return 0, None
+ + + +
+[docs] +def add_user(url, role, user, passwd, simulate): + """ + add the given user with the given credentials. + """ + + declare = "declare user name='%s' password=" % user + + if passwd != None: declare += "\'%s\'" % urllib.parse.unquote(passwd) + if role == 'admin': declare += " tags=administrator " + else: declare += ' tags="" ' + + dummy = run_rabbitmqadmin(url, declare, simulate) + + # admin and feeder gets the same permissions + + if role in ['admin,', 'feeder', 'manager']: + c = "configure=.*" + w = "write=.*" + r = "read=.*" + logger.info("permission user \'%s\' role %s %s %s %s " % + (user + '@' + url.hostname, 'feeder', c, w, r)) + declare = "declare permission vhost=/ user=%s %s %s %s" % (user, c, w, r) + dummy = run_rabbitmqadmin(url, declare, simulate) + return + + # source + + if role in ['source']: + c = "configure=^q_%s.*|^xs_%s.*" % (user, user) + w = "write=^q_%s.*|^xs_%s.*" % (user, user) + r = "read=^q_%s.*|^x[lrs]_%s.*|^x.*public$" % (user, user) + logger.info("permission user '%s' role %s %s %s %s " % + (user + '@' + url.hostname, 'source', c, w, r)) + declare = "declare permission vhost=/ user=%s %s %s %s" % (user, c, w, r) + dummy = run_rabbitmqadmin(url, declare, simulate) + return + + # subscribe + + if role in ['subscribe', 'subscriber']: + c = "configure=^q_%s.*" % user + w = "write=^q_%s.*|^xs_%s$" % (user, user) + r = "read=^q_%s.*|^x[lrs]_%s.*|^x.*public$" % (user, user) + logger.info("permission user '%s' role %s %s %s %s " % + (user + '@' + url.hostname, 'source', c, w, r)) + declare = "declare permission vhost=/ user=%s %s %s %s" % (user, c, w, r) + dummy = run_rabbitmqadmin(url, declare, simulate)
+ + + +
+[docs] +def del_user(url, user, simulate): + """ + delete user from the given broker. + """ + logger.info("deleting user %s" % user) + delete = "delete user name='%s'" % user + dummy = run_rabbitmqadmin(url, delete, simulate)
+ + + +
+[docs] +def get_exchanges(url): + """ + get the list of existing exchanges. + """ + logger.info("geting exchanges") + cmd = "list exchanges name" + return run_rabbitmqadmin(url, cmd)
+ + + +
+[docs] +def get_queues(url): + """ + get the list of existing queues. + """ + logger.info("geting queues") + cmd = "list queues name messages state" + return run_rabbitmqadmin(url, cmd)
+ + + +
+[docs] +def get_users(url): + """ + get the list of existing users. + """ + logger.info("geting users") + cmd = "list users name" + return run_rabbitmqadmin(url, cmd)
+ + + +#=========================== +# direct access to rabbitmq management plugin +# this is what rabbitmqadmin does under the cover +#=========================== + + +
+[docs] +def broker_get_exchanges(url, ssl_key_file=None, ssl_cert_file=None): + """ + get the list of existing exchanges using a url query. + """ + import http.client + method = "GET" + path = "/api/exchanges?columns=name" + + if url.scheme == 'amqps': + conn = http.client.HTTPSConnection(url.hostname, "15671", ssl_key_file, + ssl_cert_file) + else: + conn = http.client.HTTPConnection(url.hostname, "15672") + + bcredentials = bytes( + url.username + ':' + urllib.parse.unquote(url.password), "utf-8") + b64credentials = base64.b64encode(bcredentials).decode("ascii") + headers = {"Authorization": "Basic " + b64credentials} + + try: + conn.request(method, path, "", headers) + except socket.error as e: + print("Could not connect: {0}".format(e)) + + resp = conn.getresponse() + answer = resp.read() + if b'error' in answer[:5]: + print(answer) + return [] + + lst = eval(answer) + exchanges = [] + + for i in lst: + ex = i["name"] + if ex == '': continue + exchanges.append(ex) + + return exchanges
+ + + +
+[docs] +def user_access(url, user): + """ + Given an administrative URL, return a list of exchanges and queues the user can access. + + lox = list of exchanges, just a list of names. + loq = array of queues, where the value of each is the number of messages ready. + + return value:: + + { 'exchanges': { 'configure': lox, 'write': lox, 'read': lox }, + 'queues' : { 'configure': loq, 'write': loq, 'read': loq }, + 'bindings' : { <queue> : { 'exchange': <exchange> , 'key' : <routing_key> } } + } + """ + import json + import re + + found = False + for p in json.loads(exec_rabbitmqadmin(url, "list permissions")[1]): + if user == p['user']: + found = True + re_cf = re.compile(p['configure']) + re_wr = re.compile(p['write']) + re_rd = re.compile(p['read']) + + #exchanges = rabbitmq_broker_get_exchanges(url) + x_cf = [] + x_wr = [] + x_rd = [] + + for x in list( + map(lambda x: x['name'], + json.loads(exec_rabbitmqadmin(url, + "list exchanges name")[1]))): + #print( "x: %s\n" % x ) + if re_cf.match(x): + x_cf += [x] + continue + if re_wr.match(x): + x_wr += [x] + continue + if re_rd.match(x): + x_rd += [x] + continue + + q_cf = {} + q_wr = {} + q_rd = {} + + for qq in json.loads(exec_rabbitmqadmin(url, "list queues")[1]): + #print( "qq name=%s ready=%d\n\n" % (qq['name'], qq['messages_ready_ram']) ) + q = qq['name'] + nq = qq['messages_ready_ram'] + if re_cf.match(q): + q_cf[q] = nq + continue + if re_wr.match(q): + q_wr[q] = nq + continue + if re_rd.match(q): + q_rd[q] = nq + continue + + b = {} + for bb in json.loads(exec_rabbitmqadmin(url, "list bindings")[1]): + #print("\n binding: %s" % bb ) + if bb['source'] != '': + q = bb['destination'] + if (q in q_cf) or (q in q_wr) or (q in q_rd): + #print(" exchange: %s, queue: %s, topic: %s" % ( bb['source'], q, bb['routing_key'] ) ) + if not q in b: + b[q] = {'exchange': bb['source'], 'key': bb['routing_key']} + else: + b[q] += { + 'exchange': bb['source'], + 'key': bb['routing_key'] + } + + + return( { 'exchanges': { 'configure' : x_cf , 'write': x_wr, 'read': x_rd }, \ + 'queues': { 'configure' : q_cf , 'write': q_wr, 'read': q_rd }, \ + 'bindings': b } )
+ + + +if __name__ == "__main__": + url = urllib.parse.urlparse(sys.argv[1]) + print(exec_rabbitmqadmin(url, "list queue names")[1]) + + import json + + lex = list( + map(lambda x: x['name'], + json.loads(exec_rabbitmqadmin(url, "list exchanges name")[1]))) + print("exchanges: %s\n\n" % lex) + + u = 'tsource' + up = rabbitmq_user_access(url, u) + print("permissions for %s: \nqueues: %s\nexchanges: %s\nbindings %s" % + (u, up['queues'], up['exchanges'], up['bindings'])) + #print( "\n\nbindings: %s" % json.loads(exec_rabbitmqadmin(url,"list bindings")[1]) ) + + +
+[docs] +def run_rabbitmqadmin(url, options, simulate=False): + """ + spawn a subprocess to run rabbitmqadmin with the given options. + capture result. + """ + + logger.debug("sr_rabbit run_rabbitmqadmin %s" % options) + try: + (status, answer) = exec_rabbitmqadmin(url, options, simulate) + + if simulate: return + + if status != 0 or answer == None or len( + answer) == 0 or 'error' in answer: + logger.error("run_rabbitmqadmin invocation failed") + return [] + + if answer == None or len(answer) == 0: return [] + + lst = [] + try: + lst = eval(answer) + except: + pass + + return lst + + except: + logger.error("sr_rabbit/run_rabbitmqadmin failed with option '%s'" % + options) + logger.debug('Exception details: ', exc_info=True) + return []
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/transfer.html b/_modules/sarracenia/transfer.html new file mode 100644 index 000000000..a7db4cc7e --- /dev/null +++ b/_modules/sarracenia/transfer.html @@ -0,0 +1,586 @@ + + + + + + sarracenia.transfer — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.transfer

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+import calendar, datetime
+from hashlib import md5
+from hashlib import sha512
+import logging
+import os
+import random
+import signal
+import stat
+import sys
+import time
+import urllib
+import urllib.parse
+
+#from sarracenia.sr_xattr import *
+from sarracenia import nowflt, timestr2flt
+from sarracenia.featuredetection import features
+
+logger = logging.getLogger(__name__)
+
+#============================================================
+# sigalarm
+#============================================================
+
+
+
+[docs] +class TimeoutException(Exception): + """timeout exception""" + pass
+ + + +# alarm_cancel +def alarm_cancel(): + if sys.platform != 'win32': + signal.alarm(0) + + +# alarm_raise +def alarm_raise(n, f): + raise TimeoutException("signal alarm timed out") + + +# alarm_set +
+[docs] +def alarm_set(time): + """ + FIXME: replace with set itimer for > 1 second resolution... currently rouding to nearest second. + """ + + if sys.platform != 'win32': + signal.signal(signal.SIGALRM, alarm_raise) + signal.alarm(int(time + 0.5))
+ + + +# ========================================= +# sr_proto : one place for throttle, onfly checksum, buffer io timeout +# ========================================= + + +
+[docs] +class Transfer(): + """ + This is a sort of abstract base class for implementing transfer protocols. + Implemented subclasses include support for: local files, https, sftp, and ftp. + + This class has routines that do i/o given descriptors opened by the sub-classes, + so that each one does not need to re-implement copying, for example. + + Each subclass needs to implement the following routines: + + if downloading:: + + get ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 ) + getAccellerated( msg, remote_file, local_file, length ) + ls () + cd (dir) + delete (path) + + + if sending:: + + put ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 ) + putAccelerated ( msg, remote_file, local_file, length=0 ) + cd (dir) + mkdir (dir) + umask () + chmod (perm) + rename (old,new) + + Note that the ls() call returns are polymorphic. One of: + + * a dictionary where the key is the name of the file in the directory, + and the value is an SFTPAttributes structure for if (from paramiko.) + (sftp.py as an example) + * a dictionary where the key is the name of the file, and the value is a string + that looks like the output of a linux ls command. + (ftp.py as an example.) + * a sequence of bytes... will be parsed as an html page. + (https.py as an example) + + The first format is the vastly preferred one. The others are fallbacks when the first + is not available. + The flowcb/poll/__init__.py lsdir() routing will turn ls tries to transform any of + these return values into the first form (a dictionary of SFTPAttributes) + Each SFTPAttributes structure needs st_mode set, and folders need stat.S_IFDIR set. + + if the lsdir() routine gets a sequence of bytes, the on_html_page() and on_html_parser_init(, + or perhaps handle_starttag(..) and handle_data() routines) will be used to turn them into + the first form. + + web services with different such formats can be accommodated by subclassing and overriding + the handle_* entry points. + + uses options (on Sarracenia.config data structure passed to constructor/factory.) + * credentials - used to authentication information. + * sendTo - server to connect to. + * batch - how many files to transfer before a connection is torn down and re-established. + * permDefault - what permissions to set on files transferred. + * permDirDefault - what permission to set on directories created. + * timeout - how long to wait for operations to complete. + * byteRateMax - maximum transfer rate (throttle to avoid exceeding) + * bufsize - size of buffers for file transfers. + + """ + @staticmethod + def factory(proto, options) -> 'Transfer': + + for sc in Transfer.__subclasses__(): + if (hasattr(sc, 'registered_as') + and (proto in sc.registered_as())): + return sc(proto, options) + return None + +
+[docs] + def __init__(self, proto, options): + + self.o = options + if 'sarracenia.transfer.Transfer' in self.o.settings and 'logLevel' in self.o.settings: + ll = self.o.settings['sarracenia.transfer.Transfer']['logLevel'] + else: + ll = self.o.logLevel + + logger.setLevel(getattr(logging, ll.upper())) + + logger.debug("class=%s , subclasses=%s" % + (type(self).__name__, Transfer.__subclasses__())) + self.init()
+ + + def init(self): + self.sumalgo = None + self.checksum = None + self.data_sumalgo = None + self.data_checksum = None + self.fpos = 0 + self.tbytes = 0 + self.tbegin = nowflt() + self.byteRate = 0 + + def local_read_close(self, src): + #logger.debug("sr_proto local_read_close") + + src.close() + + # finalize checksum + + if self.sumalgo: self.checksum = self.sumalgo.value + if self.data_sumalgo: + self.data_checksum = self.data_sumalgo.value + + def local_read_open(self, local_file, local_offset=0): + logger.debug("sr_proto local_read_open getcwd=%s self.cwd=%s" % + (os.getcwd(), self.getcwd())) + + self.checksum = None + + # local_file opening and seeking if needed + + src = open(local_file, 'rb') + if local_offset != 0: src.seek(local_offset, 0) + + # initialize sumalgo + + if self.sumalgo: self.sumalgo.set_path(local_file) + if self.data_sumalgo: self.data_sumalgo.set_path(local_file) + + return src + + def local_write_close(self, dst): + + # flush sync (make sure all io done) + + dst.flush() + os.fsync(dst) + + # flush,sync, remember current position, truncate = no sparce, close + + self.fpos = dst.tell() + dst.truncate() + dst.close() + + # finalize checksum + + if self.sumalgo: self.checksum = self.sumalgo.value + if self.data_sumalgo: + self.data_checksum = self.data_sumalgo.value + + def local_write_open(self, local_file, local_offset=0): + #logger.debug("sr_proto local_write_open") + + # reset ckecksum, fpos + + self.checksum = None + self.fpos = 0 + + # local_file has to exists + + if not os.path.isfile(local_file): + dst = open(local_file, 'w') + dst.close() + + # local_file opening and seeking if needed + + dst = open(local_file, 'r+b') + if local_offset != 0: dst.seek(local_offset, 0) + + return dst + +
+[docs] + def on_data(self, chunk) -> bytes: + """ + transform data as it is being read. + Given a buffer, return the transformed buffer. + Checksum calculation is based on pre transformation... likely need + a post transformation value as well. + """ + + return chunk
+ + + #FIXME ... need to re-enable on_data plugins... not sure how they should work. + # sub-classing of transfer class? + + def read_write(self, src, dst, length=0): + logger.debug("sr_proto read_write") + + # reset speed + + rw_length = 0 + self.tbytes = 0.0 + self.tbegin = nowflt() + + # length = 0, transfer entire remote file to local file + + if length == 0: + while True: + if self.o.timeout: alarm_set(self.o.timeout) + chunk = src.read(self.o.bufsize) + if chunk: + new_chunk = self.on_data(chunk) + dst.write(new_chunk) + rw_length += len(chunk) + alarm_cancel() + if not chunk: break + if self.sumalgo: self.sumalgo.update(chunk) + self.throttle(chunk) + return rw_length + + # exact length to be transfered + + nc = int(length / self.o.bufsize) + r = length % self.o.bufsize + + # read/write bufsize "nc" times + + i = 0 + while i < nc: + if self.o.timeout: alarm_set(self.o.timeout) + chunk = src.read(self.o.bufsize) + if chunk: + new_chunk = self.on_data(chunk) + rw_length += len(new_chunk) + dst.write(new_chunk) + alarm_cancel() + if not chunk: break + if self.sumalgo: self.sumalgo.update(chunk) + self.throttle(chunk) + i = i + 1 + + # remaining + + if r > 0: + if self.o.timeout: alarm_set(self.o.timeout) + chunk = src.read(r) + if chunk: + new_chunk = self.on_data(chunk) + rw_length += len(new_chunk) + dst.write(new_chunk) + alarm_cancel() + if self.sumalgo: self.sumalgo.update(chunk) + self.throttle(chunk) + + return rw_length + + def read_writelocal(self, + src_path, + src, + local_file, + local_offset=0, + length=0, exactLength=False): + #logger.debug("sr_proto read_writelocal") + + # open + dst = self.local_write_open(local_file, local_offset) + + # initialize sumalgo + + if self.sumalgo: self.sumalgo.set_path(src_path) + if self.data_sumalgo: self.data_sumalgo.set_path(src_path) + + # copy source to sendTo + + # 2022/12/02 - pas - need copies to always work... + # in HPC mirroring case, a lot of short files, likely length is wrong in announcements. + # grab the whole file unconditionally for now, detect error using mismatch. + rw_length = self.read_write(src, dst, length if exactLength else 0) + + # close + self.local_write_close(dst) + + # warn if length mismatch without transformation. + # 2022/12/02 - pas should see a lot of these messages in HPC case from now on... + + if not self.o.acceptSizeWrong and length != 0 and rw_length != length: + logger.warning( + "util/writelocal mismatched file length writing %s. Message said to expect %d bytes. Got %d bytes." + % (local_file, length, rw_length)) + + return rw_length + + def readlocal_write(self, local_file, local_offset=0, length=0, dst=None): + logger.debug("sr_proto readlocal_write") + + # open + src = self.local_read_open(local_file, local_offset) + + # copy source to sendTo + + rw_length = self.read_write(src, dst, length) + + # close + + self.local_read_close(src) + + # warn if length mismatch without transformation. + + # FIXME: 2020/09 - commented out for now... unsure about this. + #if (not self.o.on_data_list) and length != 0 and rw_length != length : + # logger.error("util/readlocal mismatched file length reading %s. Message announced it as %d bytes, but read %d bytes " % (local_file,length,rw_length)) + + # 2022/12/02 - pas attempting to get files that get shorter addressed. + if ((length==0) or (rw_length < length)) and hasattr(dst,'truncate'): + dst.truncate(rw_length) + + return rw_length + + def set_sumalgo(self, sumalgo): + logger.debug("sr_proto set_sumalgo %s" % sumalgo) + self.sumalgo = sarracenia.identity.Identity.factory(sumalgo) + self.data_sumalgo = sarracenia.identity.Identity.factory(sumalgo) + + def set_sumArbitrary(self, value): + self.sumalgo.value = value + self.data_sumalgo.value = value + + def update_file(self, path): + if self.sumalgo: + self.sumalgo.update_file(path) + if self.data_sumalgo: + self.data_sumalgo.update_file(path) + + def set_path(self, path): + if self.sumalgo: + self.sumalgo.set_path(path) + if self.data_sumalgo: + self.data_sumalgo.set_path(path) + + def get_sumstr(self) -> dict: + if self.sumalgo: + #return { 'method':type(self.sumalgo).__name__, 'value':self.sumalgo.value } + return { + 'method': self.sumalgo.get_method(), + 'value': self.sumalgo.value + } + else: + return None + + def metricsReport(self): + return { 'byteRateInstant': self.byteRate } + + # throttle + def throttle(self, buf): + self.tbytes = self.tbytes + len(buf) + rspan = nowflt() - self.tbegin + if rspan > 0: + self.byteRate = self.tbytes/rspan + + if hasattr(self.o,'byteRateMax') and self.o.byteRateMax and self.o.byteRateMax > 0: + span = self.tbytes / self.o.byteRateMax + if span > rspan: + stime = span - rspan + if stime > 10: + logger.info( f"exceeded byteRateMax: {self.o.byteRateMax} sleeping for {stime:.2f}") + time.sleep(stime) + + # write_chunk + def write_chunk(self, chunk): + if self.chunk_iow: self.chunk_iow.write(chunk) + self.rw_length += len(chunk) + alarm_cancel() + if self.sumalgo: self.sumalgo.update(chunk) + self.throttle(chunk) + if self.o.timeout: alarm_set(self.o.timeout) + + # write_chunk_end + def write_chunk_end(self): + alarm_cancel() + self.chunk_iow = None + return self.rw_length + + # write_chunk_init + def write_chunk_init(self, proto): + self.chunk_iow = proto + self.tbytes = 0.0 + self.tbegin = nowflt() + self.rw_length = 0 + if self.o.timeout: alarm_set(self.o.timeout) + + def gethttpsUrl(self, path): + return None
+ + +# batteries included. +import sarracenia.transfer.file +import sarracenia.transfer.ftp +import sarracenia.transfer.https + +if features['sftp']['present']: + import sarracenia.transfer.sftp + +if features['s3']['present']: + import sarracenia.transfer.s3 + +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/transfer/file.html b/_modules/sarracenia/transfer/file.html new file mode 100644 index 000000000..d5f92dab3 --- /dev/null +++ b/_modules/sarracenia/transfer/file.html @@ -0,0 +1,497 @@ + + + + + + sarracenia.transfer.file — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.transfer.file

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, Environment Canada, 2008-2015
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+from sarracenia.transfer import Transfer
+
+import sarracenia
+
+import os, stat, subprocess, sys, time
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+#============================================================
+# file protocol in sarracenia supports/uses :
+#
+# connect
+# close
+#
+# if a source    : get    (remote,local)
+#                  ls     ()
+#                  cd     (dir)
+#                  delete (path)
+#
+# require   logger
+#           options.credentials
+#           options.sendTo
+#           options.batch
+#           options.permDefault
+#           options.permDirDefault
+#     opt   options.byteRateMax
+#     opt   options.bufsize
+
+
+
+[docs] +class File(Transfer): + """ + Transfer sub-class for local file i/o. + + """ +
+[docs] + def __init__(self, proto, options): + super().__init__(proto, options) + + self.o.add_option("accelCpCommand", "str", "/usr/bin/cp %s %d") + logger.debug("sr_file __init__") + self.cwd = None
+ + + def registered_as(): + return ['file'] + + # cd +
+[docs] + def cd(self, path): + """ + proto classes are used for remote sessions, so this + cd is for REMOTE directory... when file remote as a protocol it is for the source. + should not change the "local" working directory when downloading. + """ + logger.debug("sr_file cd %s" % path) + #os.chdir(path) + self.cwd = path + self.path = path
+ + + def check_is_connected(self): + return True + + # chmod + def chmod(self, perm, path): + logger.debug("sr_file chmod %s %s" % ("{0:o}".format(perm), path)) + os.chmod(path, perm) + + # close + def close(self): + logger.debug("sr_file close") + return + + # connect + def connect(self): + logger.debug("sr_file connect %s" % self.o.sendTo) + + self.recursive = True + self.connected = True + + return True + + # delete + def delete(self, path): + p = os.path.join( self.cwd, path ) + logger.debug("sr_file rm %s" % p) + os.unlink(p) + + # get + def get(self, + msg, + remote_file, + local_file, + remote_offset=0, + local_offset=0, + length=0, exactLength=False): + + remote_path = self.cwd + os.sep + remote_file + + logger.debug( "get %s %s (cwd: %s) %d" % (remote_path,local_file,os.getcwd(), local_offset)) + + if not os.path.exists(remote_path): + logger.warning("file to read not found %s" % (remote_path)) + return -1 + + src = self.local_read_open(remote_path, remote_offset) + dst = self.local_write_open(local_file, local_offset) + + # initialize sumalgo + if self.sumalgo: self.sumalgo.set_path(remote_file) + + # download + rw_length = self.read_write(src, dst, length) + + # close + self.local_write_close(dst) + + return rw_length + + def getAccelerated(self, msg, remote_file, local_file, length=0, remote_offset=0, exactLength=False): + + base_url = msg['baseUrl'].replace('file:', '') + if base_url[-1] == '/': + base_url = base_url[0:-1] + arg1 = base_url + self.cwd + os.sep + remote_file + arg1 = arg1.replace(' ', '\\ ') + arg2 = local_file + + cmd = self.o.accelCpCommand.replace('%s', arg1) + cmd = cmd.replace('%d', arg2).split() + + logger.info("accel_cp: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + return -1 + sz = os.stat(arg2).st_size + return sz + + def getcwd(self): + return self.cwd + + # ls + def ls(self): + logger.debug("sr_file ls") + self.entries = {} + self.root = self.path + self.ls_python(self.path) + return self.entries + + def ls_python(self, dpath): + for x in os.listdir(dpath): + dst = dpath + '/' + x + if os.path.isdir(dst): + if self.recursive: self.ls_python(dst) + continue + relpath = dst.replace(self.root, '', 1) + if relpath[0] == '/': relpath = relpath[1:] + + self.entries[relpath] = sarracenia.stat(dst)
+ + + +# file_insert +# called by file_process (general file:// processing) + + +def file_insert(options, msg): + logger.debug("file_insert") + + fp = open(msg['relPath'], 'rb') + if msg.partflg == 'i': fp.seek(msg['offset'], 0) + + ok = file_write_length(fp, msg, options.bufsize, msg.filesize, options) + + fp.close() + + return ok + + +def file_link(msg): + + try: + os.unlink(msg['new_file']) + except: + pass + try: + os.link(msg['fileOp']['link'], os.path.join(self.cwd,msg['new_file'])) + except: + return False + + msg.compute_local_checksum() + msg.onfly_checksum = "{},{}".format(msg.sumflg, msg.local_checksum) + + return True + + +# file_process (general file:// processing) + + +def file_process(options): + logger.debug("file_process") + + msg = options.msg + + # FIXME - MG - DOMINIC's LOCAL FILE MIRRORING BUG CASE + # say file.txt does not exist + # sequential commands in script + # touch file.txt + # mv file.txt newfile.txt + # under libsrshim generate 3 amqp messages : + # 1- download/copy file.txt + # 2- move message 1 : remove file.txt with newname newfile.txt + # 3- move message 2 : download newfile.txt with oldname file.txt + # message (1) will never be processed fast enough ... and will fail + # message (2) removing of a file not there is considered successfull + # message (3) is the one that will guaranty the the newfile.txt is there and mirroring is ok. + # + # message (1) fails.. in previous version a bug was preventing an error (and causing file.txt rebirth with size 0) + # In current version, returning that this message fails would put it under the retry process for ever and for nothing. + # I decided for the moment to warn and to return success... it preserves old behavior without the 0 byte file generated + + if not os.path.isfile(msg['relPath']): + logger.warning("%s moved or removed since announced" % msg['relPath']) + return True + + try: + curdir = self.cwd + except: + curdir = None + + if curdir != options.msg['new_dir']: + os.chdir(options.msg['new_dir']) + + # try link if no inserts + + p=os.path.join(self.cwd,msg['relPath']) + + if msg.partflg == '1' or \ + (msg.partflg == 'p' and msg.in_partfile) : + ok = file_link(msg) + if ok: + if options.delete: + try: + os.unlink(p) + except: + logger.error("delete of link to %s failed" % p) + return ok + + # This part is for 2 reasons : insert part + # or copy file if preceeding link did not work + try: + ok = file_insert(options, msg) + if options.delete: + if msg.partflg.startswith('i'): + logger.info("delete unimplemented for in-place part files %s" % + (msg['relPath'])) + else: + try: + os.unlink(p) + except: + logger.error("delete of %s after copy failed" % p) + + if ok: return ok + + except: + logger.error('sr_file/file_process error') + logger.debug('Exception details: ', exc_info=True) + + logger.error("could not copy %s in %s" % (p, msg['new_file'])) + + return False + + +# file_write_length +# called by file_process->file_insert (general file:// processing) + + +def file_write_length(req, msg, bufsize, filesize, options): + logger.debug("file_write_length") + + msg.onfly_checksum = None + + chk = msg.sumalgo + logger.debug("file_write_length chk = %s" % chk) + if chk: chk.set_path(msg['new_file']) + + # file should exists + if not os.path.isfile(msg['new_file']): + fp = open(msg['new_file'], 'w') + fp.close() + + # file open read/modify binary + fp = open(msg['new_file'], 'r+b') + if msg.local_offset != 0: fp.seek(msg.local_offset, 0) + + nc = int(msg['length'] / bufsize) + r = msg['length'] % bufsize + + # read/write bufsize "nc" times + i = 0 + while i < nc: + chunk = req.read(bufsize) + fp.write(chunk) + if chk: chk.update(chunk) + i = i + 1 + + # remaining + if r > 0: + chunk = req.read(r) + fp.write(chunk) + if chk: chk.update(chunk) + + if fp.tell() >= msg.filesize: + fp.truncate() + + fp.close() + + h = options.msg.headers + if options.permCopy and 'mode' in h: + try: + mod = int(h['mode'], base=8) + except: + mod = 0 + if mod > 0: os.chmod(msg['new_file'], mod) + + if options.timeCopy and 'mtime' in h and h['mtime']: + os.utime(msg['new_file'], + times=(timestr2flt(h['atime']), timestr2flt(h['mtime']))) + + if chk: + msg.onfly_checksum = "{},{}".format(chk.registered_as(), chk.value) + + return True + + +# file_truncate +# called under file_reassemble (itself and its file_insert_part) +# when inserting lastchunk, file may need to be truncated + + +def file_truncate(options, msg): + + # will do this when processing the last chunk + # whenever that is + + if (not options.randomize) and (not msg.lastchunk): return + + try: + lstat = sarracenia.stat(msg['target_file']) + fsiz = lstat.st_size + + if fsiz > msg.filesize: + fp = open(msg['target_file'], 'r+b') + fp.truncate(msg.filesize) + fp.close() + + msg['subtopic'] = msg['relPath'].split(os.sep)[1:-1] + msg['_deleteOnPost'] |= set(['subtopic']) + #msg.set_topic(options.post_topicPrefix,msg.target_relpath) + + except: + pass +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/transfer/ftp.html b/_modules/sarracenia/transfer/ftp.html new file mode 100644 index 000000000..286a39322 --- /dev/null +++ b/_modules/sarracenia/transfer/ftp.html @@ -0,0 +1,556 @@ + + + + + + sarracenia.transfer.ftp — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.transfer.ftp

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2021
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+
+import ftplib, os, subprocess, sys, time
+import logging
+from sarracenia.transfer import Transfer
+from sarracenia.transfer import alarm_cancel, alarm_set, alarm_raise
+from urllib.parse import unquote
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Ftp(Transfer): + """ + File Transfer Protocol (FTP) ( https://datatracker.ietf.org/doc/html/rfc959 ) + sarracenia transfer protocol subclass supports/uses additional custom options: + + * accelFtpputCommand (default: '/usr/bin/ncftpput %s %d' ) + * accelFtpgetCommand (default: '/usr/bin/ncftpget %s %d' ) + + built using: ftplib ( https://docs.python.org/3/library/ftplib.html ) + """ +
+[docs] + def __init__(self, proto, options): + + super().__init__(proto, options) + + self.o.add_option('accelFtpputCommand', 'str', + '/usr/bin/ncftpput %s %d') + self.o.add_option('accelFtpgetCommand', 'str', + '/usr/bin/ncftpget %s %d') + + logger.debug("sr_ftp __init__") + self.connected = False + self.ftp = None + self.details = None + self.batch = 0
+ + + def registered_as(): + return ['ftp'] + + # cd + def cd(self, path): + logger.debug("sr_ftp cd %s" % path) + + alarm_set(self.o.timeout) + self.ftp.cwd(self.originalDir) + self.ftp.cwd(path) + self.pwd = path + alarm_cancel() + + def cd_forced(self, perm, path): + logger.debug("sr_ftp cd_forced %d %s" % (perm, path)) + + # try to go directly to path + + alarm_set(self.o.timeout) + self.ftp.cwd(self.originalDir) + try: + self.ftp.cwd(path) + alarm_cancel() + return + except: + pass + alarm_cancel() + + # need to create subdir + + subdirs = path.split("/") + if path[0:1] == "/": subdirs[0] = "/" + subdirs[0] + + for d in subdirs: + if d == '': continue + # try to go directly to subdir + try: + alarm_set(self.o.timeout) + self.ftp.cwd(d) + alarm_cancel() + continue + except: + pass + + # create + alarm_set(self.o.timeout) + self.ftp.mkd(d) + alarm_cancel() + + # chmod + alarm_set(self.o.timeout) + self.ftp.voidcmd('SITE CHMOD ' + "{0:o}".format(perm) + ' ' + d) + alarm_cancel() + + # cd + alarm_set(self.o.timeout) + self.ftp.cwd(d) + alarm_cancel() + + # check_is_connected + + def check_is_connected(self): + logger.debug("sr_ftp check_is_connected") + + if self.ftp == None: return False + if not self.connected: return False + + if self.sendTo != self.o.sendTo: + self.close() + return False + + self.batch = self.batch + 1 + if self.batch > self.o.batch: + self.close() + return False + + # really connected + try: + cwd = self.getcwd() + except: + self.close() + return False + + return True + + # chmod + def chmod(self, perm, path): + logger.debug("sr_ftp chmod %s %s" % (str(perm), path)) + alarm_set(self.o.timeout) + self.ftp.voidcmd('SITE CHMOD ' + "{0:o}".format(perm) + ' ' + path) + alarm_cancel() + + # close + def close(self): + logger.debug("sr_ftp close") + + old_ftp = self.ftp + + self.init() + + try: + alarm_set(self.o.timeout) + old_ftp.quit() + except: + pass + alarm_cancel() + + # connect... + def connect(self): + logger.debug("sr_ftp connect %s" % self.o.sendTo) + + self.connected = False + self.sendTo = self.o.sendTo + + if not self.credentials(): return False + + # timeout alarm 100 secs to connect + alarm_set(self.o.timeout) + try: + expire = -999 + if self.o.timeout: expire = self.o.timeout + if self.port == '' or self.port == None: self.port = 21 + + if not self.tls: + ftp = ftplib.FTP() + ftp.encoding = 'utf-8' + ftp.connect(self.host, self.port, timeout=expire) + ftp.login(self.user, unquote(self.password)) + else: + # ftplib supports FTPS with TLS + ftp = ftplib.FTP_TLS(self.host, + self.user, + unquote(self.password), + timeout=expire) + ftp.encoding = 'utf-8' + if self.prot_p: ftp.prot_p() + # needed only if prot_p then set back to prot_c + #else : ftp.prot_c() + + ftp.set_pasv(self.passive) + + self.originalDir = '.' + + try: + self.originalDir = ftp.pwd() + except: + logger.warning("Unable to ftp.pwd") + logger.debug('Exception details: ', exc_info=True) + + self.pwd = self.originalDir + + self.connected = True + + self.ftp = ftp + + #alarm_cancel() + return True + + except: + logger.error("Unable to connect to %s (user:%s)" % + (self.host, self.user)) + logger.debug('Exception details: ', exc_info=True) + + alarm_cancel() + return False + + # credentials... + def credentials(self): + logger.debug("sr_ftp credentials %s" % self.sendTo) + + try: + ok, details = self.o.credentials.get(self.sendTo) + if details: url = details.url + + self.host = url.hostname + self.port = url.port + self.user = url.username + self.password = url.password + + self.passive = details.passive + self.binary = details.binary + self.tls = details.tls + self.prot_p = details.prot_p + + return True + + except: + logger.error( + "sr_ftp/credentials: unable to get credentials for %s" % + self.sendTo) + logger.debug('Exception details: ', exc_info=True) + + return False + + # delete + def delete(self, path): + logger.debug("sr_ftp rm %s" % path) + alarm_set(self.o.timeout) + # if delete does not work (file not found) run pwd to see if connection is ok + try: + self.ftp.delete(path) + except: + d = self.ftp.pwd() + alarm_cancel() + + # get + def get(self, + msg, + remote_file, + local_file, + remote_offset=0, + local_offset=0, + length=0, exactLength=False): + logger.debug("sr_ftp get %s %s %d" % + (remote_file, local_file, local_offset)) + + # open local file + dst = self.local_write_open(local_file, local_offset) + + # initialize sumalgo + if self.sumalgo: self.sumalgo.set_path(remote_file) + + # download + self.write_chunk_init(dst) + if self.binary: + self.ftp.retrbinary('RETR ' + remote_file, self.write_chunk, + self.o.bufsize) + else: + self.ftp.retrlines('RETR ' + remote_file, self.write_chunk) + rw_length = self.write_chunk_end() + + # close + self.local_write_close(dst) + + return rw_length + + def getAccelerated(self, msg, remote_file, local_file, length=0, remote_offset=0, exactLength=False): + + base_url = msg['baseUrl'] + if base_url[-1] == '/': + base_url = base_url[0:-1] + arg1 = base_url + self.pwd + os.sep + remote_file + arg1 = arg1.replace(' ', '\\ ') + arg2 = local_file + + cmd = self.o.accelFtpgetCommand.replace('%s', arg1) + cmd = cmd.replace('%d', arg2).split() + + logger.info("accel_ftp: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + return -1 + sz = os.stat(arg2).st_size + return sz + + # getcwd + def getcwd(self): + alarm_set(self.o.timeout) + pwd = self.ftp.pwd() + alarm_cancel() + return pwd + + # ls + def ls(self): + logger.debug("sr_ftp ls") + self.entries = {} + alarm_set(self.o.timeout) + self.ftp.retrlines('LIST', self.line_callback) + alarm_cancel() + logger.debug("sr_ftp ls = (size: %d) %s ..." % (len(self.entries), str(self.entries)[0:255])) + return self.entries + + # line_callback: entries[filename] = 'stripped_file_description' + def line_callback(self, iline): + #logger.debug("sr_ftp line_callback %s" % iline) + + alarm_cancel() + + oline = iline + oline = oline.strip('\n') + oline = oline.strip() + oline = oline.replace('\t', ' ') + opart1 = oline.split(' ') + opart2 = [] + + for p in opart1: + if p == '': continue + opart2.append(p) + + # else case is in the event of unlikely race condition + + # on linux, there are 8 fields, with spaces, perhaps more... + if len(opart2) > 7: + # university of Wisconsin as an ftp server that has an extra auth field. + if opart2[4].isnumeric(): # normal linux case. + fil = ' '.join(opart2[8:]) + else: # U. Wisconsin case. + fil = ' '.join(opart2[9:]) + else: + # guess it is on windows... + fil = ' '.join(opart2[3:]) + + line = ' '.join(opart2) + + self.entries[fil] = line + + alarm_set(self.o.timeout) + + # mkdir + def mkdir(self, remote_dir): + logger.debug("sr_ftp mkdir %s" % remote_dir) + alarm_set(self.o.timeout) + self.ftp.mkd(remote_dir) + alarm_cancel() + alarm_set(self.o.timeout) + self.ftp.voidcmd('SITE CHMOD ' + + "{0:o}".format(self.o.permDirDefault) + ' ' + + remote_dir) + alarm_cancel() + + # put + def put(self, + msg, + local_file, + remote_file, + local_offset=0, + remote_offset=0, + length=0): + logger.debug("sr_ftp put %s %s" % (local_file, remote_file)) + + # open + src = self.local_read_open(local_file, local_offset) + + # upload + self.write_chunk_init(None) + if self.binary: + self.ftp.storbinary("STOR " + remote_file, src, self.o.bufsize, + self.write_chunk) + else: + self.ftp.storlines("STOR " + remote_file, src, self.write_chunk) + rw_length = self.write_chunk_end() + + # close + self.local_read_close(src) + + return rw_length + + def putAccelerated(self, msg, local_file, remote_file, length=0): + + dest_baseUrl = self.o.sendTo + if dest_baseUrl[-1] == '/': + dest_baseUrl = dest_baseUrl[0:-1] + arg2 = dest_baseUrl + msg['new_dir'] + os.sep + remote_file + arg2 = arg2.replace(' ', '\\ ') + arg1 = local_file + + cmd = self.o.accelFtpputCommand.replace('%s', arg1) + cmd = cmd.replace('%d', arg2).split() + + logger.info("accel_ftp: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + return -1 + # FIXME: faking success... not sure how to check really. + sz = int(msg['size']) + return sz + + # rename + def rename(self, remote_old, remote_new): + logger.debug("sr_ftp rename %s %s" % (remote_old, remote_new)) + alarm_set(self.o.timeout) + self.ftp.rename(remote_old, remote_new) + alarm_cancel() + + # rmdir + def rmdir(self, path): + logger.debug("sr_ftp rmdir %s" % path) + alarm_set(self.o.timeout) + self.ftp.rmd(path) + alarm_cancel() + + # umask + def umask(self): + logger.debug("sr_ftp umask") + alarm_set(self.o.timeout) + self.ftp.voidcmd('SITE UMASK 777') + alarm_cancel()
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/transfer/https.html b/_modules/sarracenia/transfer/https.html new file mode 100644 index 000000000..69aac68d3 --- /dev/null +++ b/_modules/sarracenia/transfer/https.html @@ -0,0 +1,479 @@ + + + + + + sarracenia.transfer.https — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.transfer.https

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2021
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+import logging
+import os
+import sarracenia
+import ssl
+import subprocess
+import sys
+
+from sarracenia.transfer import Transfer
+from sarracenia.transfer import alarm_cancel, alarm_set, alarm_raise
+
+import urllib.error, urllib.parse, urllib.request
+from urllib.parse import unquote
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Https(Transfer): + """ + HyperText Transfer Protocol (HTTP) ( https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol ) + sarracenia transfer protocol subclass supports/uses additional custom options: + + * accelWgetCommand (default: '/usr/bin/wget %s -o - -O %d' ) + + built with: + urllib.request ( https://docs.python.org/3/library/urllib.request.html ) + """ +
+[docs] + def __init__(self, proto, options): + + super().__init__(proto, options) + + self.o.add_option('accelWgetCommand', 'str', '/usr/bin/wget %s -o - -O %d') + + logger.debug("sr_http __init__") + + self.tlsctx = ssl.create_default_context() + if hasattr(self.o, 'tlsRigour'): + self.o.tlsRigour = self.o.tlsRigour.lower() + if self.o.tlsRigour == 'lax': + self.tlsctx = ssl.create_default_context() + self.tlsctx.check_hostname = False + self.tlsctx.verify_mode = ssl.CERT_NONE + + elif self.o.tlsRigour == 'strict': + self.tlsctx = ssl.SSLContext(ssl.PROTOCOL_TLS) + self.tlsctx.options |= ssl.OP_NO_TLSv1 + self.tlsctx.options |= ssl.OP_NO_TLSv1_1 + self.tlsctx.check_hostname = True + self.tlsctx.verify_mode = ssl.CERT_REQUIRED + self.tlsctx.load_default_certs() + # TODO Find a way to reintroduce certificate revocation (CRL) in the future + # self.tlsctx.verify_flags = ssl.VERIFY_CRL_CHECK_CHAIN + # https://github.com/MetPX/sarracenia/issues/330 + elif self.o.tlsRigour == 'normal': + pass + else: + logger.warning( + "option tlsRigour must be one of: lax, normal, strict") + + self.init()
+ + + def registered_as(): + return ['http', 'https'] + + # cd + def cd(self, path): + logger.debug("sr_http cd %s" % path) + self.cwd = os.path.dirname(path) + self.path = path + + # for compatibility... always new connection with http + def check_is_connected(self): + logger.debug("sr_http check_is_connected") + + if not self.connected : return False + + if self.sendTo != self.o.sendTo: + self.close() + return False + + return True + + # close + def close(self): + logger.debug("sr_http close") + self.init() + + # connect... + def connect(self): + logger.debug("sr_http connect %s" % self.o.sendTo) + + if self.connected: self.close() + + self.connected = False + self.sendTo = self.o.sendTo + self.timeout = self.o.timeout + + if not self.credentials(): return False + + return True + + # credentials... + def credentials(self): + logger.debug("sr_http credentials %s" % self.sendTo) + + try: + ok, details = self.o.credentials.get(self.sendTo) + if details: url = details.url + + self.user = url.username if url.username != '' else None + self.password = url.password if url.password != '' else None + self.bearer_token = details.bearer_token if hasattr( + details, 'bearer_token') else None + + return True + + except: + logger.error( + "sr_http/credentials: unable to get credentials for %s" % + self.sendTo) + logger.debug('Exception details: ', exc_info=True) + + return False + + # get + def get(self, + msg, + remote_file, + local_file, + remote_offset=0, + local_offset=0, + length=0, exactLength=False): + logger.debug("get %s %s %d" % (remote_file, local_file, local_offset)) + logger.debug("sr_http self.path %s" % self.path) + + # open self.http + + if 'retrievePath' in msg: + url = self.sendTo + '/' + msg['retrievePath'] + else: + u = urllib.parse.urlparse( self.sendTo ) + url = u.scheme + '://' + u.netloc + '/' + urllib.parse.quote(self.path + '/' + + remote_file, safe='/+') + + ok = self.__open__(url, remote_offset, length) + + if not ok: return False + + # read from self.http write to local_file + + rw_length = self.read_writelocal(remote_file, self.http, local_file, + local_offset, length, exactLength) + + return rw_length + + def getAccelerated(self, msg, remote_file, local_file, length, remote_offset=0, exactLength=False ): + + arg1 = msg['baseUrl'] + '/' + msg['relPath'] + arg1 = arg1.replace(' ', '\\ ') + arg2 = local_file + + cmd = self.o.accelWgetCommand.replace('%s', arg1) + + cmd = cmd.replace('%d', arg2).split() + + if exactLength: + cmd = [cmd[0]] + [ f"--header=Range: bytes={remote_offset}-{length-1}" ] + cmd[1:] + else: + cmd = [cmd[0]] + cmd[1:] + + logger.info("accel_wget: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + logger.warning("binary accelerator %s returned: %d" % ( cmd, p.returncode ) ) + return -1 + # FIXME: length is not validated. + return length + + # init + def init(self): + Transfer.init(self) + + logger.debug("sr_http init") + self.connected = False + self.http = None + self.details = None + self.seek = True + + self.urlstr = '' + self.path = '' + self.cwd = '' + + self.data = '' + self.entries = {} + + +# ls + + def ls(self): + logger.debug("sr_http ls") + + # open self.http + + self.entries = {} + + url = self.sendTo + '/' + urllib.parse.quote(self.path, safe='/+') + + ok = self.__open__(url) + + if not ok: return self.entries + + # get html page for directory + + try: + dbuf = None + while True: + alarm_set(self.o.timeout) + chunk = self.http.read(self.o.bufsize) + alarm_cancel() + if not chunk: break + if dbuf: dbuf += chunk + else: dbuf = chunk + + #self.data = dbuf.decode('utf-8') + + # invoke option defined on_html_page ... if any + + #for plugin in self.o.on_html_page_list: + # if not plugin(self): + # logger.warning("something wrong") + # return self.entries + + except: + logger.warning("sr_http/ls: unable to open %s" % self.urlstr) + logger.debug('Exception details: ', exc_info=True) + + return dbuf + + # open + def __open__(self, path, remote_offset=0, length=0): + logger.debug( f"{path}") + + self.http = None + self.connected = False + self.req = None + self.urlstr = path + + # have noticed that some site does not allow // in path + if path.startswith('http://') and '//' in path[7:]: + self.urlstr = 'http://' + path[7:].replace('//', '/') + + if path.startswith('https://') and '//' in path[8:]: + self.urlstr = 'https://' + path[8:].replace('//', '/') + + alarm_set(self.o.timeout) + + try: + # when credentials are needed. + headers = {'user-agent': 'Sarracenia ' + sarracenia.__version__} + if self.bearer_token: + logger.debug('bearer_token: %s' % self.bearer_token) + headers['Authorization'] = 'Bearer ' + self.bearer_token + + if self.user != None: + password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() + # takeaway credentials info from urlstr + cred = self.user + '@' + self.urlstr = self.urlstr.replace(cred, '') + if self.password != None: + cred = self.user + ':' + self.password + '@' + self.urlstr = self.urlstr.replace(cred, '') + + # continue with authentication + password_mgr.add_password(None, self.urlstr, self.user, + unquote(self.password)) + auth_handler = urllib.request.HTTPBasicAuthHandler( + password_mgr) + + #hctx = ssl.create_default_context() + #hctx.check_hostname = False + #hctx.verify_mode = ssl.CERT_NONE + ssl_handler = urllib.request.HTTPSHandler(0, self.tlsctx) + + # create "opener" (OpenerDirector instance) + opener = urllib.request.build_opener(auth_handler, ssl_handler) + + # use the opener to fetch a URL + opener.open(self.urlstr) + + # Install the opener. + urllib.request.install_opener(opener) + + # Now all calls to get the request use our opener. + self.req = urllib.request.Request(self.urlstr, headers=headers) + + # set range in byte if needed + if remote_offset != 0: + str_range = 'bytes=%d-%d' % (remote_offset, + remote_offset + length - 1) + self.req.headers['Range'] = str_range + + # https without user : create/use an ssl context + ctx = None + if self.user == None and self.urlstr.startswith('https'): + ctx = self.tlsctx + #ctx.check_hostname = False + #ctx.verify_mode = ssl.CERT_NONE + + # open... we are connected + if self.timeout == None: + self.http = urllib.request.urlopen(self.req, context=ctx) + else: + self.http = urllib.request.urlopen(self.req, + timeout=self.timeout, + context=ctx) + + self.connected = True + + alarm_cancel() + + return True + + except urllib.error.HTTPError as e: + logger.error('Download failed 4 %s ' % self.urlstr) + logger.error( + 'Server couldn\'t fulfill the request. Error code: %s, %s' % + (e.code, e.reason)) + alarm_cancel() + self.connected = False + raise + except urllib.error.URLError as e: + logger.error('Download failed 5 %s ' % self.urlstr) + logger.error('Failed to reach server. Reason: %s' % e.reason) + alarm_cancel() + self.connected = False + raise + except: + logger.warning("unable to open %s" % self.urlstr) + logger.debug('Exception details: ', exc_info=True) + self.connected = False + alarm_cancel() + raise + + alarm_cancel() + return False
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_modules/sarracenia/transfer/sftp.html b/_modules/sarracenia/transfer/sftp.html new file mode 100644 index 000000000..f45320926 --- /dev/null +++ b/_modules/sarracenia/transfer/sftp.html @@ -0,0 +1,647 @@ + + + + + + sarracenia.transfer.sftp — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +

Source code for sarracenia.transfer.sftp

+# This file is part of sarracenia.
+# The sarracenia suite is Free and is proudly provided by the Government of Canada
+# Copyright (C) Her Majesty The Queen in Right of Canada, 2008-2021
+#
+# Sarracenia repository: https://github.com/MetPX/sarracenia
+# Documentation: https://github.com/MetPX/sarracenia
+#
+########################################################################
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; version 2 of the License.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#
+#
+
+import logging, paramiko, os, subprocess, sys, time
+from paramiko import *
+from stat import *
+
+from sarracenia.transfer import Transfer
+from sarracenia.transfer import alarm_cancel, alarm_set, alarm_raise
+from urllib.parse import unquote
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+
+[docs] +class Sftp(Transfer): + """ + SecSH File Transfer Protocol (SFTP) ( https://filezilla-project.org/specs/draft-ietf-secsh-filexfer-02.txt ) + Sarracenia transfer protocol subclass supports/uses additional custom options: + + * accelScpCommand (default: '/usr/bin/scp %s %d' ) + + The module uses the paramiko library for python SecSH support ( https://www.paramiko.org/ ) + """ +
+[docs] + def __init__(self, proto, options): + + super().__init__(proto, options) + + logger.debug("sr_sftp __init__") + + self.o.add_option("accelScpCommand", "str", "/usr/bin/scp %s %d") + # sftp command times out after 20 secs + # this setting is different from the computed timeout (protocol) + + self.connected = False + self.sftp = None + self.ssh = None + self.seek = True + + self.batch = 0 + self.connected = False + self.ssh_config = None + + try: + self.ssh_config = paramiko.SSHConfig() + ssh_config = os.path.expanduser('~/.ssh/config') + if os.path.isfile(ssh_config): + fp = open(ssh_config, 'r') + self.ssh_config.parse(fp) + fp.close() + except: + logger.error("sr_sftp/__init__: unable to load ssh config %s" % + ssh_config) + logger.debug('Exception details: ', exc_info=True)
+ + + def registered_as(): + return ['sftp', 'scp', 'ssh', 'fish'] + + # cd + def cd(self, path): + alarm_set(self.o.timeout) + logger.debug("first cd to %s" % self.originalDir) + self.sftp.chdir(self.originalDir) + logger.debug("then cd to %s" % path) + self.sftp.chdir(path) + self.pwd = path + alarm_cancel() + + # cd forced + def cd_forced(self, perm, path): + logger.debug("sr_sftp cd_forced %d %s" % (perm, path)) + + # try to go directly to path + + alarm_set(self.o.timeout) + self.sftp.chdir(self.originalDir) + try: + self.sftp.chdir(path) + alarm_cancel() + return + except: + pass + alarm_cancel() + + # need to create subdir + + subdirs = path.split("/") + if path[0:1] == "/": subdirs[0] = "/" + subdirs[0] + + for d in subdirs: + if d == '': continue + # try to go directly to subdir + try: + alarm_set(self.o.timeout) + self.sftp.chdir(d) + alarm_cancel() + continue + except: + pass + + # create and go to subdir + alarm_set(self.o.timeout) + self.sftp.mkdir(d, self.o.permDirDefault) + self.sftp.chdir(d) + alarm_cancel() + + def check_is_connected(self): + logger.debug("sr_sftp check_is_connected") + + if self.sftp == None: return False + if not self.connected: return False + + if self.sendTo != self.o.sendTo: + self.close() + return False + + self.batch = self.batch + 1 + if self.batch > self.o.batch: + self.close() + return False + + # really connected, getcwd would not work, send_ignore would not work... so chdir used + try: + alarm_set(self.o.timeout) + self.sftp.chdir(self.originalDir) + alarm_cancel() + except: + self.close() + return False + + return True + + # chmod + def chmod(self, perm, path): + logger.debug("sr_sftp chmod %s %s" % ("{0:o}".format(perm), path)) + alarm_set(self.o.timeout) + self.sftp.chmod(path, perm) + alarm_cancel() + + # close + def close(self): + logger.debug("sr_sftp close") + + old_sftp = self.sftp + old_ssh = self.ssh + + self.init() + + alarm_set(self.o.timeout) + try: + old_sftp.close() + except: + pass + try: + old_ssh.close() + except: + pass + alarm_cancel() + + # connect... + def connect(self): + + logger.debug("sr_sftp connect %s" % self.o.sendTo) + + if self.connected: self.close() + + self.connected = False + self.sendTo = self.o.sendTo + + if not self.credentials(): return False + + alarm_set(self.o.timeout) + try: + + sublogger = logging.getLogger('paramiko') + sublogger.setLevel(logging.CRITICAL) + self.ssh = paramiko.SSHClient() + # FIXME this should be an option... for security reasons... not forced + self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + if self.password: + self.ssh.connect(self.host,self.port,self.user,unquote(self.password), \ + pkey=None,key_filename=self.ssh_keyfile,\ + timeout=self.o.timeout,allow_agent=False,look_for_keys=False) + else: + self.ssh.connect(self.host,self.port,self.user,self.password, \ + pkey=None,key_filename=self.ssh_keyfile,\ + timeout=self.o.timeout) + #if ssh_keyfile != None : + # key=DSSKey.from_private_key_file(ssh_keyfile,password=None) + + sftp = self.ssh.open_sftp() + if self.o.timeout != None: + logger.debug("sr_sftp connect setting timeout %f" % + self.o.timeout) + channel = sftp.get_channel() + channel.settimeout(self.o.timeout) + + sftp.chdir('.') + self.originalDir = sftp.getcwd() + self.pwd = self.originalDir + + self.connected = True + self.sftp = sftp + + #alarm_cancel() + return True + + except: + logger.error("sr_sftp/connect: unable to connect to %s (user:%s)" % + (self.host, self.user)) + logger.debug('Exception details: ', exc_info=True) + + alarm_cancel() + return False + + # credentials... + def credentials(self): + #logger.debug("sr_sftp credentials %s" % self.sendTo) + + try: + ok, details = self.o.credentials.get(self.sendTo) + if details: url = details.url + + self.host = url.hostname + self.port = url.port + self.user = url.username + self.password = url.password + self.ssh_keyfile = details.ssh_keyfile + + if url.username == '': self.user = None + if url.password == '': self.password = None + if url.port == '': self.port = None + if self.ssh_keyfile: self.password = None + + if self.port == None: self.port = 22 + + #logger.debug("h u:p s = %s:%d %s:%s %s" % + # (self.host, self.port, self.user, self.password, + # self.ssh_keyfile)) + + if self.ssh_config == None: return True + + if self.user == None or \ + ( self.ssh_keyfile == None and self.password == None): + #logger.debug("check in ssh_config") + for key, value in self.ssh_config.lookup(self.host).items(): + if key == "hostname": + self.host = value + elif key == "user": + self.user = value + elif key == "port": + self.port = int(value) + elif key == "identityfile": + self.ssh_keyfile = os.path.expanduser(value[0]) + + #logger.debug("h u:p s = %s:%d %s:%s %s" % + # (self.host, self.port, self.user, self.password, + # self.ssh_keyfile)) + return True + + except: + logger.error( + "sr_sftp/credentials: unable to get credentials for %s" % + self.sendTo) + logger.debug('Exception details: ', exc_info=True) + + return False + + # delete + # MG sneak rmdir here in case 'R' message implies a directory (remote mirroring) + def delete(self, path): + logger.debug("sr_sftp rm %s" % path) + + alarm_set(self.o.timeout) + # check if the file is there... if not we are done,no error + try: + s = self.sftp.lstat(path) + except: + alarm_cancel() + return + + # proceed with file/link removal + if not S_ISDIR(s.st_mode): + logger.debug("sr_sftp remove %s" % path) + self.sftp.remove(path) + + # proceed with directory removal + else: + logger.debug("sr_sftp rmdir %s" % path) + self.sftp.rmdir(path) + + alarm_cancel() + + def readlink(self, link): + logger.debug("%s" % (link)) + alarm_set(self.o.timeout) + value = self.sftp.readlink(link) + alarm_cancel() + return value + + # symlink + def symlink(self, link, path): + logger.debug("(in %s), create this file %s as a link to: %s" % (self.getcwd(), path, link) ) + alarm_set(self.o.timeout) + self.sftp.symlink(link, path) + alarm_cancel() + + # get + + def get(self, + msg, + remote_file, + local_file, + remote_offset=0, + local_offset=0, + length=0, exactLength=False): + logger.debug( + "sr_sftp get %s %s %d %d %d %s" % + (remote_file, local_file, remote_offset, local_offset, length, exactLength)) + + alarm_set(2 * self.o.timeout) + rfp = self.sftp.file(remote_file, 'rb', self.o.bufsize) + if remote_offset != 0: rfp.seek(remote_offset, 0) + rfp.settimeout(1.0 * self.o.timeout) + alarm_cancel() + + # read from rfp and write to local_file + + rw_length = self.read_writelocal(remote_file, rfp, local_file, + local_offset, length, exactLength=False) + + + # close + + alarm_set(self.o.timeout) + rfp.close() + alarm_cancel() + + return rw_length + + def getAccelerated(self, msg, remote_file, local_file, length=0, remote_offset=0, exactLength=False): + + base_url = msg['baseUrl'].replace('sftp://', '') + if base_url[-1] == '/': + base_url = base_url[0:-1] + arg1 = base_url + ':' + self.pwd + os.sep + remote_file + arg1 = arg1.replace(' ', '\\ ') + arg2 = '.' + os.sep + local_file + + cmd = self.o.accelScpCommand.replace('%s', arg1) + cmd = cmd.replace('%d', arg2).split() + logger.info("accel_sftp: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + return -1 + sz = os.stat(arg2).st_size + return sz + + # getcwd + def getcwd(self): + alarm_set(self.o.timeout) + cwd = self.sftp.getcwd() if self.sftp else None + alarm_cancel() + return cwd + + # ls + def ls(self): + logger.debug("sr_sftp ls") + self.entries = {} + # timeout is at least 30 secs, say we wait for max 5 mins + alarm_set(self.o.timeout) + dir_attr = self.sftp.listdir_attr() + alarm_cancel() + for index in range(len(dir_attr)): + attr = dir_attr[index] + line = attr.__str__() + self.line_callback(line, attr) + #logger.debug("sr_sftp ls = %s" % self.entries ) + return self.entries + + # line_callback: ls[filename] = 'stripped_file_description' + def line_callback(self, iline, attr): + #logger.debug("sr_sftp line_callback %s" % iline) + + oline = iline + oline = oline.strip('\n') + oline = oline.strip() + oline = oline.replace('\t', ' ') + opart1 = oline.split(' ') + opart2 = [] + + for p in opart1: + if p == '': continue + opart2.append(p) + # else case is in the event of unlikely race condition + fil = ' '.join(opart2[8:]) + line = ' '.join(opart2) + + self.entries[fil] = attr + + # mkdir + def mkdir(self, remote_dir): + logger.debug("mkdir %s" % remote_dir) + alarm_set(self.o.timeout) + + try: + s = self.sftp.lstat(path) + if S_ISDIR(s.st_mode): + return + logger.error( f"cannot mkdir {path}, file exists" ) + alarm_cancel() + return + except FileNotFoundError: + pass + except: + alarm_cancel() + return + + self.sftp.mkdir(remote_dir, self.o.permDirDefault) + alarm_cancel() + + # put + def put(self, + msg, + local_file, + remote_file, + local_offset=0, + remote_offset=0, + length=0): + logger.debug( + "sr_sftp put %s %s %d %d %d" % + (local_file, remote_file, local_offset, remote_offset, length)) + + # simple file + + alarm_set(2 * self.o.timeout) + + if length == 0: + rfp = self.sftp.file(remote_file, 'wb', self.o.bufsize) + rfp.settimeout(1.0 * self.o.timeout) + + # parts + else: + try: + self.sftp.stat(remote_file) + except: + rfp = self.sftp.file(remote_file, 'wb', self.o.bufsize) + rfp.close() + + rfp = self.sftp.file(remote_file, 'r+b', self.o.bufsize) + rfp.settimeout(1.0 * self.o.timeout) + if remote_offset != 0: rfp.seek(remote_offset, 0) + + alarm_cancel() + + # read from local_file and write to rfp + + rw_length = self.readlocal_write(local_file, local_offset, length, rfp) + + # no sparse file... truncate where we are at + + alarm_set(self.o.timeout) + self.fpos = remote_offset + rw_length + if length != 0: rfp.truncate(self.fpos) + rfp.close() + alarm_cancel() + + return rw_length + + def putAccelerated(self, msg, local_file, remote_file, length=0): + + dest_baseUrl = self.o.sendTo.replace('sftp://', '') + if dest_baseUrl[-1] == '/': + dest_baseUrl = dest_baseUrl[0:-1] + arg2 = dest_baseUrl + ':' + msg['new_dir'] + os.sep + remote_file + arg2 = arg2.replace(' ', '\\ ') + arg1 = local_file + + cmd = self.o.accelScpCommand.replace('%s', arg1) + cmd = cmd.replace('%d', arg2).split() + + logger.info("accel_sftp: %s" % ' '.join(cmd)) + p = subprocess.Popen(cmd) + p.wait() + if p.returncode != 0: + return -1 + # FIXME: faking success... not sure how to check really. + sz = int(msg['size']) + return sz + + # rename + def rename(self, remote_old, remote_new): + logger.debug("sr_sftp rename %s %s" % (remote_old, remote_new)) + try: + self.delete(remote_new) + except: + pass + alarm_set(self.o.timeout) + self.sftp.rename(remote_old, remote_new) + alarm_cancel() + + # rmdir + def rmdir(self, path): + logger.debug("sr_sftp rmdir %s " % path) + alarm_set(self.o.timeout) + self.sftp.rmdir(path) + alarm_cancel() + + # utime + def utime(self, path, tup): + logger.debug("sr_sftp utime %s %s " % (path, tup)) + alarm_set(self.o.timeout) + self.sftp.utime(path, tup) + alarm_cancel()
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/_sources/Contribution/AMQPprimer.rst.txt b/_sources/Contribution/AMQPprimer.rst.txt new file mode 100644 index 000000000..64b36dd93 --- /dev/null +++ b/_sources/Contribution/AMQPprimer.rst.txt @@ -0,0 +1,327 @@ + +============================== + AMQP - Primer for Sarracenia +============================== + +This is a short but rather dense briefing to explain +the motivation for the use of AMQP by the MetPX-Sarracenia +data pump. Sarracenia is essentially an AMQP application, +so some understanding AMQP is very helpful. +AMQP is a vast and interesting topic in it's own right. No attempt is made to explain +all of it here. This brief just provides a little context, and introduces only +background concepts needed to understand and/or use Sarracenia. For more information +on AMQP itself, a set of links is maintained at +the `Metpx web site `_ but a search engine +will also reveal a wealth of material. + +.. contents:: + +AMQP Feature Selection +---------------------- + +AMQP is a universal message passing protocol with many different +options to support many different messaging patterns. MetPX-sarracenia specifies and uses a +small subset of AMQP patterns. An important element of Sarracenia development was to +select from the many possibilities a small subset of methods are general and +easily understood, in order to maximize potential for interoperability. + +Analogy FTP +~~~~~~~~~~~ + +Specifying the use of a protocol alone may be insufficient to provide enough information for +data exchange and interoperability. For example when exchanging data via FTP, a number of choices +need to be made above and beyond the protocol. + + - authenticated or anonymous use? + - how to signal that a file transfer has completed (permission bits? suffix? prefix?) + - naming convention + - text or binary transfer + +Agreed conventions above and beyond simply FTP (IETF RFC 959) are needed. Similar to the use +of FTP alone as a transfer protocol is insufficient to specify a complete data transfer +procedure, use of AMQP, without more information, is incomplete. The intent of the conventions +layered on top of AMQP is to be a minimum amount to achieve meaningful data exchange. + +AMQP: not 1.0, but 0.8 or 0.9 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +AMQP 1.0 standardizes the on-the-wire protocol, but removed all broker standardization. +As the use of brokers is key to Sarracenia´s use of, was a fundamental element of earlier standards, +and as the 1.0 standard is relatively controversial, this protocol assumes a pre 1.0 standard broker, +as is provided by many free brokers, such as rabbitmq and Apache QPid, often referred to as 0.8, +but 0.9 and post 0.9 brokers could inter-operate well. + + +Named Exchanges and Queues +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In AMQP prior to 1.0, many different actors can define communication parameters, such as exchanges +to publish to, queues where notification messages accumulate, and bindings between the two. Applications +and users declare and user their exchanges, queues, and bindings. All of this was dropped +in the move to 1.0 making topic based exchanges, an important underpinning of pub/sub patterns +much more difficult. + +in AMQP 0.9, one subscriber can declare a queue, and then multiple processes (given the right +permissions and the queue name) can consume from the same queue. That requires being able +to name the queue. In another protocol, such as MQTT, one cannot name the queue, and so +this processing pattern is not supported. + +The mapping convention described in `Topic <../Reference/sr3_post.7.html#topic>`_, allows +MQTT to establish separate hierarchies which provides a fixed distribution among +the workers, but not exactly the self-balancing shared queue that AMQP provides. + + +.. NOTE:: + + In RabbitMQ (the initial broker used), permissions are assigned using regular expressions. So + a permission model where AMQP users can define and use *their* exchanges and queues + is enforced by a naming convention easily mapped to regular expressions (all such + resources include the username near the beginning). Exchanges begin with: xs__. + Queue names begin with: q__. + + +Topic-based Exchanges +~~~~~~~~~~~~~~~~~~~~~ + +Topic-based exchanges are used exclusively. AMQP supports many other types of exchanges, +but sr3_post have the topic sent in order to support server side filtering by using topic +based filtering. At AMQP 1.0, topic-based exchanges (indeed all exchanges, are no +longer defined.) Server-side filtering allows for much fewer topic hierarchies to be used, +and for much more efficient subsciptions. + +In Sarracenia, topics are chosen to mirror the path of the files being announced, allowing +straight-forward server-side filtering, to be augmented by client-side filtering on +message reception. + +The root of the topic tree is the version of the message payload. This allows single brokers +to easily support multiple versions of the protocol at the same time during transitions. *v02*, +created in 2015, is the third iteration of the protocol and existing servers routinely support previous +versions simultaneously in this way. The second sub-topic defines the type of message. +At the time of writing: v02.post is the topic prefix for current notification messages. + +Little Data +~~~~~~~~~~~ + +The AMQP messages contain notification messages, no actual file data. AMQP is optimized for and assumes +small messages. Keeping the messages small allows for maximum message throughtput and permits +clients to use priority mechanisms based on transfer of data, rather than the notification messages. +Accomodating large messages would create many practical complications, and inevitably require +the definition of a maximum file size to be included in the message itself, resulting in +complexity to cover multiple cases. + +Sr3_post is intended for use with arbitrarily large files, via segmentation and multi-streaming. +Blocks of large files are announced independently and blocks can follow different paths +between initial pump and final delivery. The protocol is unidirectional, in that there +is no dialogue between publisher and subscriber. Each post is a stand-alone item that +is one message in a stream, which on receipt may be spread over a number of nodes. + +However, it is likely that, for small files over high latency links, it is +more efficient to include the body of the files in the notification messages themselves, +rather than forcing a separate retrieval phase. The relative advantage depends on: + +* relative coarseness of server side filtering means some filtering is done on + the client side. Any data embedded for notification messages discarded on the client-side + are waste. + +* Sarracenia establishes long-lived connections for some protocols, such as SFTP, + so the relative overhead for a retrieval may not be long. + +* One will achieve a higher messaging rate without data being embedded, and if the + notification messages are distributed to a number of workers, it is possible that the resulting + message rate is higher without embedded data (because of faster distribution for + parallel download) than the savings from embedding. + +* the lower the latency of the connection, the lesser the performance advantage + of embedding, and the more it becomes a limiting factor on high performance + transfers. + +Further work is needed to better clarify when it makes sense to embed content +in notification messages. For now, the *content* header is included to allow such experiments +to occur. + +Other Parameters +~~~~~~~~~~~~~~~~ + +AMQP has many other settings, and reliability for a particular use case +is assured by making the right choices. + +* persistence (have queues survive broker restarts, default to true), + +* expiry (how long a queue should exist when no-one is consuming from it. Default: a few + minutes for development, but can set much longer for production) + +* message_ttl (the life-span of queued notification messages. Messages that are too old will not + be delivered: default is forever.) + +* Pre-fetch is an AMQP tunable to determine how many notification messages a client will + retrieve from a broker at once, optimizing streaming. (default: 25) + +These are used in declarations of queues and exchanges to provide appropriate +message processing. This is not an exhaustive list. + + + +Mapping AMQP Concepts to Sarracenia +----------------------------------- + +.. image:: ../Explanation/Concepts/AMQP4Sarra.svg + :scale: 50% + :align: center + +An AMQP Server is called a Broker. *Broker* is sometimes used to refer to the software, +other times server running the broker software (same confusion as *web server*.) In the above diagram, AMQP vocabulary is in Orange, and Sarracenia terms are in blue. + +There are many different broker software implementations. We use rabbitmq. +Not trying to be rabbitmq specific, but management functions differ between implementations. +So admin tasks require 'porting' while the main application elements do not. + +*Queues* are usually taken care of transparently, but you need to know + - A Consumer/subscriber creates a queue to receive notification messages. + - Consumer queues are *bound* to exchanges (AMQP-speak) + +An *exchange* is a matchmaker between *publisher* and *consumer* queues. + - A message arrives from a publisher. + - message goes to the exchange, is anyone interested in this message? + - in a *topic based exchange*, the message topic provides the *exchange key*. + - interested: compare message key to the bindings of *consumer queues*. + - message is routed to interested *consumer queues*, or dropped if there aren't any. + +- Multiple processes can share a *queue*, they just take turns removing notification messages from it. + - This is used heavily for sr_sarra and sr_subcribe multiple instances. + +- *Queues* can be *durable*, so even if your subscription process dies, + if you come back in a reasonable time and you use the same queue, + you will not have missed any notification messages. + +- How to Decide if Someone is Interested. + - For Sarracenia, we use (AMQP standard) *topic based exchanges*. + - Subscribers indicate what topics they are interested in, and the filtering occurs server/broker side. + - Topics are just keywords separated by a dot. wildcards: # matches anything, * matches one word. + - We create the topic hierarchy from the path name (mapping to AMQP syntax) + - Resolution & syntax of server filtering is set by AMQP. (. separator, # and * wildcards) + - Server side filtering is coarse, notification messages can be further filtered after download using regexp on the actual paths (the reject/accept directives.) + +- topic prefix? We start the topic tree with fixed fields + - v02 the version/format of sarracenia notification messages. + - post ... the message type, this is an notification message + of a file (or part of a file) being available. + + +Sarracenia is an MQP Application +-------------------------------- + +in Version 2, MetPX-Sarracenia is only a light wrapper/coating around AMQP. +in Version 3, this was reworked and an MQTT driver was added to make it +less AMQP specific. + +- A MetPX-Sarracenia pump is a python AMQP application that uses an (rabbitmq) + broker to co-ordinate SFTP and HTTP client data transfers, and accompanies a + web server (apache) and sftp server (openssh) on the same user-facing address. + +- Wherever reasonable, we use their terminology and syntax. + If someone knows AMQP, they understand. If not, they can research. + + - Users configure a *broker*, instead of a pump. + - users explicitly can pick their *queue* names. + - users set *subtopic*, + - topics with dot separator are minimally transformed, rather than encoded. + - queue *durable*. + - we use *message headers* (AMQP-speak for key-value pairs) rather than encoding in JSON or some other payload format. + +- reduce complexity through conventions. + - use only one type of exchanges (Topic), take care of bindings. + - naming conventions for exchanges and queues. + - exchanges start with x. + - xs_Weather - the exchange for the source (amqp user) named Weather to post notification messages + - xpublic -- exchange used for most subscribers. + - queues start with q + +- Internet resources are more useful and reduce our documentation burden. +- We write less code (exposing raw AMQP means less glue.) +- Less potential for bugs/ higher reliability. +- we make minimum number of choices/restrictions +- set sensible defaults. + + +Review +------ + +If you understood the rest of the document, this should make sense to you: + +An AMQP broker is a server process that houses exchanges and queues used to route notification messages +with very low latency. A publisher sends notification messages to an exchange, while a consumer reads +notification messages from their queue. Queues are *bound* to exchanges. Sarracenia links a broker +to a web server to provide fast notifications, and uses topic exchanges to enable +consumers' server side filtering. The topic tree is based on the file tree you can +browse if you visit the corresponding web server. + + +Appendix A: Background +---------------------- + +Why Use AMQP? +~~~~~~~~~~~~~ + +- open standard, multiple free implementations. +- low latency message passing. +- encourages asynchronous patterns/methods. +- language, protocol & vendor neutral. +- very reliable. +- robust adoption (next two sections as examples) + + +Where does AMQP Come From? +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Open International standard from financial world. +- Many proprietary similar systems exist, AMQP built to get away from lock-in. Standard is built with long experience of vendor messaging systems, and so quite mature. +- invariably used behind the scenes as a component in server-side processing, not user visible. +- many web companies (soundcloud) +- seeing good adoption in monitoring and integration for HPC + +Intel/Cray HPC Stack +~~~~~~~~~~~~~~~~~~~~ + +`Intel/Cray HPC stack `_ + +.. image:: AMQPprimer/IntelHPCStack.png + :scale: 50% + :align: center + + +OpenStack +~~~~~~~~~ + +`AMQP is the messaging technology chosen by the OpenStack cloud. `_ + + +.. image:: AMQPprimer/OpenStackArch.png + :scale: 70% + :align: center + + +How to Adopt AMQP +~~~~~~~~~~~~~~~~~ + +Adopting AMQP is more like adopting XML than it is like adopting FTP. FTP interoperability +is easy as choices are limited. With XML, however you get **more palette than painting.** Many +different dialects, schema methods, etc... XML will be valid and parse, but without +additional standardization, data exchange remains uncertain. For real interoperabiltiy, +one must standardize specific dialects. Examples: + + - RSS/Atom, + - Common Alerting Protocol (CAP) + +AMQP brokers and the client software can connect and send notification messages, but without +additional standardization, applications will not communicate. AMQP calls +those additional layers *applications*. AMQP enables every conceivable message +pattern, so a **well formed application is** built by eliminating features from +consideration, **choosing the colours to use.** +Sarracenia is an applicaton of AMQP message passing to file transfer. + +As CAP narrows XML, Sarracenia narrows the scope of AMQP. This narrowing is necessary to obtain a useful result: Interoperability. Sarracenia conventions and formats are defined in: + + - `sr_post format man page <../Reference/sr_post.7.html>`_ + + + diff --git a/_sources/Contribution/BasicIdea.rst.txt b/_sources/Contribution/BasicIdea.rst.txt new file mode 100644 index 000000000..cb70def16 --- /dev/null +++ b/_sources/Contribution/BasicIdea.rst.txt @@ -0,0 +1,199 @@ + +Status: Approved-Draft1-20150608 + +========== +Basic Idea +========== + +MetPX-Sarracenia is a data duplication or distribution engine that leverages existing +standard technologies (sftp and web servers and AMQP brokers) to achieve real-time message +delivery and end to end transparency in file transfers. Whereas in Sundew, each +pump is a standalone configuration which transforms data in complex ways, in +sarracenia, the data sources establish a structure which is carried through any +number of intervening pumps until they arrive at a client. The consumer can +provide explicit acknowledgement that propagates back through the network to the +source. + +Whereas traditional file pumping is a point-to-point affair where knowledge is only +between each segment, in Sarracenia, information flows from end to end in both directions. +At it's heart, sarracenia exposes a tree of web accessible folders (WAF), using +any standard HTTP server (tested with apache). Weather applications are soft real-time, +where data should be delivered as quickly as possible to the next hop, and +minutes, perhaps seconds, count. The standard web push technologies, ATOM, RSS, etc... +are actually polling technologies that when used in low latency applications consume a great +deal of bandwidth an overhead. For exactly these reasons, those standards +stipulate a minimum polling interval of five minutes. Advanced Message Queueing +Protocol (AMQP) messaging brings true push to notifications, and makes real-time +sending far more efficient. + +.. image:: ../Explanation/Concepts/e-ddsr-components.jpg + + +Sources of data announce their products, pumping systems pull the data onto their +WAF trees, and then announce their trees for downstream clients. When clients +download data, they may write a log message back to the server. Servers are configured +to forward those client log messages back through the intervening servers back to +the source. The Source can see the entire path that the data took to get to each +client. With traditional pumping applications, sources only see that they delivered +to the first hop in a chain. Beyond that first hop, routing is opaque, and tracing +the path of data required assistance from administrators of each intervening system. +With Sarracenia's log forwarding, the pumping network is completely transparent +to the sources, in that they can see where it went. With end to end logs, diagnostics +are vastly simplified for everyone. + +For large files / high performance, files are segmented on ingest if they are sufficiently +large to make this worthwhile. Each file can traverse the pump network independently, +and reassembly is only needed at end points. A file of sufficient size will announce +the availability of several segments for transfer, multiple threads or transfer nodes +will pick up segments and transfer them. The more segments available, the higher +the parallelism of the transfer. Sarracenia manages parallelism and network usage +without explicit user intervention. As intervening pumps do not store and +forward entire files, the maximum file size which can traverse the network is +maximized. + + +These concepts below are not in order (yet?) maybe we will do that later. +not sure about priorities, just number to be able to refer to them. +They are meant to help guide (reflect?) design/implementation decisions: + +For each objective/consideration/advice below, see if they make sense, +and seem helpful. We should get rid of any that are not helpful. + + +1. The pump is, or any number of pumps are, transparent. + Put another way: + The source is in charge of the data they provide. + + The source determines the distribution (scope, and permissions) + The source can obtain any information about themselves:: + + - when status changed: start,stop,drop. + - when notification messages are accepted. + - when data is pulled by a consumer (a scope layer, or a end point.) + + +2. AMQP brokers do not transfer any user data, just metadata. + + reasoning: + need to keep the notification messages small so that the forwarding rate is high. + large notification messages will gum up the works. also permissions become interesting. + end up with a 'maximum size' threshold, and implementing two methods for everything. + +3. Config changes should propagate, not be unique to a host + you should not have to do dsh, or px-push. + That sort of management is built in. the message bus is there for that. + might use 'scope' to have commands propagate through multiple clusters. + + + +4. Log is data. + + *It is not enough for justice to be done. Justice must be seen to be done.* + + It is not enough for data to be delivered. That delivery must be logged, + and that log must be returned to the source. While we want to supply + enough information to data sources, we do not want to drown the network + in meta data. The local component logs will have much more information, + The log messages traverse the network to the source are ´final dispositions´ + whenever an operation is either completed or finally abandoned. + + + +5. This is a data distribution tool, not a file tree replicator. + + - we do not need to know what linux uid/gid owned it originally. + - we do not care when it was modified. + - we do not care about it's original permission bits. + - we do not care what ACL's it has (they aren't relevant on the destination.) + - we do not care about extended attributes. (portability, win,mac,lin,netapp?) + + again doubtful about this one. Does it help? + + + +6. Not worried about performance in phase 1 + - performance is enabled by the scalability of the design:: + + -- segmentation/re-assembly provides multi-threading. + -- segmentation means bigger files transfer with greater parallelism. + adds multiple streams when that is worthwhile, uses a single stream + when that makes sense. + -- validation provides source bandwidth limiting. + + - need to prove all the moving parts work together first. + + - much later, may return to see how to make each transfer engine + go faster. + +7. This is not a web application, this is not an FTP server. + + This application uses HTTP as one of the transport protocols, that's all. + It is not trying to be a web site, any more than it is trying to be an sftp server. + + +8. Common management not needed, just pass logs around. + + Different groups can manage different pumps. + when we interconnect pumps, they become a source for us. + log messages are routed to the data sources, so they get our logs on their + data. (security can have something to say about that.) + +9. It needs to run anywhere. + ubuntu,centos -- primary. + but windows also. + + We are trying to make a pump that others can easily adopt. + That means they can install and go. + + It needs to be easy to set up, both client and server. + (this aspect dealt with in packaging) + + +10. the application does not need to pursue absolute reliability. + + Node failure is rare in a Data Centre environment. + Working well in the normal case is the priority. + if it breaks, information is never lost. + Worst case, just re-post, and the system will resend the missing parts + through the nodes that are left. + + There might be some diagnostics to figure out which files are 'in flight' + when a given node goes down (deadman timers). But not sure that multiple + acks with guarantees in the face of node failure is needed. + going faster and being simpler is likely more reliable in practice. + + this is not a database, but a transfer engine. + + +11. Bulletins getting less common, Files are larger... No file too large. + + old apps are used to tiny files (millions of them) in EC/MSC. + but even in EC, files are getting bigger, and will likely grow a lot. + Satellite sensor data is now very critical, and that is substantially larger. + A traditional WMO format weather warning was limited to 15Kbytes (limited by internals + systems to 32 Kbytes now) and those sizes were rarely reached. It was more like 7-12K. + an average modern XML weather warning (CAP) is 60K so, so a five to eight fold increase. + WMO since raised the limit to 500,000 bytes for WMO-GTS messages. and other mechanisms, + such as FTP, have no fixed limit. + + Other scientific domains use very large files (measured in terabytes.) aim to be able + to flow those through the pumps. Worth thinking about transporting huge files. + + +12. Normal operation should not require programming knowledge. + + Configuratin and coding are distinct activities. One should not have to modify scripts + to configure standard elements of the application. Software can be much simpler if it + just leaves all features implemented as plug-in scripts. leaving the local details + for the scripts. But most people will not be able to use it. + + Need to provide all core functionality through CLI at the very least. + config files are consiered part of the CLI, which is why we try to choose carefully + there as well. For programmers, difference between script and config is subtle, + not so for most other people. + + Scripting should only be required to extend features beyond what is standard. + to provide added flexibility. If the flexibility proves generally useful over time, + then it should be brought out of scripts and into the configuration realm. + + diff --git a/_sources/Contribution/Design.rst.txt b/_sources/Contribution/Design.rst.txt new file mode 100644 index 000000000..90e8a5ec5 --- /dev/null +++ b/_sources/Contribution/Design.rst.txt @@ -0,0 +1,729 @@ + +Status: Draft + +================= + Strawman Design +================= + +.. section-numbering:: + +This document reflects the current design resulting from discussions and thinking +at a more detailed level that the outline document. See `Outline `_ +for an overview of the design requirements. See `use-cases `_ for +an exploration of functionality of how this design works in different situations. +The way to make progress towards a working implementation is described in `plan `_. + +.. contents:: + +Assumptions/Constraints +----------------------- + + - Are there cluster file systems available everywhere? No. + + - an operational team might want to monitor/alert when certain transfers experience difficulty. + + - security may want to run different scanning on different traffic (each block?) + security might want us to refuse certain file types, so they go through heavier scanning. + or perform heavier scanning on those file types. + + - extranet zones cannot initiate connections to internal zones. + extranet zones receive inbound connections from anywhere. + + - Government operations zones can initiate connections anywhere. + however, Science is considered a sort of extranet to all the partners. + + - No-one can initiate connections into partner networks, but all partner departments can initiate + connections into science.gc.ca zone. Within the science zones, there is the shared file system + area, where servers access a common cluster oriented file system, as well as some small restricted + zones, where very limited access is afforded to ensure availability. + + - Within NRC, there are labs with equipment which cannot be maintained, software-wise, + to address disclosed vulnerabilities because of excessive testing dependencies (ie. certifying + that a train shaker still works after applying a patch.) These systems are not given access + to the internet, only to a few other systems on the site. + + - collaborators are academic, other-governmental, or commercial entities which which government + scientists exchange data. + + - collaborators connect to extranet resources from their own networks. Similarly to partners, + (subject to exceptions) no connections can be initiated into any collaborator network. + + - There are no proxies, no systems in the extranet are given exceptional permissions to + initiate inbound connections. File storage protocols etc... are completely isolated between + them. There are no file systems that cross network zone boundaries. + + - One method of improving service reliability is to use internal services for internal use + and reserve public facing services for external users. Isolated services on the inside + are completely impervious to internet ´weather´ (DDOS of various forms, load, etc...) + internal and external loads can be scaled independently. + + +Number of Switches +------------------ + +The application is supposed to support any number of topologies, that is any number of pumps S=0,1,2,3 +may exist between origin and final delivery, and do the right thing. + +Why isn´t everything point to point, or when do you insert a pumps? + + - network topology/firewall rules sometimes require being at rest in a transfer area between two + organizations. Exception to these rules create vulnerabilities, so prefer to avoid. + whenever traffic prevents initiating a connection, that indicates a store & forward pumps + may be needed. + + - physical topology. While connectivity may be present, optimal bandwidth use may involve + not taking the default path from A to B, but perhaps passing through C. + + - when the transfer is not 1:1, but 1: many. The pumping takes + care of sending it to multiple points. + + - when the source data needs to be reliably available. This translates to making many copies, + rather than just one, so it is easier for the source to post once, and have the network + take care of replication. + + - for management reasons, you want to centrally observe data large transfers. + + - for management reasons, to have transfers routed a certain way. + + - for management reasons, to ensure that transfer failures are detected and escalated + when appropriate. They can be fixed rather than waiting for ad-hoc monitoring to detect + the issue. + + - For asynchronous transfers. If the source has many other activities, it may want + to give responsibility to another service to do potentially lengthy file transfers. + the pump is inserted very near to the source, and is full store & forward. sr_post + completes (nearly instant), and from then on the pumping network manages transfers. + + +AMQP Feature Selection +---------------------- + +AMQP is a universal message passing protocol with many different options to support many +different messaging patterns. MetPX-sarracenia specifies and uses a small subset of AMQP +patterns. Indeed an important element of sarracenia development was to select from the +many possibilities a small subset of methods are general and easily understood, in order +to maximize potential for interoperability. + +Specifying the use of a protocol alone may be insufficient to provide enough information for +data exchange and interoperability. For example when exchanging data via FTP, a number of choices +need to be made above and beyond the basic protocol. + + - authenticated or anonymous use? + - how to signal that a file transfer has completed (permission bits? suffix? prefix?) + - naming convention. + - text or binary transfer. + +Agreed conventions above and beyond simply FTP (IETF RFC 959) are needed. + +Similar to the use of FTP alone as a transfer protocol is insufficient to specify a complete data +transfer procedure, use of AMQP, without more information, is incomplete. + +AMQP 1.0 standardizes the on the wire protocol, but leaves out many features of broker interaction. +As the use of brokers is key to sarracenia´s use of, was a fundamental element of earlier standards, +and as the 1.0 standard is relatively controversial, this protocol assumes a pre 1.0 standard broker, +as is provided by many free brokers, such as rabbitmq, often referred to as 0.8, but 0.9 and post +0.9 brokers are also likely to inter-operate well. + +In AMQP, many different actors can define communication parameters. To create a clearer +security model, sarracenia constrains that model: sr3_post clients are not expected to declare +Exchanges. All clients are expected to use existing exchanges which have been declared by +broker administrators. Client permissions are limited to creating queues for their own use, +using agreed upon naming schemes. Queue for client: qc_.???? + +Topic-based exchanges are used exclusively. AMQP supports many other types of exchanges, +but sr3_post have the topic sent in order to support server side filtering by using topic +based filtering. The topics mirror the path of the files being announced, allowing +straight-forward server-side filtering, to be augmented by client-side filtering on +message reception. + +The root of the topic tree is the version of the message payload. This allows single brokers +to easily support multiple versions of the protocol at the same time during transitions. v02 +is the third iteration of the protocol and existing servers routinely support previous versions +simultaneously in this way. The second topic in the topic tree defines the type of message. +at the time of writing: v02.post is the topic prefix for current notification messages. + +The AMQP messages contain notification messages, no actual file data. AMQP is optimized for and assumes +small messages. Keeping the messages small allows for maximum message throughtput and permits +clients to use priority mechanisms based on transfer of data, rather than the notification messages. +Accomodating large messages would create many practical complications, and inevitably require +the definition of a maximum file size to be included in the message itself, resulting in +complexity to cover multiple cases. + +sr_post is intended for use with arbitrarily large files, via segmentation and multi-streaming. +blocks of large files are announced independently. and blocks can follow different paths +between initial pump and final delivery. + +AMQP vhosts are not used. Never saw any need for them. The commands support their optional +use, but there was no visible purpose to using them is apparent. + +Aspects of AMQP use can be either constraints or features: + + - interaction with a broker are always authenticated. + + - We define the *anonymous* for use in many configurations. + + - users authenticate to local cluster only. We don´t impose any sort of credential or identity propagation + or federation, or distributed trust. + + - pumps represent users by forwarding files on their behalf, there is no need to include + information about the source users later on in the network. + + - This means that if user A from S0 is defined, and a user is given the same name on S1, then they may + collide. sad. Accepted as a limitation. + + +Application +----------- + +Description of application logic relevant to discussion. There is a ´control plane´ where notification messages about new +data available are made, and log messages reporting status of transfers of the same data are routed among +control plane users and pumps. A pump is an AMQP broker, and users authenticate to the broker. Data +may (most of the time does) have a different other authentication method. + +There are very different security use cases for file transfer: + + 1. **Public Dissemination** data is being produced, whose confidentiality is not an issue, the purpose is to + disseminate to all who are interested as quickly and reliably as possible, potentially involving many + copies. The data authentication is typically null for this case. Users just issue HTTP GET requests with + no authentication. For AMQP authentication, it can be done as anonymous, with no ability for providers to + monitor. If there is to be support from the data source, then the source would assign a non-anonymous user + for the AMQP traffic, and the client would ensure logging was working, enabling the provider to monitor and + alert when problems arise. + + 2. **Private Transfer** proprietary data is being generated, and needs to be moved to somewhere where it can be + archived and/or processed effectively, or shared with specific collaborators. AMQP and HTTP traffic must + be encrypted with SSL/TLS. Authentication is typically common between AMQP and HTTPS. For Apache httpd + servers, the htpasswd/htaccess method will need to be continuously configured by the delivery system. + These transfers can have requirements for be high availability. + + 3. **Third Party Transfer** the control plane is explicitly used only to control the transfer, authentication + at both ends is done separately. Users authenticate to the data-less, or SEP pump with AMQP, but the + authentication at both ends is outside sarracenia control. Third-party transfer is limited to S=0. + If the data does not cross the pump, it cannot be forwarded. So no routing is relevant to this case. + Also dependent on the availability of the two end points throughout, so more difficult to assure in practice. + +Both public and private transfers are intended to support arbitrary chains of pumps between *source* and *consumer*. +The cases depend on routing of notification messages and log messages. + +.. NOTE:: + forward routing... Private and Public transfers... not yet clear, still considering. + what is written here on that subject is tentative. wondering if split, and do public + first, then private later? + +To simplify discussions, names will be selected with a prefix things according to the type +of entity: + + - exchanges start with x. + - queues start with q. + - users start with u. users are also referred to as *sources* + - servers start with svr + - clusters start with c + - ´pumps´ is used as a synonym for cluster, and they start with S (capital S.): S0, S1, S2... + +on pumps: + - users that pumps used to authenticate to each other are **interpump accounts**. Another word: **feeder** , **concierge** ? + - users that inject data into the network are called **sources**. + - users that subscribe to data are called **consumers**. + + +Routing +------- + +There are two distinct flows to route: notification messages, and logs. +The following header in messages relate to routing, which are set in all messages. + + - *source* - the user that injected the original notification messages. + - *source_cluster* - the cluster where the source injected the notification messages. + - *to_clust* - the comma separated list of destination clusters. + - *private* - the flag to indicate whether the data is private or public. + +An important goal of notification messages routing is that the *source* decides where notification messages go, so +pumping of individual products must be done only on the contents of the notification messages, not +some administrator configuration. + +Administrators configure the inter pump connections (via SARRA and other components) +to align with network topologies, but once set up, all data should flow properly with +only source initiated routing commands. Some configuration may be needed on all pumps +whenever a new pump is added to the network. + + +Routing Posts +~~~~~~~~~~~~~ + +Post routing is the routing of the notification messages announced by data *sources*. +The data corresponding to the source follows the same sequence of pumps as the notification messages +themselves. When a notification message is processed on a pump, it is downloaded, and then the +notification message is modified to reflect that´s availability from the next-hop pump. + +Post messages are defined in the sr_post(7) man page. They are initially emitted by *sources*, +published to xs_source. After Pre-Validation, they go (with modifications described in Security) to +either xPrivate or xPublic. + +.. note:: + FIXME: Tentative!? + if not separate exchange, then anyone can see any notification message (not the data, but yes the notification message) + I think that´s not good. + +For Public data, *feeders* for downstream pumps connect to xPublic. +They look at the to_clust Header in each message, and consult a post2cluster.conf file. +post2cluster.conf is just a list of cluster names configured by the administrator:: + + ddi.cmc.ec.gc.ca + dd.weather.gc.ca + ddi.science.gc.ca + +This list of clusters is supposed to be the clusters that are reachable by traversing +this pump. If any cluster in post2cluster.conf is listed in the to_clust of the +message field, then the data needs to tr + +Separate Downstream *feeders* connect to xPrivate for private data. Only *feeders* are +allowed to connect to xprivate. + +.. Note:: + FIXME: perhaps feed specific private exchanges for each feeder? x2ddiedm, x2ddidor, x2ddisci ? + using one xPrivate means pumps can see messages they may not be allowed to download + (lesser issue than with xPublic, but depends how trusted downstream pump is.) + +Routing Logs +~~~~~~~~~~~~ + +Log messages are defined in the sr_log(7) man page. They are emitted by *consumers* at the end, +as well as *feeders* as the messages traverse pumps. log messages are posted to +the xl_ exchange, and after log validation queued for the xlog exchange. + +Messages in xlog destined for other clusters are routed to destinations by +log2cluster component using log2cluster.conf configuration file. log2cluster.conf +uses space separated fields: First field is the cluster name (set as per soclust in +notification messages, the second is the destination to send the log messages for posting +originating from that cluster to) Sample, log2cluster.conf:: + + clustername amqp://user@broker/vhost exchange=xlog + +Where message destination is the local cluster, log2user (log2source?) will copy +the messages where source= to sx_. + +When a user wants to view their messages, they connect to sx_. and subscribe. +this can be done using *sr_subscribe -n --topic_prefix=v02.log* or the equivalent *sr_log*. + + +Security Model +-------------- + + + +Users, Queues & Exchanges +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each user Alice, on a broker to which she has access: + - has an exchange xs_Alice, where she writes her notification messages, and reads her logs from. + - has an exchange xl_Alice, where she writes her log messages. + - can create queues qs_Alice\_.* to bind to exchanges. + +Switches connect with one another using inter-exchange accounts. + - Alice can create and destroy her own queues, but no-one else's. + - Alice can only write to her xs_exchange, + - Exchanges are managed by the administrator, and not any user. + - Alice can only post data that she is publishing (it will refer back to her) + +..NOTE:: + tester ^q_tester.* ^q_tester.*|xs_tester ^q_tester.*|^xl_tester$ + leaving all permissions for queues for an amqp users also gives the permission + do create/configure/write any amqp objects with a name starting with q_tester + in this example. + + +Pre-Validation +~~~~~~~~~~~~~~ + +Pre-Validation refers to security and correctness checks performed on +the information provided by the notification message before the data itself is downloaded. +Some tools may refer to this as *message validation* + + - input sanitizing (looking for errors/malicious input.) + - an undefined number of checks that need to be configurable (script?) + - vary per configuration, and installation (sizes) + +When reading from a source: + - a notification message arrives on xs_Alice, from a user logged in as Alice. + - overwrites the source to be Alice: source=Alice ... or reject? + - sets some headers that we do not trust users to do: cluster= + - set cluster header to local one. + +Reading from a feeder: + - source doesn´t matter. (feeders can represent other users) + - do not overwrite source. + - ensure cluster is not local cluster (as that would be a lie.) ? + +Regardless: + - check the partitioning size, if it exceeds pump maximum, Reject. + - check the bandwidth limitations in place. If exceeded, Hold. + - check the disk usage limit in place. If exceeded, Hold. + - If the private flag is set, then accept by copying to xPrivate + - If the private flag is not set, then accept by copy to xPublic + +Results: + - Accept means: queue the message to another exchange (xinput) for downloading. + - Reject means: do not copy message (still accept & ack so it leaves queue) product log message. + - Hold means: do not consume... but sleep for a while. + +Hold is for temporary failure type reasons, such as bandwidth of disk space reasons. +as these reasons are independent of the particular message, hold applies for +the entire queue, not just the message. + +After Pre-Processing, a component like sr_sarra assumes the notification message is good, +and just processes it. That means it will fetch the data from the posting source. +Once the data is downloaded, it goes through Post-Validation. + + +Post-Validation +~~~~~~~~~~~~~~~ + +When a file is downloaded, before re-announcing it for later hops it goes +through some analysis. The tools may call this *file validation*: + + - when a file is downloaded, it goes through post-validation, + - invoke one or more virus scanners chosen by security + - the scanners will not be the same everywhere, even different locations within + same org, may have different scanning standards (function on security zone.) + + - Accept means: it is OK to send this data to further hops in the network. + - Reject menas: do not forward this data (potentially delete local copy.) Essentially *quarantine* + + +Log Validation +~~~~~~~~~~~~~~ + +When a client like sarra or subscribe completes an operation, it creates a log message +corresponding to the result of the operation. (This is much lower granularity than a +local log files.) It is important for one client not to be able to impersonate another +in creating log messages. + + - Messages in exchanges have no reliable means of determining who inserted them. + - so users publish their log messages to sl_ exchange. + - For each user, log reader reads the message, and overwrites the consuminguser to force match. (if reading a message from sl_Alice, it forces the consuminguser field to be Alice) see sr_log(7) for user field + - sl_* are write-only for all users, they cannot read their own notification messages for that. + - is there some check about consuminghost? + - Accepting a log message means publishing on the xlog exchange. + - Only admin functions can read from xlog. + - downstream processing is from xlog exchange which is assumed clean. + - Rejecting a log message means not copying it anywhere. + + - sourcce check does not make sense when channels are used for inter-pump log routing. + Essentially, all downstream pumps can do is forward to the source cluster. + The pumps receiving the log messages must not convert the consuminguser on those links. + evidence of need of some sort of setting: user vs. inter-pump setting. + +... NOTE:: + FIXME: if you reject a log message, does it generate a log message? + Denial of service potential by just generating infinite bogs log messages. + It is sad that if a connection is mis-configured as a user one, when it is inter-pump, + that will cause messages to be dropped. how to detect configuration error? + + +Private vs. Public Data Transfer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Transfers in the past have been public, just a matter of sharing public information. +A crucial requirement of the package is to support private data copies, where the +ends of the transfer are not sharing with arbitrary others. + +.. NOTE:: + FIXME: This section is a half-baked idea! not sure how things will turn out. + basic problem: Alice connecting to S1 wants to share with Bob, who has an + account on S3. To get from S1 to S3, one needs to traverse S2. the normal + way such routing is done is via a sr_sarra subscription to xpublic on S1, and + S2. So Eve, a user on S1 or S2, can see the data, and presumably download it. + unless the http permissions are set to deny on S1 and S2. Eve should not have + access. Implement via http/auth permitting inter-pump accounts on S2 + to access S1/ and S3 account to S2/. then permit bob on + S3. + +There are two modes of sending products through a network, private vs. public. +With public sending, the information transmitted is assumed to be public and available +to all comers, If someone sees the data on an intervening pump, then they are likely +to be able to download it at will without further arrangements. public data is posted +for inter-pump copies using the xPublic exchange, which all users may access as well. + +Private data is only made available to those who are explicitly permitted access. +private data is made available only on the xPrivate exchange. Only Interpump channel +users are given access to these messages. + +.. NOTE:: + - Is two exchanges needed, or is setting permissions enough? + - if nobody on B is permitted, then only C is able to download from B, which just works. + - This only works with http because setting sftp permissions is going to be hell. + - If only using http, then Even can still see all postings, just not get data, unless xprivate happens. + +For SEP topologies (see Topologies) things are much simpler as end users can just use mode bits. + + +HTTPS Private Access +~~~~~~~~~~~~~~~~~~~~ + +.. NOTE:: + FIXME: Not designed yet. + Really not baked yet. For https, need to create/manage .htaccess (canned but generated every day) + and .htpasswd (generated every day) files. + +Need some kind of adm message that sources can send N pumps later to alter the contents of .htpasswd +CRUD? or just overwrite every time? query? + +Sarra likely needs to look at this and add the ht* files every day. Need to talk with the webmailteam guys. + +How to change passwords + + +Topologies +---------- + +Questions... There are many choices for cluster layout. One can do simple H/A on a pair of nodes, +simple active/passive? One can go to scalable designs on an array of nodes, which requires a load +balancer ahead of the processing nodes. The disks of a cluster can be shared or individual to +the processing nodes, as can broker state. Exploring whether to support any/all configurations, +or to determine if there is a particular design pattern that can be applied generally. + +To make these determinations, considerable exploration is needed. + +We start with naming the topologies so they can be referred to easily in further discussions. +None of the topologies assume that disks are pumped among servers in the traditional HA style. + +Based on experience, disk pumping is considered unreliable in practice, as it involves complex +interaction with many layers, including the application. Disks are either dedicated to nodes, +or a cluster file system is to be used. The application is expected to deal with those two +cases. + +most of the cluster management is taken care of by the sr3_tools project: + + https://github.com/MetPX/sr3_tools + +A review of that project to manage deployments regardless of topology, would be helpful. + +Some document short-hand: + +Bunny + A shared/clustered broker instance, where multiple nodes use a common broker to co-ordinate. + + +Capybara Effect + *capybara through a snake* where a large rodent distorts the body of a snake + as it is being digested. Symbolic of poor load balancing, where one node + experiences a spike in load and slows down inordinately. + +Fingerprint Winnowing + Each product has a checksum and size intended to identify it uniquely, referred to as + as fingerprint. If two products have the same fingerprint, they are considered + equivalent, and only one may be forwarded. In cases where multiple sources of equivalent + data are available but downstream consumers would prefer to receive single notification messages + of products, processes may elect to publish notifications of the first product + with a given fingerprint, and ignore subsequent ones. + + This is the basis for the most robust strategy for high availability, but setting up + multiple sources for the same data, accepting notification messages for all of them, but only + forwarding one downstream. In normal operation, one source may be faster than the + other, and so the second source's products are usually 'winnowed'. When one source + disappears, the other source's data is automatically selected, as the fingerprints + are now *fresh* and used, until a faster source becomes available. + + The advantage of this method is that now A/B decision is required, so the time + to *pumpover* is zero. Other strategies are subject to considerable delays + in making the decision to pumpover, and pathologies one could summarize as flapping, + and/or deadlocks. + + +Standalone +~~~~~~~~~~ + +In a standalone configuration, there is only one node in the configuration. I runs all components +and shares none with any other nodes. That means the Broker and data services such as sftp and +apache are on the one node. + +One appropriate usage would be a small non-24x7 data acquisition setup, to take responsibility of data +queueing and transmission away from the instrument. + + +DDSR: Switching/Routing Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is a more scalable configuration involving several data mover nodes, and potentially several brokers. +These clusters are not destinations of data transfers, but intermediaries. Data flows through them, but +querying them is more complicated because no one node has all data available. The downstream clients +of DDSR's are essentially other sarracenia instances. + +There are still multiple options available within this configuration pattern. +ddsr one broker per node? (or just one broker ( clustered,logical ) broker?) + +On a pumping/router, once delivery has occurred to all contexts, can you delete the file? +Just watch the log files and tick off as each scope confirms receipt. +when last one confirmed, delete. (makes re-xmit difficult ;-) + +Based on a file size threshold? if the file is too big, don´t keep it around? + +The intended purpose has a number of implementation options, which must be further sub-divided for analysis. + + +Independent DDSR +~~~~~~~~~~~~~~~~ + +In Independent DDSR, there is a load balancer which distributes each incoming connection to +an individual broker running on a single node. + +ddsr - broker + +pre-fetch validation would happen on the broker. then re-post for the sarra's on the movers. + + + - each node broker and transfer engines act independently. Highest robustness to failure. + - load balancer removes mover nodes from operation on detection of a failure. + - individual files land, mostly entirely on single nodes. + - no single data mover sees all of the files of all of the users in a cluster. + +CONFIRM: Processes running on the individual nodes, are subscribed to the local broker. +Highly susceptible to the *Capybara Effect* where all of the blocks of +the large file are channelled though a single processing node. Large file transfers +with trigger it. + +CONFIRM: Maximum performance for a single transfer is limited to a single node. + + +Shared Broker DDSR +~~~~~~~~~~~~~~~~~~ + +While the data nodes disk space remain independent, the brokers are clustered together to +form a single logical entity. + +on all nodes, the mover processes use common exchanges and queues. + + - each node transfers independently, but dependent on the broker cluster. + - load balancer removes nodes (broker or mover) from operation. + - external users connect to shared queues, not node specific ones. + - transfer engines connect to cluster queues, obtaining blocks. + - no single data mover sees all of the files of all of the users in a cluster. + - requires broker to be clustered, adding complexity there. + +In Shared Broker DDSR, *Capybara Effect* is minimized as individual blocks of a transfer +are distributed across all the mover nodes. When a large file arrives, all of the movers +on all of the nodes may pick up individual blocks, so the work automatically is +distributed across them. + +This assumes that large files are segmented. As different transfer nodes will have +different blocks of a file, and the data view is not shared, no re-assembly of files +is done. + +Broker clustering is considered mature technology, and therefore relatively trustworthy. + + + +DD: Data Dissemination Configuration (AKA: Data Mart) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The sr3 deployment configuration is more of an end-point configuration. Each node is expected to +have a complete copy of all the data downloaded by all the nodes. Giving a unified view makes +it much more compatible with a variety of access methods, such as a file browser (over http, +or sftp) rather than being limited to AMQP notification messages. This is the type of view presented by +dd.weather.gc.ca. + +Given this view, all files must be fully reassembled on receipt, prior to announcing downstream +availability. Files may have been fragmented for transfer across intervening pumps. + +There are multiple options for achieving this end user visible effect, each with tradeoffs. +In all cases, there is a load balancer in front of the nodes which distributes incoming +connection requests to a node for processing. + + - multiple server nodes. Each standalone. + + - sr3 - load balancer, just re-directs to a sr3 node? + dd1,dd2, + + broker on sr3 node has connection thereafter. + + +Independent DD +~~~~~~~~~~~~~~ + + - The load balancer hands the incoming requests to multiple Standalone_ configurations. + + - Each node downloads all data. Disk space requirements for nodes in this configuration + are far larger than for DDSR nodes, where each node only has 1/n of the data. + + - Each node announces each product that it has downloaded, using it's own node name, because + it does not know if other nodes have that product. + + - Once a connection is established, the client will communicate exclusively with that node. + ultimate performance is limited by the individual node performance. + + - The data movers can (for maximum reliability) be configured independently, but if inputs + are across the WAN, one can reduce bandwidth usage N times by havng N nodes + share queues for distant sources and then have local transfers between the nodes. + + CONFIRM: is *Fingerprint Winnowing* required for intra-cluster copies? + + When a single node fails, it ceases to download, and the other n-1 nodes continue transferring. + +.. NOTE:: + FIXME: shared broker and shared file system... hmm... Could use second broker + instance to do cooperating download via fingerpring winnowing. + + + +Shared-Broker DD +~~~~~~~~~~~~~~~~ + + - a single clustered broker is shared by all nodes. + + - Each node downloads all data. Disk space requirements for nodes in this configuration + are far larger than for DDSR nodes, where each node only has 1/n of the data. + + - clients connect to a cluster-wide broker instance, so the download links can be from any + node in the cluster. + + - if the clustered broker fails, the service is down. (should be reliable) + + - A node cannot announce each product that it has downloaded, using it's own node name, because + it does not know if other nodes have that product. (announce as dd1 vs. dd) + + - Either: + + -- Can only announce a product once it is clear that every active node has the product. + -- 1st come, 1st serve: apply fingerprint winnowing. Announce only node that got the data first. + + + - as in the independent configuration, nodes share queues and download a fraction upstream data. + They therefore need to exchange data amongst each other, but that means using a non-clustered + broker. So likely there will be two brokers access by the nodes, one node local, and one shared. + + - this is more complicated, but avoids the need for a clustered file system. hmm... pick your poison. + demo both? + +Shared-Data DD +~~~~~~~~~~~~~~ + + - The load balancer hands the incoming request to multiple nodes. + + - Each node has read/write access to a shared/cluster file system. + + - clustered broker configuration, all nodes see the same broker. + + - downloaded once means available everywhere (written to a shared disk) + + - so can advertise immediately with shared host spec (dd vs. dd1) + + - if the clustered broker fails, the service is down. (should be reliable) + + - if the clustered file system fails, the service is down. (??) + + + +SEP: Shared End-Point Configuration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The SEP configuration, all of the mover nodes are directly accessible to users. +The broker does not provide data service, just a pure message broker. Can be called +*data-less* pump, or a *bunny*. + +The broker is run clustered, and nothing can be said about the mover nodes. +Consumers and watchers can be started up by anyone on any collection of nodes, +and all data visible from any node where cluster file systems provide that benefit. + +Disk space administration is entirely a user configuration setting, not in +control of the application (users set ordinary quotas for their file systems directly) + diff --git a/_sources/Contribution/Development.rst.txt b/_sources/Contribution/Development.rst.txt new file mode 100644 index 000000000..b648c6aa9 --- /dev/null +++ b/_sources/Contribution/Development.rst.txt @@ -0,0 +1,1352 @@ +==================================== + MetPX-Sarracenia Developer's Guide +==================================== + +:version: |release| +:date: |today| + + +Tools you Need +-------------- + + +To hack on the Sarracenia source, you need: + +- A linux development environment, either a workstation, or a VM of some kind. + setup using ubuntu is automated, but adjustment for other distros is possible. + command-line comfort a must. + +- python3. The application is developed in and depends on python versions >= 3.5. + +- style: PEP8 except max line length is 119, enforced via `pycodestyle `_ for VSCode, yapf3 or other similar tool. + +- an account on github.com will help in submitting patches for consideration. + +Things that will be installed by automated setup: + +- a bunch of other python modules indicated in the dependencies (setup.py or debian/control) + +- python3 pyftpdlib module, used to run an ftpserver on a high port during the flow test. + +- git. in order to download the source from the github repository, and to prepare and submit + changes. + +- a dedicated rabbitmq broker, with administrative access, to run the sr_insects tests. + this is installed by automated tools for setting up the linux environment. + The flow test creates and destroys exchanges and will disrupt any active flows on the broker. + +after you have cloned the source code:: + + git clone -b development https://github.com/MetPX/sarracenia sr3 + git clone -b development https://github.com/MetPX/sarrac sr3c + git clone https://github.com/MetPX/sr_insects insects + cd sr3 + +The rest of the Guide takes the above for granted. + +Documentation +------------- + +`Documentation Standards `_ exist in /docs/Contribution/Documentation.rst +process for locally building the docs are there, as well as live web-site maintenance +methods. + +Where to Put Options +~~~~~~~~~~~~~~~~~~~~ + +Options are documented in sr3_options(7) dictionary style in alphabetic order. +Should it be worthwhile, examples of use could be added to other guides. + + +Development +----------- + +In general, the development workflow is to get a laptop or a VM where one can run +the flow_tests (available from http://github.com/MetPX/sr_insects ) The first step +in configuring a development environment is ensuring that the sr_insects flow tests +work, as they function as a gate for commits to important branches. + +Development is most commonly done on Ubuntu >=18.04 platform. + + +v2 Workflow +~~~~~~~~~~~ + +Finished development work for version 2 is committed to on the v2_dev branch, which is used +to produce daily snapshots. One should not normally commit changes to the v2_dev branch, +but rather merge them from a working branch. + +Development branches are named after the issue they are meant to address "v2_issue365", for +example. If there are multiple attempts to address a given issue, then use the issue +as a name prefix. For example, there could be issue365, but if we decide that isn't +a good way to address the issue, there could be an issue365_methodB branch. + +**Before submitting a pull-request (PR), please ensure that the flow tests from +sr_insects have been run successfully: at least static_flow, flakey_broker, and dynamic_flow** + +When a PR is generated, the second developer can look it over for concerns. +Once satisfied with the nature of the patch, the second developer should pull the branch +and run the flow tests again (the same three) to confirm. Only after the flow tests +have been run on multiple machines should a change be merged to stable. + +issues unique to v2 should be tagged *v2only*. +on Launchpad.net: + + * daily repository packages of v2 will be build from v2_dev + + * pre-release repository packages of v2 will be build from v2_dev + + * release repository packages are generated from v2_stable. + + +v3 Workflow +~~~~~~~~~~~ + +The upcoming version of Sarracenia is developed in the development (work in progress) branch. +As the major refactor is substantially complete, the remaining work is now entirely constructive +and all development is co-ordinated through issues exactly as v2 is. Issues unique to v3, be +they regressions or enhancements that don't make sense to add to v2, have the tag *v3only*. +Issues that are common between the releases are tagged *v3*. + +The workflow with v3 is similar to v2 but with different branches. branches are assumed +to be branched from the *development* branch, so v3 is assumed unless v2\_ is present. +Having all the flow tests complete fairly successfully +is one criterion for acceptance into development. + +To run the sr_insects tests, the repository must be cloned with the development branch. +A gate for merging to development is for a second developer to run the flow_tests. +**For v03, these tests must run: static_flow, flakey_broker, dynamic_flow, transform_flow** + + + + * launchpad has recipes to produce metpx-sr3 packages from various branches. + + * The *MetPX Daily* repository is a snapshot of the development branch. + + * The *MetPX Pre-Release* repository should receive versions ending in rcX (release candidate) + The packages here from from pre-release branch which comes from snapshots of the development branch. + There is also a pre-release-py36 branch for building pre-release packages for older operating systems. + + * stable comes from on snapshots of (version 3) pre-release branch. + + * The *MetPX* repository should only contain stable releases that have graduated from the rcX series. + there is a stable_py36 branch to build packages for older operating systems that have + python 3.6 (redhat 8, ubuntu 18, ubuntu 20) or are too old to use hatchling installer. + + +sr_insects +~~~~~~~~~~ + +The sr_insects repository has it's own issues DB, and work on sr_insects is encouraged. +Both v2 and v3 are supported on the stable branch of sr_insects. That branch should be +used to support all development in both versions.... + + +Local Installation +------------------ + +There are many different ways to install python packages on a computer. Different developers +will prefer different methods, and all the methods need to be tested prior to each release. +Sarracenia can work with either mqtt or amqp (most mature and stable) message passing libraries. +Install one of those first. in these examples, we use amqp. + +* **Wheel** when people are running different operating systems (non-ubuntu, non-debian) people will be installing wheels, typically that have been uploaded to pypi.python.org. On the other hand, it is a bit of a pain/noise to upload every development version, so we only upload releases, so testing of wheels is done by building local wheels. Need to build a new wheel every time a change is made. + +* **pip install metpx-sr3[amqp]** would pull a wheel down from pypi.python.org. Generally not used during development of Sarracenia itself. + one could also pull in all possible dependencies with **pip install metpx-sr3[all]** +* **pip install -e .[amqp] ...** lets you edit the source code of the installed package, ideal for debugging problems, because it allows live changes to the application without having to go through building and installing a new package. + +* **apt install metpx-sr3** install debian package from repositories, similarly to pip install (not -e), normally dev snapshots are not uploaded to repositories, so while this would be the normal way for users of ubuntu servers, it is not available during development of the package itself. Also need **apt install python3-amqp** + +* **dpkg -i** builds a debian package for local installation. This is how packages are tested prior to upload to repositories. It can also be used to support development (have to run dpkg -i for each package change.) also need **apt install python3-amqp** + +The sr_insects tests invokes the version of metpx-sarracenia that is installed on the system, +and not what is in the development tree. It is necessary to install the package on +the system in order to have it run the sr_insects tests. + +Prepare a Vanilla VM +~~~~~~~~~~~~~~~~~~~~ + +This section describes creating a test environment for use in a virtual machine. One way to build +a virtual machine is to use multipass (https://multipass.run) Assuming it is installed, one can +create a vm with:: + + multipass launch -m 8G -d 30G --name flow + +need to have ssh localhost work in the multipass container. Can do that by copying multipass +private key into the container:: + + fractal% multipass list + Name State IPv4 Image + primary Stopped -- Ubuntu 20.04 LTS + flow Running 10.23.119.56 Ubuntu 20.04 LTS + keen-crow Running 10.23.119.5 Ubuntu 20.04 LTS + fractal% + +Weird issues with ssh keys not being interpreted properly by paramiko, work around +( https://stackoverflow.com/questions/54612609/paramiko-not-a-valid-rsa-private-key-file ) +:: + + fractal% sudo cat /var/snap/multipass/common/data/multipassd/ssh-keys/id_rsa | sed 's/BEGIN .*PRIVATE/BEGIN RSA PRIVATE/;s/END .*PRIVATE/END RSA PRIVATE/' >id_rsa_container + chmod 600 id_rsa_container + scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.175:/home/ubuntu/.ssh/id_rsa + 100% 1704 2.7MB/s 00:00 + + fractal% scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.106:/home/ubuntu/.ssh/id_rsa + The authenticity of host '10.23.119.106 (10.23.119.106)' can't be established. + ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8. + Are you sure you want to continue connecting (yes/no/[fingerprint])? yes + Warning: Permanently added '10.23.119.106' (ECDSA) to the list of known hosts. + id_rsa_container 100% 1712 9.4MB/s 00:00 + fractal% multipass shell flow + Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64) + + * Documentation: https://help.ubuntu.com + * Management: https://landscape.canonical.com + * Support: https://ubuntu.com/advantage + + System information as of Fri Aug 27 21:12:16 EDT 2021 + + System load: 0.42 Processes: 112 + Usage of /: 4.4% of 28.90GB Users logged in: 0 + Memory usage: 5% IPv4 address for ens4: 10.23.119.106 + Swap usage: 0% + + + 1 update can be applied immediately. + To see these additional updates run: apt list --upgradable + + + To run a command as administrator (user "root"), use "sudo ". + See "man sudo_root" for details. + + ubuntu@flow:~$ + +then prompt ssh to accept the localhost key:: + + ubuntu@flow:~$ ssh localhost ls -a + The authenticity of host 'localhost (127.0.0.1)' can't be established. + ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8. + Are you sure you want to continue connecting (yes/no/[fingerprint])? yes + Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts. + . + .. + .bash_logout + .bashrc + .cache + .profile + .ssh + ubuntu@flow:~$ + + +This will provide a shell in an initialized VM. To configure it:: + + + git clone -b development https://github.com/MetPX/sarracenia sr3 + cd sr3 + +There are scripts that automate the installation of necessary environment to be able to run tests:: + + travis/flow_autoconfig.sh + travis/add_sr3.sh + +You should be able to see an empty configuration:: + + sr3 status + +sr3c and sr3 are now installed, and should be ready to run a flow test from the sr_insects module, which +has also been cloned: + + cd ../sr_insects + +The v03 branch of sr_insects supports testing of both v2 and v3, and both versions are now installed. +The flow tests are intended to be run to confirm compatibility between v2 and v3, and so one +must be able to test v2 as well:: + + ubuntu@flow:~/sr_insects$ dpkg -l | grep metpx + ii metpx-libsr3c 3.21.08a1-0~202108270410~ubuntu20.04.1 amd64 C-Implementation of a Sarracenia Client + ii metpx-sarracenia 2.21.08-0~202108241854~ubuntu20.04.1 all Directory mirroring in real-time for users, file servers and web sites. + ii metpx-sr3 3.00.008exp all v3 Directory mirroring in real-time for users, file servers and web sites. + ii metpx-sr3c 3.21.08a1-0~202108270410~ubuntu20.04.1 amd64 C-Implementation of a Sarracenia Client + ubuntu@flow:~/sr_insects$ + +The v2 package is metpx-sarracenia, whereas the v3 one is metpx-sr3. the flow tests will detect +which version is installed and test v3 if both are present. To override that:: + + ubuntu@flow:~/sr_insects$ export sarra_py_version=2.21.08 + ubuntu@flow:~/sr_insects$ + +Then one can run flow_tests from this shell normally. + +Ubuntu 18.04 +~~~~~~~~~~~~ + +A number of systems run Ubuntu 18.04 even though it is pretty old. + +''' + +multipass launch -m 8G bionic + + +''' + +can run developer tests as per multipass as described above. + +Python Wheel +~~~~~~~~~~~~ + +If you have not used add_sr3.sh (which builds a debian package), then one can use this procedure +for local installation on a computer with a python wheel for testing and development:: + + python3 setup.py bdist_wheel + +or... on newer systems, using build instead:: + + python3 -m build --no-isolation + +Should build a wheel in the dist sub-directory. +then as root install that new package:: + + pip3 install --upgrade .../dist/metpx*.whl + +Local Pip install +~~~~~~~~~~~~~~~~~ + +For local installation on a computer, using a pip +For testing and development:: + + pip3 install -e . + export PATH=${HOME}/.local/bin:${PATH} + +Using the local python package installer (PIP) to create a locally editable version. +The above will install the package in ~/.local/bin... so need to ensure the path includes +that directory. + + + + + +Debian/Ubuntu +~~~~~~~~~~~~~ + +For local installation on a computer, using a debian package. +This process builds a local .deb in the parent directory using standard debian mechanisms. +- Check the **build-depends** line in *debian/control* for dependencies that might be needed to build from source. +- The following steps will build sarracenia but not sign the changes or the source package:: + + cd metpx/sarracenia + sudo apt-get install devscripts + debuild -uc -us + sudo dpkg -i ../ + +which accomplishes the same thing using debian packaging. +The options are detailed below: + + +Committing Code +~~~~~~~~~~~~~~~ + +What should be done prior to committing to the development branch? +Checklist: + +- do development on some other branch. Usually the branch will be named after the issue being + addressed. Example: issue240, if we give up on an initial approach and start another one, + there may be issue240_2 for a second attempt. There may also be feature branches, such as v03. + +- **sr_insects tests works** (See Testing) The development branch should always be functional, do not commit code if the sr_insects tests are not working. + +- Natural consequence: if the code changes means tests need to change, include the test change in the commit. + +- **update doc/** manual pages should get their updates ideally at the same time as the code. + +Usually there will be many such cycles on an issueXXX branch before one is ready +to issue a pull request. Eventually, we get to `Commits to the Development Branch`_ + + +sr_insects Tests Description +---------------------------- + +Before committing code to the stable branch, as a Quality Assurance measure, one should run +all available self-tests. It is assumed that the specific changes in the code have already been unit +tested. Please add self-tests as appropriate to this process to reflect the new ones. +Generally speaking one should solve problems at the first test that fails as each test +is more complicated than the previous one. + +There is a separate git repository containing the more complex tests https://github.com/MetPX/sr_insects + +A typical development workflow will be (Do not try this, this is just an overview of the steps that will be +explained in detail in following sections):: + + git branch issueXXX + git checkout issueXXX + cd sarra ; *make coding changes* + cd .. + debuild -uc -us + cd ../sarrac + debuild -uc -us + sudo dpkg -i ../*.deb + cd .. + + git clone -b development https://github.com/MetPX/sr_insects + cd sr_insects + sr3 status # make sure there are no components configured before you start. + # test results will likely be skewed otherwise. + for test in unit static_flow flakey_browser transform_flow dynamic_flow; do + cd $test + ./flow_setup.sh # *starts the flows* + ./flow_limit.sh # *stops the flows after some period (default: 1000) * + ./flow_check.sh # *checks the flows* + ./flow_cleanup.sh # *cleans up the flows* + cd .. + done + + #assuming all the tests pass. + git commit -a # on the branch... + + +Unit +~~~~ + +The *unit* test in sr_insects is the shortest one taking a minute or so, and not requiring +much configuration at all. They are sanity tests of code behaviour. Generally takes a minute +or two on a laptop. + +Static Flow +~~~~~~~~~~~ + +The *static_flow* tests are a bit more complicated, testing more components, using single +threaded components in a linear way (all data moves uniformly forward.) It should be +more straight-forward to identify issues as there is no deletion and so it lends itself well +to repeating subset tests to identify individual issues. It takes about two minutes on a laptop. + +Flakey Broker +~~~~~~~~~~~~~ + +The *flakey_broker* tests are the same as the *static_flow*, but slowed down so that they last +a few minutes, and the broker is shutdown and restarted while the posting is happenning. +Note that post_log prints before a notification message is posted (because post_log is an on_post plugin, and +that action, allows one to modify the notification message, so it needs to be before the post actually happens.) + + +Dynamic Flow +~~~~~~~~~~~~ + +The *dynamic_flow* test add advanced features: multi-instances, the winnow component, retry logic testing, +and includes file removals as well. Most of the documentation here refers to runnig the +dynamic_flow test, as it is the most complicated one, and the ancestor of the others. The unit +test was separated out from the beginnig of the dynamic_flow test, and the static_flow is +a simplified version of the original flow test as well. + +Generally speaking, one should run the tests in sequence and ensure the results of earlier +tests are good before proceeding to the next test. + +Note that the development system must be configured for the sr_insects tests to run successfully. See the next +section for configuration instructions. For development with a fresh OS installation, +the configuration steps have been automated and can be applied with the flow_autoconfig.sh +script in sr_insects (https://github.com/MetPX/sr_insects/blob/stable/flow_autoconfig.sh). Blind +execution of this script on a working system may lead to undesirable side effects; you have been warned! + + +The configuration one is trying to replicate: + +.. image:: Development/Flow_test.svg + + +Following table describes what each element of the dynamic flow test does, and the test coverage +shows functionality covered. + ++-------------------+--------------------------------------+-------------------------------------+ +| | | | +| Configuration | Does | Test Coverage | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| subscribe t_ddx | copy from data mart to local broker | read amqps public data mart (v02) | +| | posting notification messages to | as ordinary user. | +| | local xwinno00 and xwinnow01 | | +| | exchanges. | shared queue and multiple processes | +| | | 3 instances download from each q | +| | | | +| | | post amqp to a local exchange (v02) | +| | | as feeder(admin) user | +| | | | +| | | post_exchangeSplit to xwinnow0x | ++-------------------+--------------------------------------+-------------------------------------+ +| winnow t0x_f10 | winnow processing publish for xsarra | read local amqp v02 | +| | exchange for downloading. | as feeder user. | +| | | | +| | | complete caching (winnow) function | +| | as two sources identical, only half | | +| | notification messages are posted to | post amqp v02 to local excchange. | +| | next | | ++-------------------+--------------------------------------+-------------------------------------+ +| sarra download | download the winnowed data from the | read local amqp v02 (xsarra) | +| f20 | data mart to a local directory | | +| | (TESTDOCROOT= ~/sarra_devdocroot) | download using built-in python | +| | | | +| | add a header at application layer | shared queue and multiple processes | +| | longer than 255 characters. | 5 instances download from each q | +| | | | +| | | download using accel_wget plugin | +| | | | +| | | AMQP header truncation on publish. | +| | | | +| | | post amqp v02 to xpublic | +| | | as feeder user | +| | | as http downloads from localhost | ++-------------------+--------------------------------------+-------------------------------------+ +| subscribe t | download as client from localhost | read amqp from local broker | +| | to downloaded_by_sub_t directory. | as ordinary user/client. | +| | | | +| | | shared queue and multiple processes | +| | | 5 instances download from each q | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| watch f40 | watch downloaded_by_sub_t | client v03 post of local file. | +| | (post each file that appears there.) | (file: url) | +| | | | +| | memory ceiling set low | auto restarting on memory ceiling. | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| sender | read local file, send via sftp | client consume v03 notification | +| tsource2send | to sent_by_tsource2send directory | messages. | +| | | consumer read local file. | +| | post to xs_tsource_output | | +| | | send via sftp. | +| | | | +| | | plugin replace_dir | +| | | | +| | | posting sftp url. | +| | | post v02 (converting v03 back.) | +| | | | +| | | test post_exchangeSuffix option. | ++-------------------+--------------------------------------+-------------------------------------+ +| subscribe | download via sftp from localhost | client sftp download. | +| u_sftp_f60 | putting files in downloaded_by_sub_u | | +| | directory. | accel_sftp plugin. | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| post test2_f61 | post files in sent_by_tsource2send | explicit file posting | +| | with ftp URL's in the | | +| | xs_tsource_poll exchange | ftp URL posting. | +| | | | +| | (wrapper script calls post) | post_exchangeSuffix option | ++-------------------+--------------------------------------+-------------------------------------+ +| poll f62 | poll sent_by_tsource2send directory | polling | +| | posting sftp download URL's | | +| | | post_exchangeSuffix option | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| subscribe ftp_f70 | subscribe to test2_f61 ftp' posts. | ftp url downloading. | +| | download files from localhost | | +| | to downloaded_by_sub_u directory. | | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| subscribe q_f71 | subscribe to poll, downloading | confirming poll post quality. | +| | to recd_by_srpoll_test1 | | ++-------------------+--------------------------------------+-------------------------------------+ +| shovel pclean f90 | clean up files so they don't | shovel function. | +| | accumulate | | +| | fakes failures to exercise retries | | +| | | retry logic. | +| | | | ++-------------------+--------------------------------------+-------------------------------------+ +| shovel pclean f91 | clean up files so they don't | shovel with posting v03 | +| | accumulate | | +| | | retry logic. | ++-------------------+--------------------------------------+-------------------------------------+ +| shovel pclean f92 | clean up files so they don't | shovel with consuming v03 | +| | accumulate | | +| | | posting v02. | +| | | | +| | | retry logic. | ++-------------------+--------------------------------------+-------------------------------------+ + +Assumption: test environment is a Linux PC, either a laptop/desktop, or a server on which one +can start a browser. If working with the C implementation as well, there are also the following +flows defined: + +.. image:: Development/cFlow_test.svg + + +Running Flow Tests +------------------ + +This section documents these steps in much more detail. +Before one can run the sr_insects tests, some pre-requisites must be taken care of. +Note that there is Github Actions integration for at least the development branch +to verify functionality on a variety of python version. Consult:: + + https://github.com/MetPX/sarracenia/actions + +.. Note:: + + for the latest test results. Note that the results include dozens of tests, and are + a bit unreliable, typically it may take a few retries for it to work completely + (3 or 4 fail after initial attempt, then re-run the failed ones, and then + perhaps 1 or two will be left, and on the third pass the last one passes.) + + +Install Servers on Workstation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To prepare a computer to run the flow test, one must install some server +software and configurations. This same work is done by travis/flow_autoconfig.sh +which is run in `Prepare a Vanilla VM`_ but if you need to configure it +manually, below is the process. + +Install a minimal localhost broker and configure rabbitmq test users:: + + sudo apt-get install rabbitmq-server + sudo rabbitmq-plugins enable rabbitmq_management + + mkdir ~/.config/sarra + cat > ~/.config/sarra/default.conf << EOF + declare env FLOWBROKER=localhost + declare env MQP=amqp + declare env SFTPUSER=${USER} + declare env TESTDOCROOT=${HOME}/sarra_devdocroot + declare env SR_CONFIG_EXAMPLES=${HOME}/git/sarracenia/sarra/examples + EOF + + RABBITMQ_PASS=S0M3R4nD0MP4sS + cat > ~/.config/sarra/credentials.conf << EOF + amqp://bunnymaster:${RABBITMQ_PASS}@localhost/ + amqp://tsource:${RABBITMQ_PASS}@localhost/ + amqp://tsub:${RABBITMQ_PASS}@localhost/ + amqp://tfeed:${RABBITMQ_PASS}@localhost/ + amqp://anonymous:${RABBITMQ_PASS}@localhost/ + amqps://anonymous:anonymous@hpfx.collab.science.gc.ca + amqps://anonymous:anonymous@hpfx1.collab.science.gc.ca + amqps://anonymous:anonymous@hpfx2.collab.science.gc.ca + amqps://anonymous:anonymous@dd.weather.gc.ca + amqps://anonymous:anonymous@dd1.weather.gc.ca + amqps://anonymous:anonymous@dd2.weather.gc.ca + ftp://anonymous:anonymous@localhost:2121/ + EOF + + cat > ~/.config/sarra/admin.conf << EOF + cluster localhost + admin amqp://bunnymaster@localhost/ + feeder amqp://tfeed@localhost/ + declare source tsource + declare subscriber tsub + declare subscriber anonymous + EOF + + sudo rabbitmqctl delete_user guest + + sudo rabbitmqctl add_user bunnymaster ${RABBITMQ_PASS} + sudo rabbitmqctl set_permissions bunnymaster ".*" ".*" ".*" + sudo rabbitmqctl set_user_tags bunnymaster administrator + + sudo systemctl restart rabbitmq-server + cd /usr/local/bin + sudo mv rabbitmqadmin rabbitmqadmin.1 + sudo wget http://localhost:15672/cli/rabbitmqadmin + sudo chmod 755 rabbitmqadmin + + sr3 --users declare + +.. Note:: + + Please use other passwords in credentials for your configuration, just in case. + Passwords are not to be hard coded in self test suite. + The users bunnymaster, tsource, tsub, and tfeed are to be used for running tests. + + The idea here is to use tsource, tsub, and tfeed as broker accounts for all + self-test operations, and store the credentials in the normal credentials.conf file. + No passwords or key files should be stored in the source tree, as part of a self-test + suite. + +Setup Flow Test Environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Once the server environment is established, the flow tests use sftp transfers to localhost. + +It is also required that passwordless ssh access is configured on the test host +for the system user that will run the flow test. This can be done by creating +a private/public ssh key pair for the user (if there isn't one already) and copying +the public key to the authorized_keys file in the same directory as the keys (~/.ssh). +For associated commands, see http://www.linuxproblem.org/art_9.html + +Note that on systems where older versions of Paramiko (< 2.7.2) are installed, and the ssh key pair was generated with OpenSSH >= 6.5, manually testing the below command will work, but Paramiko will not be able to connect. This is likely the case if the ``~/.ssh/id_rsa`` file contains ``BEGIN OPENSSH PRIVATE KEY``. To work around this, convert the private key's format using ``ssh-keygen -p -m PEM -f ~/.ssh/id_rsa``. + +To confirm that that passwordless ssh to localhost works:: + + ssh localhost ls + +This should run and complete. If it prompts for a password, the flow tests will not work. + +Check that the broker is working:: + + systemctl status rabbitmq-server + +One part of the flow test runs an sftp server, and uses sftp client functions. +Need the following package for that:: + + sudo apt-get install python3-pyftpdlib python3-paramiko + + + +The setup script starts a trivial web server, and ftp server, and a daemon that invokes sr_post. +It also tests the C components, which need to have been already installed as well +and defines some fixed test clients that will be used during self-tests:: + + cd + git clone https://github.com/MetPX/sr_insects + cd sr_insects + cd static_flow + . ./flow_setup.sh + + blacklab% ./flow_setup.sh + cleaning logs, just in case + rm: cannot remove '/home/peter/.cache/sarra/log/*': No such file or directory + Adding flow test configurations... + 2018-02-10 14:22:58,944 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/cno_trouble_f00.inc to /home/peter/.config/sarra/cpump/cno_trouble_f00.inc. + 2018-02-10 09:22:59,204 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/no_trouble_f00.inc to /home/peter/.config/sarra/shovel/no_trouble_f00.inc + 2018-02-10 14:22:59,206 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpost/veille_f34.conf to /home/peter/.config/sarra/cpost/veille_f34.conf. + 2018-02-10 14:22:59,207 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd1_f04.conf to /home/peter/.config/sarra/cpump/pelle_dd1_f04.conf. + 2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd2_f05.conf to /home/peter/.config/sarra/cpump/pelle_dd2_f05.conf. + 2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f14.conf to /home/peter/.config/sarra/cpump/xvan_f14.conf. + 2018-02-10 14:22:59,209 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f15.conf to /home/peter/.config/sarra/cpump/xvan_f15.conf. + 2018-02-10 09:22:59,483 [INFO] copying /home/peter/src/sarracenia/sarra/examples/poll/f62.conf to /home/peter/.config/sarra/poll/f62.conf + 2018-02-10 09:22:59,756 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/shim_f63.conf to /home/peter/.config/sarra/post/shim_f63.conf + 2018-02-10 09:23:00,030 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/test2_f61.conf to /home/peter/.config/sarra/post/test2_f61.conf + 2018-02-10 09:23:00,299 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/tsarra_f20.conf to /home/peter/.config/sarra/report/tsarra_f20.conf + 2018-02-10 09:23:00,561 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow00_f10.conf to /home/peter/.config/sarra/report/twinnow00_f10.conf + 2018-02-10 09:23:00,824 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow01_f10.conf to /home/peter/.config/sarra/report/twinnow01_f10.conf + 2018-02-10 09:23:01,086 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sarra/download_f20.conf to /home/peter/.config/sarra/sarra/download_f20.conf + 2018-02-10 09:23:01,350 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sender/tsource2send_f50.conf to /home/peter/.config/sarra/sender/tsource2send_f50.conf + 2018-02-10 09:23:01,615 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd1_f00.conf to /home/peter/.config/sarra/shovel/t_dd1_f00.conf + 2018-02-10 09:23:01,877 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd2_f00.conf to /home/peter/.config/sarra/shovel/t_dd2_f00.conf + 2018-02-10 09:23:02,137 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cclean_f91.conf to /home/peter/.config/sarra/subscribe/cclean_f91.conf + 2018-02-10 09:23:02,400 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cdnld_f21.conf to /home/peter/.config/sarra/subscribe/cdnld_f21.conf + 2018-02-10 09:23:02,658 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cfile_f44.conf to /home/peter/.config/sarra/subscribe/cfile_f44.conf + 2018-02-10 09:23:02,921 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/clean_f90.conf to /home/peter/.config/sarra/subscribe/clean_f90.conf + 2018-02-10 09:23:03,185 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cp_f61.conf to /home/peter/.config/sarra/subscribe/cp_f61.conf + 2018-02-10 09:23:03,455 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/ftp_f70.conf to /home/peter/.config/sarra/subscribe/ftp_f70.conf + 2018-02-10 09:23:03,715 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/q_f71.conf to /home/peter/.config/sarra/subscribe/q_f71.conf + 2018-02-10 09:23:03,978 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/t_f30.conf to /home/peter/.config/sarra/subscribe/t_f30.conf + 2018-02-10 09:23:04,237 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/u_sftp_f60.conf to /home/peter/.config/sarra/subscribe/u_sftp_f60.conf + 2018-02-10 09:23:04,504 [INFO] copying /home/peter/src/sarracenia/sarra/examples/watch/f40.conf to /home/peter/.config/sarra/watch/f40.conf + 2018-02-10 09:23:04,764 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t00_f10.conf to /home/peter/.config/sarra/winnow/t00_f10.conf + 2018-02-10 09:23:05,027 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t01_f10.conf to /home/peter/.config/sarra/winnow/t01_f10.conf + Initializing with sr_audit... takes a minute or two + OK, as expected 18 queues existing after 1st audit + OK, as expected 31 exchanges for flow test created. + Starting trivial http server on: /home/peter/sarra_devdocroot, saving pid in .httpserverpid + Starting trivial ftp server on: /home/peter/sarra_devdocroot, saving pid in .ftpserverpid + running self test ... takes a minute or two + sr_util.py TEST PASSED + sr_credentials.py TEST PASSED + sr_config.py TEST PASSED + sr_cache.py TEST PASSED + sr_retry.py TEST PASSED + sr_consumer.py TEST PASSED + sr_http.py TEST PASSED + sftp testing start... + sftp testing config read... + sftp testing fake message built ... + sftp sr_ftp instantiated ... + sftp sr_ftp connected ... + sftp sr_ftp mkdir ... + test 01: directory creation succeeded + test 02: file upload succeeded + test 03: file rename succeeded + test 04: getting a part succeeded + test 05: download succeeded + test 06: onfly_checksum succeeded + Sent: bbb into tztz/ddd 0-5 + test 07: download succeeded + test 08: delete succeeded + Sent: bbb into tztz/ddd 0-5 + Sent: bbb into tztz/ddd 0-5 + Sent: bbb into tztz/ddd 0-5 + Sent: bbb into tztz/ddd 0-5 + Sent: bbb into tztz/ddd 0-5 + /home/peter + /home/peter + test 09: bad part succeeded + sr_sftp.py TEST PASSED + sr_instances.py TEST PASSED + OK, as expected 9 tests passed + Starting flow_post on: /home/peter/sarra_devdocroot, saving pid in .flowpostpid + Starting up all components (sr start)... + done. + OK: sr3 start was successful + Overall PASSED 4/4 checks passed! + blacklab% + +As it runs the setup, it also executes all existing unit_tests. +Only proceed to the flow_check tests if all the tests in flow_setup.sh pass. + + +Run A Flow Test +~~~~~~~~~~~~~~~ + +The flow_check.sh script reads the log files of all the components started, and compares the number +of notification messages, looking for a correspondence within +- 10% It takes a few minutes for the +configuration to run before there is enough data to do the proper measurements:: + + ./flow_limit.sh + +sample output:: + + initial sample building sample size 8 need at least 1000 + sample now 1021 + Sufficient! + stopping shovels and waiting... + 2017-10-28 00:37:02,422 [INFO] sr_shovel t_dd1_f00 0001 stopping + 2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: instances option not implemented, ignored. + info: instances option not implemented, ignored. + 2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: report option not implemented, ignored. + info: report option not implemented, ignored. + 2017-10-28 00:37:02,436 [INFO] sr_shovel t_dd2_f00 0001 stopping + running instance for config pelle_dd1_f04 (pid 15872) stopped. + running instance for config pelle_dd2_f05 (pid 15847) stopped. + maximum of the shovels is: 1022 + + +Then check show it went with flow_check.sh:: + + TYPE OF ERRORS IN LOG : + + 1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/' + 1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/' + + + test 1 success: shovels t_dd1_f00 ( 1022 ) and t_dd2_f00 ( 1022 ) should have about the same number of items read + test 2 success: sarra tsarra (1022) should be reading about half as many items as (both) winnows (2240) + test 3 success: tsarra (1022) and sub t_f30 (1022) should have about the same number of items + test 4 success: max shovel (1022) and subscriber t_f30 (1022) should have about the same number of items + test 5 success: count of truncated headers (1022) and subscribed messages (1022) should have about the same number of items + test 6 success: count of downloads by subscribe t_f30 (1022) and messages received (1022) should be about the same + test 7 success: downloads by subscribe t_f30 (1022) and files posted by watch (1022) should be about the same + test 8 success: posted by watch(1022) and sent by sr_sender (1022) should be about the same + test 9 success: 1022 of 1022: files sent with identical content to those downloaded by subscribe + test 10 success: 1022 of 1022: poll test1_f62 and subscribe q_f71 run together. Should have equal results. + test 11 success: post test2_f61 1022 and subscribe r_ftp_f70 1021 run together. Should be about the same. + test 12 success: cpump both pelles (c shovel) should receive about the same number of messages (3665) (3662) + test 13 success: cdnld_f21 subscribe downloaded (1022) the same number of files that was published by both van_14 and van_15 (1022) + test 14 success: veille_f34 should post the same number of files (1022) that subscribe cdnld_f21 downloaded (1022) + test 15 success: veille_f34 should post the same number of files (1022) that subscribe cfile_f44 downloaded (1022) + test 16 success: Overall 15 of 15 passed! + + blacklab% + +If the flow_check.sh passes, then one has a reasonable confidence in the overall functionality of the +python application, but the test coverage is not exhaustive. This is the lowest gate for committing +changes to thy python code into the development branch. It is more qualitative sampling of the most +common use cases rather than a thorough examination of all functionality. While not +thorough, it is good to know the flows are working. + +Note that the *fclean* subscriber looks at files in and keeps files around long enough for them to go through all the other +tests. It does this by waiting a reasonable amount of time (45 seconds, the last time checked.) then it compares the file +that have been posted by watch to the files created by downloading from it. As the *sample now* count proceeds, +it prints "OK" if the files downloaded are identical to the ones posted by sr_watch. The addition of fclean and +the corresponding cfclean for the cflow_test, are broken. The default setup which uses *fclean* and *cfclean* ensures +that only a few minutes worth of disk space is used at a given time, and allows for much longer tests. + +By default, the flow_test is only 1000 files, but one can ask it to run longer, like so:: + + ./flow_limit.sh 50000 + +To accumulate fifty thousand files before ending the test. This allows testing of long term performance, especially +memory usage over time, and the housekeeping functions of on_heartbeat processing. + + +Flow Cleanup +~~~~~~~~~~~~ + +When done testing, run the ./flow_cleanup.sh script, which will kill the +running servers and daemons, and delete all configuration files installed for +the flow test, all queues, exchanges, and logs. This also needs to be done +between each run of the flow test:: + + blacklab% ./flow_cleanup.sh + Stopping sr... + Cleanup sr... + Cleanup trivial http server... + web server stopped. + if other web servers with lost pid kill them + Cleanup trivial ftp server... + ftp server stopped. + if other ftp servers with lost pid kill them + Cleanup flow poster... + flow poster stopped. + if other flow_post.sh with lost pid kill them + Deleting queues: + Deleting exchanges... + Removing flow configs... + 2018-02-10 14:17:34,150 [INFO] info: instances option not implemented, ignored. + 2018-02-10 14:17:34,150 [INFO] info: report option not implemented, ignored. + 2018-02-10 14:17:34,353 [INFO] info: instances option not implemented, ignored. + 2018-02-10 14:17:34,353 [INFO] info: report option not implemented, ignored. + 2018-02-10 09:17:34,837 [INFO] sr_poll f62 cleanup + 2018-02-10 09:17:34,845 [INFO] deleting exchange xs_tsource_poll (tsource@localhost) + 2018-02-10 09:17:35,115 [INFO] sr3_post shim_f63 cleanup + 2018-02-10 09:17:35,122 [INFO] deleting exchange xs_tsource_shim (tsource@localhost) + 2018-02-10 09:17:35,394 [INFO] sr3_post test2_f61 cleanup + 2018-02-10 09:17:35,402 [INFO] deleting exchange xs_tsource_post (tsource@localhost) + 2018-02-10 09:17:35,659 [INFO] sr_report tsarra_f20 cleanup + 2018-02-10 09:17:35,659 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:35,661 [INFO] deleting queue q_tfeed.sr_report.tsarra_f20.89336558.04455188 (tfeed@localhost) + 2018-02-10 09:17:35,920 [INFO] sr_report twinnow00_f10 cleanup + 2018-02-10 09:17:35,920 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:35,922 [INFO] deleting queue q_tfeed.sr_report.twinnow00_f10.35552245.50856337 (tfeed@localhost) + 2018-02-10 09:17:36,179 [INFO] sr_report twinnow01_f10 cleanup + 2018-02-10 09:17:36,180 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:36,182 [INFO] deleting queue q_tfeed.sr_report.twinnow01_f10.48262886.11567358 (tfeed@localhost) + 2018-02-10 09:17:36,445 [WARNING] option url deprecated please use post_base_url + 2018-02-10 09:17:36,446 [WARNING] use post_base_dir instead of document_root + 2018-02-10 09:17:36,446 [INFO] sr_sarra download_f20 cleanup + 2018-02-10 09:17:36,446 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:36,448 [INFO] deleting queue q_tfeed.sr_sarra.download_f20 (tfeed@localhost) + 2018-02-10 09:17:36,449 [INFO] exchange xpublic remains + 2018-02-10 09:17:36,703 [INFO] sr_sender tsource2send_f50 cleanup + 2018-02-10 09:17:36,703 [INFO] AMQP broker(localhost) user(tsource) vhost(/) + 2018-02-10 09:17:36,705 [INFO] deleting queue q_tsource.sr_sender.tsource2send_f50 (tsource@localhost) + 2018-02-10 09:17:36,711 [INFO] deleting exchange xs_tsource_output (tsource@localhost) + 2018-02-10 09:17:36,969 [INFO] sr_shovel t_dd1_f00 cleanup + 2018-02-10 09:17:36,969 [INFO] AMQP broker(dd.weather.gc.ca) user(anonymous) vhost(/) + 2018-02-10 09:17:37,072 [INFO] deleting queue q_anonymous.sr_shovel.t_dd1_f00 (anonymous@dd.weather.gc.ca) + 2018-02-10 09:17:37,095 [INFO] exchange xwinnow00 remains + 2018-02-10 09:17:37,095 [INFO] exchange xwinnow01 remains + 2018-02-10 09:17:37,389 [INFO] sr_shovel t_dd2_f00 cleanup + 2018-02-10 09:17:37,389 [INFO] AMQP broker(dd.weather.gc.ca) user(anonymous) vhost(/) + 2018-02-10 09:17:37,498 [INFO] deleting queue q_anonymous.sr_shovel.t_dd2_f00 (anonymous@dd.weather.gc.ca) + 2018-02-10 09:17:37,522 [INFO] exchange xwinnow00 remains + 2018-02-10 09:17:37,523 [INFO] exchange xwinnow01 remains + 2018-02-10 09:17:37,804 [INFO] sr_subscribe cclean_f91 cleanup + 2018-02-10 09:17:37,804 [INFO] AMQP broker(localhost) user(tsub) vhost(/) + 2018-02-10 09:17:37,806 [INFO] deleting queue q_tsub.sr_subscribe.cclean_f91.39328538.44917465 (tsub@localhost) + 2018-02-10 09:17:38,062 [INFO] sr_subscribe cdnld_f21 cleanup + 2018-02-10 09:17:38,062 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:38,064 [INFO] deleting queue q_tfeed.sr_subscribe.cdnld_f21.11963392.61638098 (tfeed@localhost) + 2018-02-10 09:17:38,324 [WARNING] use post_base_dir instead of document_root + 2018-02-10 09:17:38,324 [INFO] sr_subscribe cfile_f44 cleanup + 2018-02-10 09:17:38,324 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:38,326 [INFO] deleting queue q_tfeed.sr_subscribe.cfile_f44.56469334.87337271 (tfeed@localhost) + 2018-02-10 09:17:38,583 [INFO] sr_subscribe clean_f90 cleanup + 2018-02-10 09:17:38,583 [INFO] AMQP broker(localhost) user(tsub) vhost(/) + 2018-02-10 09:17:38,585 [INFO] deleting queue q_tsub.sr_subscribe.clean_f90.45979835.20516428 (tsub@localhost) + 2018-02-10 09:17:38,854 [WARNING] extended option download_cp_command = ['cp --preserve=timestamps'] (unknown or not declared) + 2018-02-10 09:17:38,855 [INFO] sr_subscribe cp_f61 cleanup + 2018-02-10 09:17:38,855 [INFO] AMQP broker(localhost) user(tsource) vhost(/) + 2018-02-10 09:17:38,857 [INFO] deleting queue q_tsource.sr_subscribe.cp_f61.61218922.69758215 (tsource@localhost) + 2018-02-10 09:17:39,121 [INFO] sr_subscribe ftp_f70 cleanup + 2018-02-10 09:17:39,121 [INFO] AMQP broker(localhost) user(tsource) vhost(/) + 2018-02-10 09:17:39,123 [INFO] deleting queue q_tsource.sr_subscribe.ftp_f70.47997098.27633529 (tsource@localhost) + 2018-02-10 09:17:39,386 [INFO] sr_subscribe q_f71 cleanup + 2018-02-10 09:17:39,386 [INFO] AMQP broker(localhost) user(tsource) vhost(/) + 2018-02-10 09:17:39,389 [INFO] deleting queue q_tsource.sr_subscribe.q_f71.84316550.21567557 (tsource@localhost) + 2018-02-10 09:17:39,658 [INFO] sr_subscribe t_f30 cleanup + 2018-02-10 09:17:39,658 [INFO] AMQP broker(localhost) user(tsub) vhost(/) + 2018-02-10 09:17:39,660 [INFO] deleting queue q_tsub.sr_subscribe.t_f30.26453890.50752396 (tsub@localhost) + 2018-02-10 09:17:39,924 [INFO] sr_subscribe u_sftp_f60 cleanup + 2018-02-10 09:17:39,924 [INFO] AMQP broker(localhost) user(tsource) vhost(/) + 2018-02-10 09:17:39,927 [INFO] deleting queue q_tsource.sr_subscribe.u_sftp_f60.81353341.03950190 (tsource@localhost) + 2018-02-10 09:17:40,196 [WARNING] option url deprecated please use post_base_url + 2018-02-10 09:17:40,196 [WARNING] use post_broker to set broker + 2018-02-10 09:17:40,197 [INFO] watch f40 cleanup + 2018-02-10 09:17:40,207 [INFO] deleting exchange xs_tsource (tsource@localhost) + 2018-02-10 09:17:40,471 [INFO] sr_winnow t00_f10 cleanup + 2018-02-10 09:17:40,471 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:40,474 [INFO] deleting queue q_tfeed.sr_winnow.t00_f10 (tfeed@localhost) + 2018-02-10 09:17:40,480 [INFO] deleting exchange xsarra (tfeed@localhost) + 2018-02-10 09:17:40,741 [INFO] sr_winnow t01_f10 cleanup + 2018-02-10 09:17:40,741 [INFO] AMQP broker(localhost) user(tfeed) vhost(/) + 2018-02-10 09:17:40,743 [INFO] deleting queue q_tfeed.sr_winnow.t01_f10 (tfeed@localhost) + 2018-02-10 09:17:40,750 [INFO] deleting exchange xsarra (tfeed@localhost) + 2018-02-10 14:17:40,753 [ERROR] config cno_trouble_f00 not found. + Removing flow config logs... + rm: cannot remove '/home/peter/.cache/sarra/log/sr_audit_f00.log': No such file or directory + Removing document root ( /home/peter/sarra_devdocroot )... + Done! + +After the flow_cleanup.sh, to check that a test has completed, use:: + + sr3 status + +which should show that there are no active configurations. + +If the static_flow test works, then re-run the other tests: flakey_broker, +transform_flow, and dynamic_flow. + +Dynamic Flow Test Length +~~~~~~~~~~~~~~~~~~~~~~~~ + +While most tests have a fixed duration, the dynamic flow test queries a remote +server and can run for any length desired. The dynamic flow_test length defaults +to 1000 files being flowed through the test cases. When in rapid development, +one can supply an argument to shorten that:: + + ./flow_limit.sh 200 + +Towards the end of a development cycle, longer flow_tests are adviseable:: + + ./flow_limit.sh 20000 + +to identify more issues. sample run to 100,000 entries:: + + blacklab% ./flow_limit.sh 100000 + initial sample building sample size 155 need at least 100000 + sample now 100003 content_checks:GOOD missed_dispositions:0s:0 + Sufficient! + stopping shovels and waiting... + 2018-02-10 13:15:08,964 [INFO] 2018-02-10 13:15:08,964 [INFO] info: instances option not implemented, ignored. + info: instances option not implemented, ignored. + 2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored. + 2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored. + running instance for config pelle_dd2_f05 (pid 20031) stopped. + running instance for config pelle_dd1_f04 (pid 20043) stopped. + Traceback (most recent call last):ng... +   File "/usr/bin/rabbitmqadmin", line 1012, in +     main() +   File "/usr/bin/rabbitmqadmin", line 413, in main +     method() +   File "/usr/bin/rabbitmqadmin", line 593, in invoke_list +     format_list(self.get(uri), cols, obj_info, self.options) +   File "/usr/bin/rabbitmqadmin", line 710, in format_list +     formatter_instance.display(json_list) +   File "/usr/bin/rabbitmqadmin", line 721, in display +     (columns, table) = self.list_to_table(json.loads(json_list), depth) +   File "/usr/bin/rabbitmqadmin", line 775, in list_to_table +     add('', 1, item, add_to_row) +   File "/usr/bin/rabbitmqadmin", line 742, in add +     add(column, depth + 1, subitem, fun) +   File "/usr/bin/rabbitmqadmin", line 742, in add +     add(column, depth + 1, subitem, fun) +   File "/usr/bin/rabbitmqadmin", line 754, in add +     fun(column, subitem) +   File "/usr/bin/rabbitmqadmin", line 761, in add_to_row +     row[column_ix[col]] = maybe_utf8(val) +   File "/usr/bin/rabbitmqadmin", line 431, in maybe_utf8 +     return s.encode('utf-8') + AttributeError: 'float' object has no attribute 'encode' + maximum of the shovels is: 100008 + + +While it is running one can run flow_check.sh at any time:: + + NB retries for sr_subscribe t_f30 0 + NB retries for sr_sender 18 + +       1 /home/peter/.cache/sarra/log/sr_cpost_veille_f34_0001.log [ERROR] sr_cpost rename: /home/peter/sarra_devdocroot/cfr/observations/xml/AB/today/today_ab_20180210_e.xml cannot stat. +       1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/' +       1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/' +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905 +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905. Type: , Value: +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321 +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321. Type: , Value: +       1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CS/CWEG/12/CSCN03_CWEG_101200___12074 + more than 10 TYPES OF ERRORS found... for the rest, have a look at /home/peter/src/sarracenia/test/flow_check_errors_logged.txt for details + + test  1 success: shovels t_dd1_f00 (100008) and t_dd2_f00 (100008) should have about the same number of items read + test  2 success: sarra tsarra (100008) should be reading about half as many items as (both) winnows (200016) + test  3 success: tsarra (100008) and sub t_f30 (99953) should have about the same number of items + test  4 success: max shovel (100008) and subscriber t_f30 (99953) should have about the same number of items + test  5 success: count of truncated headers (100008) and subscribed messages (100008) should have about the same number of items + test  6 success: count of downloads by subscribe t_f30 (99953) and messages received (100008) should be about the same + test  7 success: same downloads by subscribe t_f30 (199906) and files posted (add+remove) by watch (199620) should be about the same + test  8 success: posted by watch(199620) and subscribed cp_f60 (99966) should be about half as many + test  9 success: posted by watch(199620) and sent by sr_sender (199549) should be about the same + test 10 success: 0 messages received that we don't know what happenned. + test 11 success: sarra tsarra (100008) and good audit 99754 should be the same. + test 12 success: poll test1_f62 94865 and subscribe q_f71 99935 run together. Should have equal results. + test 13 success: post test2_f61 99731 and subscribe r_ftp_f70 99939 run together. Should be about the same. + test 14 success: posts test2_f61 99731 and shim_f63 110795 Should be the same. + test 15 success: cpump both pelles (c shovel) should receive about the same number of messages (160737) (160735) + test 16 success: cdnld_f21 subscribe downloaded (50113) the same number of files that was published by both van_14 and van_15 (50221) + test 17 success: veille_f34 should post twice as many files (100205) as subscribe cdnld_f21 downloaded (50113) + test 18 success: veille_f34 should post twice as many files (100205) as subscribe cfile_f44 downloaded (49985) + test 19 success: Overall 18 of 18 passed (sample size: 100008) ! + + blacklab% + +This test was fired up at the end of the day, as it takes several hours, and results examined the next morning. + +High volume sample +~~~~~~~~~~~~~~~~~~ + +Trying the flow test with higher volume of notification messages (ie. 100 000) is one step closer to the goal of having a flow test running continously. This is motivated by our testing purposes. + +Limitation +++++++++++ + +Ubuntu have a limitation that tops inotify watches and that we encountered in `#204 `_ . We can overcome this by setting the related sysctl variable. First, check what is the limit of your system:: + + $ sysctl fs.inotify.max_user_watches + fs.inotify.max_user_watches = 8196 + +If the limit is too low (ie. 8196), change it to a more appropriate level for the flow test:: + + $ sudo sysctl fs.inotify.max_user_watches=524288 + +To make this change permanent add this line to ``/etc/sysctl.conf``:: + + fs.inotify.max_user_watches=524288 + +Then excute ``sysctl -p`` and the system should now support high volume of inotify events. + +Flow Test Stuck ++++++++++++++++ + +Sometimes flow tests (especially for large numbers) get stuck because of problems with the data stream (where multiple files get the same name) and so earlier versions remove later versions and then retries will always fail. Eventually, we will succeed in cleaning up the dd.weather.gc.ca stream, but for now sometimes a flow_check gets stuck 'Retrying.' The test has run all the notification messages required, and is at a phase of emptying out retries, but just keeps retrying forever with a variable number of items that never drops to zero. + +To recover from this state without discarding the results of a long test, do:: + + ^C to interrupt the flow_check.sh 100000 + blacklab% sr3 stop + blacklab% cd ~/.cache/sarra + blacklab% ls */*/*retry* + shovel/pclean_f90/sr_shovel_pclean_f90_0001.retry shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry subscribe/t_f30/sr_subscribe_t_f30_0002.retry.new + shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry.state + shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry.state subscribe/q_f71/sr_subscribe_q_f71_0004.retry.new + blacklab% rm */*/*retry* + blacklab% sr3 start + blacklab% + blacklab% ./flow_check.sh 100000 + Sufficient! + stopping shovels and waiting... + 2018-04-07 10:50:16,167 [INFO] sr_shovel t_dd2_f00 0001 stopped + 2018-04-07 10:50:16,177 [INFO] sr_shovel t_dd1_f00 0001 stopped + 2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored. + 2018-04-07 14:50:16,235 [INFO] info: report option not + implemented, ignored. + 2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored. + 2018-04-07 14:50:16,235 [INFO] info: report option not + implemented, ignored. + running instance for config pelle_dd1_f04 (pid 12435) stopped. + running instance for config pelle_dd2_f05 (pid 12428) stopped. + maximum of the shovels is: 100075 + + + blacklab% ./flow_check.sh + + | dd.weather routing | + test 1 success: sr_shovel (100075) t_dd1 should have the same number + of items as t_dd2 (100068) + test 2 success: sr_winnow (200143) should have the sum of the number + of items of shovels (200143) + test 3 success: sr_sarra (98075) should have the same number of items + as winnows'post (100077) + test 4 success: sr_subscribe (98068) should have the same number of + items as sarra (98075) + | watch routing | + test 5 success: watch (397354) should be 4 times subscribe t_f30 (98068) + test 6 success: sr_sender (392737) should have about the same number + of items as watch (397354) + test 7 success: sr_subscribe u_sftp_f60 (361172) should have the same + number of items as sr_sender (392737) + test 8 success: sr_subscribe cp_f61 (361172) should have the same + number of items as sr_sender (392737) + | poll routing | + test 9 success: sr_poll test1_f62 (195408) should have half the same + number of items of sr_sender(196368) + test 10 success: sr_subscribe q_f71 (195406) should have about the + same number of items as sr_poll test1_f62(195408) + | flow_post routing | + test 11 success: sr3_post test2_f61 (193541) should have half the same + number of items of sr_sender(196368) + test 12 success: sr_subscribe ftp_f70 (193541) should have about the + same number of items as sr3_post test2_f61(193541) + test 13 success: sr3_post test2_f61 (193541) should have about the same + number of items as shim_f63 195055 + | py infos routing | + test 14 success: sr_shovel pclean_f90 (97019) should have the same + number of watched items winnows'post (100077) + test 15 success: sr_shovel pclean_f92 (94537}) should have the same + number of removed items winnows'post (100077) + test 16 success: 0 messages received that we don't know what happenned. + test 17 success: count of truncated headers (98075) and subscribed + messages (98075) should have about the same number of items + | C routing | + test 18 success: cpump both pelles (c shovel) should receive about the + same number of messages (161365) (161365) + test 19 success: cdnld_f21 subscribe downloaded (47950) the same + number of files that was published by both van_14 and van_15 (47950) + test 20 success: veille_f34 should post twice as many files (95846) as + subscribe cdnld_f21 downloaded (47950) + test 21 success: veille_f34 should post twice as many files (95846) as + subscribe cfile_f44 downloaded (47896) + test 22 success: Overall 21 of 21 passed (sample size: 100077) ! + + NB retries for sr_subscribe t_f30 0 + NB retries for sr_sender 36 + + +So, in this case, the results are still good in spite of not quite being +able to terminate. If there was a significant problem, the cumulation +would indicate it. + +Flow tests with MQTT +~~~~~~~~~~~~~~~~~~~~ + +Flow tests can be run where certain components use the MQTT protocol, instead of AMQP. + +FIXME: steps missing, more clarity required. + +* MQTT broker is installed +* the bunnymaster tsource, tfeed, tsub users defined and given passwords (broker dependent.) +* for each user: an mqtt://user:pw@brokerhost url's line is added to ~/.config/sr3/credentials.conf +* edit the variable MQP in ~/.config/sr3/default.conf, MQP is used by the flow tests. + +Most components will use MQTT instead of amqp and can be run normally. + +Commits to the Development Branch +--------------------------------- + +Aside from typos, language fixups in the documentation, and incrementing +the version, developers are not expected to commit to stable. All work +happens on development branches, and all testing is expected to pass before +one considers affecting stable. Once the branch development is complete, +or a unit of work-in-progress is felt to be worth merging to stable, one +must summarize the changes from the branch for the debian change log, +request on github. + +:: + + git checkout issueXXX # v02_issueXXX for v2 work., github suggested branch names are fine also. + vi CHANGES.rst # summarize the changes in Restructured Text + dch # copy/paste from CHANGES.rst, inserting one leading space. + vi doc/UPGRADING.rst # rarely, if code has user impact. + vi doc/fr/UPGRADING.rst # bon... ceci est visible aux usagers, donc... + git commit -a + git push + # issue a pull request on github.com. + +A Second developer will review the pull request and the reviewer will decide on whether +merging is appropriate. The developer is expected to examine each commit, and +understand it to some degree. + +If the pull-request has one of the following (substantial changes, new functionality, modifications to critical code structure) , it is recommended to have a Third developer also review the pull request. The expectation from this developer are the same as from the previous. + +The github Actions looks at pull requests and will flow tests on them. +If the tests pass, then that is good qualitative indicator, however the tests are a bit +fragile at the moment, so if they fail, it would be ideal for the reviewer to run +the tests in their own development environment. If it passes in the local developer +environment one can approve a merge in spite of Github Actions' complaints. + + +Key Branches +------------ + +There is a long running discussion about `Which Version is stable `_ +The current set up is that there are four principal branches: + +* stable branch is the release version of sr3, merging from pre-release. used to build sr3 packages in the + `MetPX `_ repository. + +* development ... The `version 3 `_ work in progress branch is a next version of sarracenia. + the development branch is used to build sr3 packages for the `Daily `_ + and `Pre-Release `_ repositories on launchpad.net. + +* stable_py36 and pre-relrease-36 are branched from stable and pre_release respectively to adjust for building + packages on older operating systems that have older versions of python (and no support for hatchling.) + +* issue branches to be merged to development, it should be start with issueXXX or suggested branch names from github are ok also. + +* sometimes, multiple branches are needed for a single issue, say for variations of a fix, eg. issueXXX_2_do_it_this_way . + +* v2_dev ... the integration branch for v2 maintenance used prior to promotion to v2_stable. + +* v2_stable ... generally this branch gets code via merges from v2_dev, after the pre-release has been tested on a + as many systems as possible. used to build packages on the stable: `MetPX `_ + + +Repositories +------------ + +For Ubuntu operating systems, the launchpad.net site is the best way to provide packages that are fully integrated +( built against current patch levels of all dependencies (software components that Sarracenia relies +on to provide full functionality.)) Ideally, when running a server, a one should use one of the repositories, +and allow automated patching to upgrade them as needed. + +Repositories: + +* Daily https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-daily (living on the edge... ) + automated daily build of sr3 packages happens from *development* branch. + +* Pre-Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-pre-release (for newest features.) + from *development* branch. Developers manually trigger builds here when it seems appropriate (testing out + code that is ready for release.) + +* Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx (for maximum stability) + from *v2_stable* branch. After testing in systems subscribed to pre-releases, Developers + merge from v2_dev branch into v2_stable one, and manually trigger a build. + +for more discussion see `Which Version is stable `_ + + + +Local Python +~~~~~~~~~~~~ + +Working with a non-packaged version: + +notes:: + + pip install -e . + + +Windows +~~~~~~~ + +Install winpython from github.io version 3.5 or higher. Then use pip to install from PyPI. + + + +Conventions +----------- + +Below are some coding practices that are meant to guide developers when contributing to sarracenia. +They are not hard and fast rules, just guidance. + + +When to Report +~~~~~~~~~~~~~~ + +sr_report(7) notification messages should be emitted to indicate final disposition of the data itself, not +any notifications or report messages (don't report report messages, it becomes an infinite loop!) +For debugging and other information, the local log file is used. For example, sr_shovel does +not emit any sr_report(7) messages, because no data is transferred, only messages. + + +Adding a New Dependency +----------------------- + +Dependency Management is a complicated topic, because python has many different installation methods into disparate environments, and Sarracenia is multi-platform. Standard python practice for dependencies is to make +them *required* by listing them in requirements.txt or setup.py, and require all users to install them. +In most python applications, if a dependency is missing, it just crashes with a import failure message +of some kind. + +In Sr3, we have found that there are many different environments being deployed into where satisfying +dependencies can be more trouble than they are worth, so each of the dependencies in setup.py are also +dealt with in sarracenia/featuredetection, and the feature detection code allows the application to +keep working, just without the functionality provided by the missing module. This is called *degradation* +or *degraded mode*. The idea being to help the user do as much as they can, in the environment they have, +while telling them what is missing, and what would ideally be added. + +for a full discussion see: + +`Managing Dependencies (Discussion) ` + +Short version: + +In addition to requirements.dev/setup.py, if you need to add a new library that isn't part of +*batteries included*, typically provided by a separate os or pip package, then you want to +provide for the package to still work in the event that the package is not available (albeit +without the function you are adding) and to add support for explaining what's missing using +the sarracenia/featuredetection.py module. + +In that module is a *features* data structure, where you add an entry explaining the import +needed, and the functionality it brings to Sr3. You also add if feature['x']['present'] guards +in the code where you are using the feature, in order to allow the code to degrade elegantly. + +If the dependency is added in a plugin, then there is also a method for that described here: + +`Plugin Developer Guide <../Explanation/SarraPluginDev.html#callbacks-that-need-python-modules>` + diff --git a/_sources/Contribution/Documentation.rst.txt b/_sources/Contribution/Documentation.rst.txt new file mode 100644 index 000000000..3dd6160ce --- /dev/null +++ b/_sources/Contribution/Documentation.rst.txt @@ -0,0 +1,214 @@ +======================= +Documentation Standards +======================= + + +Folder Structure +~~~~~~~~~~~~~~~~~ + +Before starting work with documentation read `the entire divo documentation article +`_ (and the links on the left hand sidebar). +It'll take no longer than 30 minutes and you'll gain a complete understanding of the +expected structure, style and content of the documentation here. + +.. Backup divo link in case site dies : https://github.com/divio/diataxis-documentation-framework/ +.. image:: https://documentation.divio.com/_images/overview.png + :target: https://documentation.divio.com/ + +We look to the unix directive in our documentation, each documentation file does one thing +and does it well with respect to the 4 quadrants. + + +.. note:: TODO:Add example links to each of the below sections: + +Processing +---------- + +The entire documentation is under the docs/source tree. It is processed using sphinx, invoked +using the Makefile in docs/. One can install sphinx locally, and run make to build locally +and debug. The result is produced in docs/build/html:: + + pip install -f requirements-dev.txt + cd docs + make html + +then point a browser to docs/build/html. + +There is a github Actions jobs that does this on each push to appropriate branchs to update +the main documentation. the main url for the resulting web-site is: + + https://metpx.github.io/sarracenia/ + +The publication process is automated by two github CI/CD actions: + + * run sphinx and commit the results to a gh-pages branch: https://github.com/MetPX/sarracenia/actions/workflows/docs.yml + * update the web-site from the branch: https://github.com/MetPX/sarracenia/actions/workflows/pages/pages-build-deployment + +It is very good to have a look at the sphinx error messages generated by the first job. There are typically +hundreds of small problems to fix (links that are not quite right, broken table formatting, etc...) + + + +Tutorials +--------- + +- Learning-oriented, specifically learning how rather than learning *that*. +- Allow the user to learn by doing to get them started, be sure your tutorial works and users can see results immediately. +- Tutorials must be reliably repeatable focused on concrete steps (not abstract concepts) with the minimum necessary explanation. + +Many of the Tutorials are built using jupyter notebooks. See docs/source/Tutorials/README.md for +how to work with them. + + +How2Guides +---------- + +- Problem and goal oriented: "**I want to... How do I...**" Differing from tutorials in that tutorials are for complete beginners, how to guides assume some knowledge and understanding with a basic setup and tools. +- Provide a series of steps focused on the results of some particular problem. +- Don't explain concepts, if they are important, they can be linked to in `../Explanation/` +- There are multiple ways to skin a cat, remain flexible in your guide so users can see *how* things are done. +- **NAME GUIDES WELL** enough to tell the user *exactly* what they do at a glance. + +Explanation +----------- + +- Understanding-oriented: can be equally considered discussions. Much more relaxed version of documentation where concepts are explored from a higher level or different perspectives. +- Provide context, discuss alternatives and opinions while providing technical reference (for other sections). + +Reference +--------- + +- Dictionary style. +- Information oriented: code-determined descriptions of functionality. +- Strictly for the man pages and direct reference of the various programs, protocols, and functions. + +Contribution +------------ + +- Information critical to the enhancement and progression of the Sarracenia project, ie: for those that are looking to develop Sarracenia. +- Style guide(s) +- Template(s) + +Process +~~~~~~~ + +The development process is to write up what one intends to do or have done into +a `reStructuredText `_ +file in `/docs/Explanation/Design/`. Ideally, the discussion of information there acts +as a starting point which can be edited into documentation for the feature(s) as they +are implemented. Each new component sr\_, needs to have relevant man pages +implemented. The how to guides and tutorials should also be revised to reflect additions +or changes of the new component. + +.. error:: Need Peter to help ID worthy information in doc/design to pull over to + /docs/Explanation/Design/ + + +Style Guide +~~~~~~~~~~~ + +Command line execution shall be written in the style of:: + + An initial comment describing the following steps or processes:: + + $ command 1 + relevant output + $command 2 + . + . + relevant output + newline relevant output + +Important notes: + +- Initial comment ends with `::` followed by an empty newline +- Thereafter lies the (two space) indented code block +- Commands syntax: '`$ `' + + - Alternatively indicate root level commands with '`# `' +- Command output is (two space) indented from leading command. + + - Irrelevant lines of output may be substituted for dots or outright omitted. + +pick and stick to a default header hierarchy (ie : = > ~ > - > ... for title > h1 > h2 > h3... etc) + +Code Style +---------- + +We generally follow `PEP 8 `_ standards for code formatting, and use `YAPF `_ to automatically re-format code. One exception to PEP 8 is that we use a 119 character line length. + +For docstrings in code, we are following the Google Style Guide. These docstrings will be parsed into formatted documentation by Sphinx. + +Detailed examples can be found in the `Napoleon Sphinx plugin's docs `_ and the `Google Python Style Guide `_. + +Selected examples from ``credentials.py``: + +.. code-block:: python + + class Credential: + """An object that holds information about a credential, read from a + credential file, which has one credential per line, format:: + url option1=value1, option2=value2 + + Examples:: + sftp://alice@herhost/ ssh_keyfile=/home/myself/mykeys/.ssh.id_dsa + ftp://georges:Gpass@hishost/ passive = True, binary = True + + `Format Documentation. `_ + + Attributes: + url (urllib.parse.ParseResult): object with URL, password, etc. + ssh_keyfile (str): path to SSH key file for SFTP + passive (bool): use passive FTP mode, defaults to ``True`` + binary (bool): use binary FTP mode, defaults to ``True`` + tls (bool): use FTPS with TLS, defaults to ``False`` + prot_p (bool): use a secure data connection for TLS + bearer_token (str): bearer token for HTTP authentication + login_method (str): force a specific login method for AMQP (PLAIN, + AMQPLAIN, EXTERNAL or GSSAPI) + """ + + def __init__(self, urlstr=None): + """Create a Credential object. + + Args: + urlstr (str): a URL in string form to be parsed. + """ + + +.. code-block:: python + + def isValid(self, url, details=None): + """Validates a URL and Credential object. Checks for empty passwords, schemes, etc. + + Args: + url (urllib.parse.ParseResult): ParseResult object for a URL. + details (sarracenia.credentials.Credential): sarra Credential object containing additional details about + the URL. + + Returns: + bool: ``True`` if a URL is valid, ``False`` if not. + """ + +Why rST? +-------- + +`reStructuredText`_ was chosen primarily as it supports the auto-creation of a table of contents with the '``.. contents::``' directive. +Like many other markup languages, it also supports inline styling, tables, headings and literal blocks. + +In Jupyter Notebooks, unfortunately, only Markdown is supported, elsewhere RST is great. + + +Localization +~~~~~~~~~~~~ + +This project is intended to be translated in French and English at a minimum as it's +used across the Government of Canada which has these two official languages. + +The French documentation has the same file structure and names as the English, but +is placed under the fr/ sub-directory. It's easiest if the documentation is produced +in both languages at once. At the very least use an auto translation tool (such as +`deepl `_) to provide a starting point. Same procedure in +reverse for Francophones. + + diff --git a/_sources/Contribution/Evolution.rst.txt b/_sources/Contribution/Evolution.rst.txt new file mode 100644 index 000000000..b3c73daf4 --- /dev/null +++ b/_sources/Contribution/Evolution.rst.txt @@ -0,0 +1,24 @@ + +Design Changes since Original (2015) +==================================== + +as of 2022/03, the design has not changed much, but sr3's implementation +is totally different from v2. Design changes: + +* explicit core application support for reports were ripped out + as they were never used, can easily be re-inserted as callbacks + and conventions. +* nobody used segmented files, and they were very complicated, + but everyone finds them fantastic in theory. Need to re-implement + post sr3 re-factor. +* mirroring was a use case we had to address, had to add metadata, + and evolve a bit. +* the cluster routing concepts have been removed (cluster_from, cluster_to, etc...) + it got in the analysts' way more than it helped. Dead easy to + implement using flow callbacks, if we ever want them back. +* the `Flow algorithm <../Explanation/Concepts.html#the-flow-algorithm>`_ + emerged as a unifying concept for all the different components + originally envisaged. In early work on v2, we didn't know if + all the components would work the same, so they were written + separately from scratch. A lot of copy/paste code among + entry points. diff --git a/_sources/Contribution/Philosophy/AboutTime.ipynb.txt b/_sources/Contribution/Philosophy/AboutTime.ipynb.txt new file mode 100644 index 000000000..45eaab6ea --- /dev/null +++ b/_sources/Contribution/Philosophy/AboutTime.ipynb.txt @@ -0,0 +1,4250 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc172731", + "metadata": {}, + "source": [ + "# STATUS: WIP\n", + "work in progress. Not worth reading yet. more notes than anything else." + ] + }, + { + "cell_type": "markdown", + "id": "6395a549", + "metadata": {}, + "source": [ + "# It´s About Time\n", + "\n", + "Say you are a weather forecaster, charged with producing a prediction of the weather, perhaps for the public, or perhaps for a vertical domain, such as aviation, or transport. In normal situations, the forecaster will be asked to produce a forecast at a specific time of day, to help her clients plan their business.\n", + "\n", + "How does a forecaster build a forecast? \n", + "\n", + "Assuming the forecaster is in the americas, one could use a browser:\n", + "\n", + "* look at a picture of the sky from a GOES satellite image (visible spectrum), to see where it is cloudy at the moment.\n", + "* look at a different web site for closer in shots from polar orbiting satellites (e.g. HRDPS) that cover a smaller area.\n", + "* look at a United States Geological Survey, or Water Survey of Canada web site to see the current stream flows.\n", + "* look at a some weather web sites, to see the current observations from surface stations.\n", + "* look at some Aviation (FAA or NAVCANADA web sites) to find weather observations at airports (which tend to have a tightly controlled quality, and so are relatively reliable.)\n", + "* Find RADAR imagery on the net for the areas of interest (it might be that two or three RADAR stations have coverage of the area one is looking at.\n", + "\n", + "If you want to look at all that information together... you build a really large spreadsheet where each cell describes one part of the sky, and for each cell, you would store something like temperature, barometric pressure, and wind (direction and speed.)\n" + ] + }, + { + "cell_type": "markdown", + "id": "2502a307", + "metadata": {}, + "source": [ + "Now to get movement, you need to be looking at a trend, not just a snapshot. \n", + "From one picture, you don´t know the direction that anything is moving. \n", + "So you really need at least two snapshots to get speed.\n", + "\n", + "Cloud is a one location, p0 at an initial time, and and has moved to p1 at time t1." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "703ed5c2", + "metadata": {}, + "outputs": [], + "source": [ + "#illustration with two points." + ] + }, + { + "cell_type": "markdown", + "id": "8d6d3a94", + "metadata": {}, + "source": [ + "\n", + "Now to predict the future, with just two points in time, you have to assume stuff is always moving at the same speed. If you think air masses are accellerating or slowing down, you need another picture. With three of them you can compare the speeds between the first two pictures, and the last two pictures, and see a change in speed.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c1a4040", + "metadata": {}, + "outputs": [], + "source": [ + "#illustration with three points." + ] + }, + { + "cell_type": "markdown", + "id": "e5006a41", + "metadata": {}, + "source": [ + "Now with three points, you have constant accelleration... but if the acceleration itself is varying, then you need more again." + ] + }, + { + "cell_type": "markdown", + "id": "eb70daaa", + "metadata": {}, + "source": [ + "The more points in time you have, the better you can understand the movement to establish the wind speed and direction at a given point in time, and how it will change in future.\n", + "\n", + "So once you have good information about the clouds that exist, and the rate they are moving at, and whether they are speeding up, or building up, or dissipating... then a forecaster is trained in physics, and applies the rules of physics to understand how clouds will move in the future. Essentially it uses the spreadsheet to calculate what the sky should look like one small step later in time, and then another step, and another, until you have arrived as far in time as needed for the forecast product.\n", + "\n", + "At this point, the result is a spreadsheet. The forecaster then can use the spreadsheet and write descriptions of weather for their client, or have an automated process do that, or produce simulated ¨satellite\" imagery to show where the clouds till in the future.\n", + "\n", + "Restating things:\n", + "\n", + "* step 1: Acquisiion: gather a time series of data for kinds of data.\n", + "* step 2: Assimilation: put all the different data into a pile of spreadsheets.\n", + "* step 3: Numerical Model: hit calculate on the spreadsheet for the number of timestesp you need.\n", + "* step 4: Services: translating the spreadsheets back into things people can understand (maps, text, and simulated future images.)\n", + "\n", + "\n", + "Looking at all these steps, it is obvious that they are extremely tedious for a human to do, and things that a computer, in principle should be great at. Rather than having a human look at web sites, and extract data,\n", + "have a computer do it, and present the forecaster with a best first guess product.\n", + "\n", + "## Let the Computer Do It\n", + "\n", + "Instead of a human browsing dozens of web sites and mapping stuff into a spreadsheet manually, and then hitting calculate, and then mapping back the spreadsheet back to something his clients will understand, the forecaster hits a button, and has the computer does all the tedious work. So when the forecaster pushes the button, the computer:\n", + "\n", + "* scans the entire world for a number of hours of satellite, RADAR, and point observations from weather stations, airports, ships, and planes. looking for information for the last hour or two to get a good trend.\n", + "* stuffs that into a spreadsheet.\n", + "* runs the spreadsheet.\n", + "* extracts the interesting numbers or pictures from the spreadsheet.\n", + "\n", + "Great, how long will that take, and how good will the result be?\n", + "\n", + "Well the quality of the result will vary with the quality of the input. To make a spreadsheet, the forecaster decides how big an volume of space to cover with each cell. The bigger the volume covered by each cell, the more you are taking different data points and averaging them to get one value for the the cell, so the \"fuzzier\" the pictures that result. \n", + "\n", + "How big are the spreadsheets? 30 years ago, the \"high resolution\" spreadsheet covering North America had each cell was 150 km. on a side, At that time, computers were not big enough to cover the whole world. Today, the high resolution models are around 10km on a side (100 sq. km.) As the area of the earth is around 500 million sq. km. that means that the grid today should be about 5 million cells. per level, models typically have 25 levels representing different heights of air in the atmosphere, so that means 125 million cells to calculate.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "9881d7f4", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1753: UserWarning:\n", + "\n", + "Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.\n", + "\n", + "/home/peter/.local/lib/python3.10/site-packages/plotly/express/_core.py:1754: UserWarning:\n", + "\n", + "Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format.\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "12:05:00", + "12:45:00", + "13:05:00", + "13:45:00" + ], + "hovertemplate": "Start=%{base}
Finish=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 2400000, + 1200000, + 2400000, + 1500000 + ], + "xaxis": "x", + "y": [ + "Acquisition", + "Assimilation", + "Model", + "Services" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Acquisition\", Start='12:05:00', Finish='12:45:00'),\n", + " dict(Task=\"Assimilation\", Start='12:45:00', Finish='13:05:00'),\n", + " dict(Task=\"Model\", Start='13:05:00', Finish='13:45:00'),\n", + " dict(Task=\"Services\", Start='13:45:00', Finish='14:10:00')\n", + " \n", + "])\n", + "\n", + "fig = px.timeline(df, x_start=\"Start\", x_end=\"Finish\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "3fdd3540", + "metadata": {}, + "source": [ + "Each cell has information about different phenomena, called variables, say 25 of them. so to get a full picture, one needs 125 million x 25 variables x 8 bytes per variable = 6.2 billion variables in about 50 gbytes of memory. To calculate the what the values of those variables are at the next unit in time involve many calculations using that data, and the prediction's accuracy involves some tradeoffs:\n", + "\n", + "The simpler the model, the simpler the math, the less calculation time to get a result. The more complicated the model & math, the better the result, but the longer it takes to do." + ] + }, + { + "cell_type": "markdown", + "id": "64fb191a", + "metadata": {}, + "source": [ + "* https://en.wikipedia.org/wiki/History_of_numerical_weather_prediction#cite_note-RFE-43 note on Weather model from 1989\n", + "\n", + "* \n", + "https://devops.com/dont-build-microservices-pursue-loose-coupling/\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "958fed92", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import matplotlib.dates as mdates\n", + "from datetime import datetime\n", + "\n", + "try:\n", + " # Try to fetch a list of Matplotlib releases and their dates\n", + " # from https://api.github.com/repos/matplotlib/matplotlib/releases\n", + " import urllib.request\n", + " import json\n", + "\n", + " url = 'https://api.github.com/repos/matplotlib/matplotlib/releases'\n", + " url += '?per_page=100'\n", + " data = json.loads(urllib.request.urlopen(url, timeout=1).read().decode())\n", + "\n", + " dates = []\n", + " names = []\n", + " for item in data:\n", + " if 'rc' not in item['tag_name'] and 'b' not in item['tag_name']:\n", + " dates.append(item['published_at'].split(\"T\")[0])\n", + " names.append(item['tag_name'])\n", + " # Convert date strings (e.g. 2014-10-18) to datetime\n", + " dates = [datetime.strptime(d, \"%Y-%m-%d\") for d in dates]\n", + "\n", + "except Exception:\n", + " # In case the above fails, e.g. because of missing internet connection\n", + " # use the following lists as fallback.\n", + " names = ['v2.2.4', 'v3.0.3', 'v3.0.2', 'v3.0.1', 'v3.0.0', 'v2.2.3',\n", + " 'v2.2.2', 'v2.2.1', 'v2.2.0', 'v2.1.2', 'v2.1.1', 'v2.1.0',\n", + " 'v2.0.2', 'v2.0.1', 'v2.0.0', 'v1.5.3', 'v1.5.2', 'v1.5.1',\n", + " 'v1.5.0', 'v1.4.3', 'v1.4.2', 'v1.4.1', 'v1.4.0']\n", + "\n", + " dates = ['2019-02-26', '2019-02-26', '2018-11-10', '2018-11-10',\n", + " '2018-09-18', '2018-08-10', '2018-03-17', '2018-03-16',\n", + " '2018-03-06', '2018-01-18', '2017-12-10', '2017-10-07',\n", + " '2017-05-10', '2017-05-02', '2017-01-17', '2016-09-09',\n", + " '2016-07-03', '2016-01-10', '2015-10-29', '2015-02-16',\n", + " '2014-10-26', '2014-10-18', '2014-08-26']\n", + "\n", + " # Convert date strings (e.g. 2014-10-18) to datetime\n", + " dates = [datetime.strptime(d, \"%Y-%m-%d\") for d in dates]\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "69ddd6c2", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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qC+fAgMDdW7jcePGadYpnU/llbt8+bKI6M6nnJwciYyMFFdXV1GpVDJo0CC5evWqNYetU+ltacnY6NtnSu6fpWOTnJwsAwcOFE9PT7G3txdvb28ZM2aMnDt3ztqh0CjKypLTPr7iYVerTByWLVsm7dq1EycnJ3FxcZHOnTvLypUrtb7O3NZypDR9OWJObGbPnGlyjohU3+Nxyf0pPzPTYnHav3+/BAQESL169aR27drSqlUref3117UeN9QVp9Kq42TPkvnE87th5ca7TRtRKRRSR6mUzh07GnVsy8jIkPHjx0v9+vXF1dVVhgwZonWcqsxjfE2/NqkqCpH//5wAVRvF2dn4s8s/t919T8RD6eRUxT2iBw1z0HZwW5bFmFQeW4u1rY3HkAdtvLaupm/Pmt7/qsI/vUBERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHpGJirOz8UfrNvijdRsUZ2dXdXeohmM+lY/xYQzKw9gYh3EyDuNUuRjvysHJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42atEqampGDNmDHx9faFUKjF9+nSTyqelpcHb2xsKhQJ3794td92IiAi0bNkSKpUKHh4eCAsLw7lz58zvPJGJtm3bhqCgIHh4eMDFxQUBAQHYtWuXwXJXr15FaGgonJ2d4e7ujqlTpyI/P7/cMqtXr0afPn3g4uJi1P5RnWz77js8d+0qeiZdQP1GjYyK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u3btqnM8uvql65sFTb0eqOj2tOa1iDVzzlp1m7t9K/vbOKsinvrWUX8b58KFCy0WL0P90bfPlPw2Tmtvn+p2TK0ITvasSNffEGnRooXBizhdf9vG09PTaslc+m+IGOpjTWXO9tBZRzWPV0XHWVVjtMT2MaaNyty/jOmPubE2Jl6m1K+rvpJ/v6x27dpV83f2DPTf1LzR9TfcLD02S7ZR0W1Ylccnc/O7vPgZylNLjFdfHHX9XTF1e4Zib04sLLU9zW1b198kLX2stGbOWetcZM72rSxVHU9d7Tdq1KjM39nTxMuK1xq6+lL6uGDt7VPdjqnm4p9esLKCe/ewsV07/F1YhC5R/0VgUJDBr2ktKipC7K5d2Pr00wAUGLruC/QNDrbqV/4eOnQIqampaNy4MXr16lUzvkrWDOZsj9JqQrwqOs6qGqMlto8hlb1/GdMfc2NtTLxMqb9kfe0/XYb9kyYhubAQ/m++icjp06365xb0Mab/puZN7t27mOvjg2sFBVYbmyXbMHcbWmsfMoW5+V1e/EqOsdPna6BwdMStW7cseqzSF8fyxmMo9ubEwlLb09y217dth9+ys+D6/PPoGxSEPn36VHj/s3a/jWHO9q0sVR1PXe0D0FnO2tcapev/V9++iIuLq9TtU92OqebgZM/KirOz8WcXfwCA74l4KJ2crFqOyvegxLWmjrOy+l1T41OapcdRsr5WvxzGhZ7/sljd1mRqHCpj+1dVjj0IuV1dt581+lWV29PYtmtizlXnPld130xp39p9repYVJc+VBS/jZOIiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2aBaVd0BosqkdHJCm3N/VHU3qIoxDwxTqlSMUQUwx6znQYrtgzTWysS4WgbjWDPwzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72yGpSU1MxZswY+Pr6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7dlQCu7d0zvW/fv3S0BAgNSrV09q164tze3tZYKrq6SV+LMxNTGPREyf7J328ZXGjRpZJIce5GuB/MxM8bCrJQDkwpkzmuXmxFFEJCMjQ8aPHy/169cXV1dXGTJkiFZMSsdx308/VZtzaXU4J1WHPlSUQkTUTxyQFRRnZ+PPLv/cgvc9EQ+lk5NVyxEBNTd/amq/q4ql41VT429qv2vqOI1hK2Or6nGY035V99nSjB2PrY27qlV1PKu6/erWl+rQh4riZ/aIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDapV1R2wdUonJ7Q590dVd4MeMMy7BwO38z8Yh/9hLCyDcWQMqgrj/j+MhWXwzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBNjXZS01NxZgxY+Dr6wulUonp06ebVD4tLQ3e3t5QKBS4e/duuevm5eVhypQpcHd3h7OzMwYPHozr16+b33mqsczNO4VCUeZn1apVetdPT0/HlClT4OvrCycnJzRr1gxTp05FRkaGhUZC5dm2bRuCgoLg4eEBFxcXBAQEYNeuXQbLTZs2Df7+/nB0dESnTp2Mamv16tXo06cPXFxcjDoeVabDhw+jZ8+ecHNzg0qlQuvWrfHJJ58YLHf16lWEhobC2dkZ7u7umDp1KvLz88stU52Ps+bGwdbyoaLMiePJkycxevRoNG3aFCqVCm3atMHSpUvLLVPdj5/m5pMar1/MY27cbe38XTIOzm5u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TchvQxQiIkbeBKRKVJydjT+7/HN73fdEPJRO+h9nIbIVzPuq9aDE/0EZZ01WE7dRTeyzJTyo47ZV3J7abCEeNe7bOImIiIiIiMgwTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbxMkeERERERGRDeJkj4iIiIiIyAZxskdERERERGSDONkjIiIiIiKyQZzsERERERER2SBO9oiIiIiIiGwQJ3tEREREREQ2iJM9IiIiIiIiG8TJHhERERERkQ3iZI+IiIiIiMgGcbJHRERERERkgzjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72iIiIiIiIbBAne0RERERERDaIkz0iIiIiIiIbpBARqepOEBERERERkWXxzh4REREREZEN4mSPiIiIiIjIBnGyR0REREREZIM42SMiIiIiIrJBnOwRERERERHZIE72AKSmpmLMmDHw9fWFUqnE9OnTTSqflpYGb29vKBQK3L17V+966enpmDJlCnx9feHk5IRmzZph6tSpyMjIqNgAiMywbds2BAUFwcPDAy4uLggICMCuXbsMlrt69SpCQ0Ph7OwMd3d3TJ06Ffn5+eWWycvLw5QpU+Du7g5nZ2cMHjwY169ft9RQaiRz4n/y5EmMHj0aTZs2hUqlQps2bbB06VKDbVVl/A8fPoyePXvCzc0NKpUKrVu3xieffGKwnKl5xuOreSozD/v06QOFQqH1M2rUKLP6bU5epaWlITg4GF5eXnB0dETTpk0RGRmJzMzMcsutXr0affr0gYuLi8HzvLWZuz+p8XqleqnI9ly7di38/PxQu3ZtNGrUCJGRkeWuXxPOw+bEY+3atWWOK+qfW7du6SzzoOV3raruQHWQl5cHDw8PzJo1y6SDptpzzz0HPz8/JCcnl7teSkoKUlJSsGjRIrRt2xZXrlzBpEmTkJKSgq+//trc7hOZ5eDBgwgKCsLChQtRv359REdHIzQ0FEePHkXnzp11likqKkJI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8+Y6y1Sn87C19uv79+9j8+bNeO+994zqxwOV31U506xsu3fvFkdHR/H19S3zyElCQoIkJCSIv7+/jBkzRhISEiQxMVHz+rZt28TX11dv3bre8bt+/br4+vrK0aNHReSfdxC6d+8uHTp0kKSkJElNTdX8FBYWWnawRP+fvrzfuHGj1KpVS1asWKGVi3fv3tWsUzrvCwsLpX379tKvXz85ceKE7N27V7y9vSUyMlKzTum8FxGZNGmSeHt7y969e+XEiRPSt29f6dix4wOR95aMv/qRufDwcK0yt27d0qxTVfHXN87ly5fL9u3b5fz583L+/HmJiooSFxcXmTVrlt5xmpNnPL6Wr6rzMCkpSd555x05duyYXL58WX744Qdp3bq1dO7cudztY8m8+uGHHyQqKkpOnz6t6UO7du2kZ8+eevst8s9jnwkJCbJmzRoBIAcPHpSEhARJS0szOv6msuS4S+P1SuWz9PYMCwuTdu3ayS+//CKnT5+WQYMGSdu2bTWPIVb387A18vvzzz+X2rVrl7l7KcL8fqAme4WFhdK4cWMBIBcvXtR6DUCZn+bNm2tej46OlvLmxroOnpcvXxYAEhsbq7WOrp/Lly9bcKRE/6Mv7wMDA3Xm4rhx4zTr6Mr7K1euSEhIiKhUKnF1dZXIyEjNs/ciZfNeRCQnJ0ciIyPF1dVVVCqVDBo0SK5evWq1MVcnloz/nDlzDB6rqir++sa5bNkyadeunTg5OYmLi4t07txZVq5cqfXV25bIMx5fy1fVeXj16lXp3bu3uLq6ioODg7Rs2VKmTp1qcMJkybzav3+/BAQESL169aR27drSqlUref3118s9b5c33ujoaANRN5+l96eSeL1S+Sy9PTMyMmT8+PFSv359cXV1lSFDhmgd06v7edga+R0QECBjxozR2d6Dnt8Kkf//DAMRERERERHZjAfiTy8QERERERE9aDjZIyIiIiIiskGc7BEREREREdkgTvaIiIiIiIhsECd7RERERERENoiTPSIiIiIiIhvEyR4REREREZEN4mSPiIiIiIjIBnGyR0REREREZINqVXUHLE1EcO/evaruBhERERERkVXVrVsXCoVC7+s2N9m7d+8e6tWrV9XdICIiIiIisqqMjAy4uLjofV0hIlKJ/bE6S9zZy8zMRNOmTXHt2rVyg2dJld2mrbdXFW3aensPSpu2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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Choose some nice levels\n", + "levels = np.tile([-5, 5, -3, 3, -1, 1],\n", + " int(np.ceil(len(dates)/6)))[:len(dates)]\n", + "\n", + "# Create figure and plot a stem plot with the date\n", + "fig, ax = plt.subplots(figsize=(8.8, 4), layout=\"constrained\")\n", + "ax.set(title=\"Matplotlib release dates\")\n", + "\n", + "ax.vlines(dates, 0, levels, color=\"tab:red\") # The vertical stems.\n", + "ax.plot(dates, np.zeros_like(dates), \"-o\",\n", + " color=\"k\", markerfacecolor=\"w\") # Baseline and markers on it.\n", + "\n", + "# annotate lines\n", + "for d, l, r in zip(dates, levels, names):\n", + " ax.annotate(r, xy=(d, l),\n", + " xytext=(-3, np.sign(l)*3), textcoords=\"offset points\",\n", + " horizontalalignment=\"right\",\n", + " verticalalignment=\"bottom\" if l > 0 else \"top\")\n", + "\n", + "# format x-axis with 4-month intervals\n", + "ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4))\n", + "ax.xaxis.set_major_formatter(mdates.DateFormatter(\"%b %Y\"))\n", + "plt.setp(ax.get_xticklabels(), rotation=30, ha=\"right\")\n", + "\n", + "# remove y-axis and spines\n", + "ax.yaxis.set_visible(False)\n", + "ax.spines[[\"left\", \"top\", \"right\"]].set_visible(False)\n", + "\n", + "ax.margins(y=0.1)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d38bd681", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "import matplotlib\n", + "import matplotlib.pyplot as plt\n", + "import datetime as dt\n", + "\n", + "# from https://www.datacamp.com/tutorial/how-to-make-gantt-chart-in-python-matplotlib\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "6cc70c33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " task team start end completion_frac\n", + "0 A R&D 2022-10-20 2022-10-31 1.00\n", + "1 B Accounting 2022-10-24 2022-10-28 1.00\n", + "2 C Sales 2022-10-26 2022-10-31 1.00\n", + "3 D Sales 2022-10-31 2022-11-08 1.00\n", + "4 E IT 2022-11-03 2022-11-09 1.00\n", + "5 F R&D 2022-11-07 2022-11-18 0.95\n", + "6 G IT 2022-11-10 2022-11-17 0.70\n", + "7 H Sales 2022-11-14 2022-11-22 0.35\n", + "8 I Accounting 2022-11-18 2022-11-23 0.10\n", + "9 J Accounting 2022-11-23 2022-12-01 0.00\n", + "10 K Sales 2022-11-28 2022-12-05 0.00\n", + "11 L IT 2022-11-30 2022-12-05 0.00\n" + ] + } + ], + "source": [ + "df = pd.DataFrame({'task': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'],\n", + " 'team': ['R&D', 'Accounting', 'Sales', 'Sales', 'IT', 'R&D', 'IT', 'Sales', 'Accounting', 'Accounting', 'Sales', 'IT'],\n", + " 'start': pd.to_datetime(['20 Oct 2022', '24 Oct 2022', '26 Oct 2022', '31 Oct 2022', '3 Nov 2022', '7 Nov 2022', '10 Nov 2022', '14 Nov 2022', '18 Nov 2022', '23 Nov 2022', '28 Nov 2022', '30 Nov 2022']),\n", + " 'end': pd.to_datetime(['31 Oct 2022', '28 Oct 2022', '31 Oct 2022', '8 Nov 2022', '9 Nov 2022', '18 Nov 2022', '17 Nov 2022', '22 Nov 2022', '23 Nov 2022', '1 Dec 2022', '5 Dec 2022', '5 Dec 2022']),\n", + " 'completion_frac': [1, 1, 1, 1, 1, 0.95, 0.7, 0.35, 0.1, 0, 0, 0]})\n", + "print(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "2bea7be6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " task team start end completion_frac days_to_start \\\n", + "0 A R&D 2022-10-20 2022-10-31 1.00 0 \n", + "1 B Accounting 2022-10-24 2022-10-28 1.00 4 \n", + "2 C Sales 2022-10-26 2022-10-31 1.00 6 \n", + "3 D Sales 2022-10-31 2022-11-08 1.00 11 \n", + "4 E IT 2022-11-03 2022-11-09 1.00 14 \n", + "5 F R&D 2022-11-07 2022-11-18 0.95 18 \n", + "6 G IT 2022-11-10 2022-11-17 0.70 21 \n", + "7 H Sales 2022-11-14 2022-11-22 0.35 25 \n", + "8 I Accounting 2022-11-18 2022-11-23 0.10 29 \n", + "9 J Accounting 2022-11-23 2022-12-01 0.00 34 \n", + "10 K Sales 2022-11-28 2022-12-05 0.00 39 \n", + "11 L IT 2022-11-30 2022-12-05 0.00 41 \n", + "\n", + " days_to_end task_duration completion_days \n", + "0 11 12 12.00 \n", + "1 8 5 5.00 \n", + "2 11 6 6.00 \n", + "3 19 9 9.00 \n", + "4 20 7 7.00 \n", + "5 29 12 11.40 \n", + "6 28 8 5.60 \n", + "7 33 9 3.15 \n", + "8 34 6 0.60 \n", + "9 42 9 0.00 \n", + "10 46 8 0.00 \n", + "11 46 6 0.00 \n" + ] + } + ], + "source": [ + "df['days_to_start'] = (df['start'] - df['start'].min()).dt.days\n", + "df['days_to_end'] = (df['end'] - df['start'].min()).dt.days\n", + "df['task_duration'] = df['days_to_end'] - df['days_to_start'] + 1 # to include also the end date\n", + "df['completion_days'] = df['completion_frac'] * df['task_duration']\n", + "print(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "de5e70ba", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.barh(y=df['task'], width=df['task_duration'], left=df['days_to_start'])\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "bf7f4335", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "2009-01-01", + "2009-03-05", + "2009-02-20" + ], + "hovertemplate": "Start=%{base}
Finish=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 5011200000, + 3542400000, + 8553600000 + ], + "xaxis": "x", + "y": [ + "Job A", + "Job B", + "Job C" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import pandas as pd\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Job A\", Start='2009-01-01', Finish='2009-02-28'),\n", + " dict(Task=\"Job B\", Start='2009-03-05', Finish='2009-04-15'),\n", + " dict(Task=\"Job C\", Start='2009-02-20', Finish='2009-05-30')\n", + "])\n", + "\n", + "fig = px.timeline(df, x_start=\"Start\", x_end=\"Finish\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "663b0fcd", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " Task Start_time Duration End_time \\\n", + "0 Acquisition 2023-09-08 12:05:00 0 days 00:40:00 2023-09-08 12:45:00 \n", + "1 GOES images T-12 2023-09-08 12:05:00 0 days 00:05:00 2023-09-08 12:10:00 \n", + "2 GOES images T-11 2023-09-08 12:10:00 0 days 00:05:00 2023-09-08 12:15:00 \n", + "3 GOES images T-10 2023-09-08 12:15:00 0 days 00:05:00 2023-09-08 12:20:00 \n", + "4 GOES images T-09 2023-09-08 12:20:00 0 days 00:05:00 2023-09-08 12:25:00 \n", + "5 Assimilation 2023-09-08 12:45:00 0 days 00:30:00 2023-09-08 13:15:00 \n", + "6 Model 2023-09-08 13:15:00 0 days 00:45:00 2023-09-08 14:00:00 \n", + "\n", + " Start_with Start_after \n", + "0 NaN NaN \n", + "1 Acquisition NaN \n", + "2 NaN GOES images T-12 \n", + "3 NaN GOES images T-11 \n", + "4 NaN GOES images T-10 \n", + "5 NaN Acquisition \n", + "6 NaN Assimilation \n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/peter/.local/lib/python3.10/site-packages/_plotly_utils/basevalidators.py:105: FutureWarning:\n", + "\n", + "The behavior of DatetimeProperties.to_pydatetime is deprecated, in a future version this will return a Series containing python datetime objects instead of an ndarray. To retain the old behavior, call `np.array` on the result\n", + "\n" + ] + }, + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "alignmentgroup": "True", + "base": [ + "2023-09-08T12:05:00", + "2023-09-08T12:05:00", + "2023-09-08T12:10:00", + "2023-09-08T12:15:00", + "2023-09-08T12:20:00", + "2023-09-08T12:45:00", + "2023-09-08T13:15:00" + ], + "hovertemplate": "Start_time=%{base}
End_time=%{x}
Task=%{y}", + "legendgroup": "", + "marker": { + "color": "#636efa", + "pattern": { + "shape": "" + } + }, + "name": "", + "offsetgroup": "", + "orientation": "h", + "showlegend": false, + "textposition": "auto", + "type": "bar", + "x": [ + 2400000, + 300000, + 300000, + 300000, + 300000, + 1800000, + 2700000 + ], + "xaxis": "x", + "y": [ + "Acquisition", + "GOES images T-12", + "GOES images T-11", + "GOES images T-10", + "GOES images T-09", + "Assimilation", + "Model" + ], + "yaxis": "y" + } + ], + "layout": { + "barmode": "overlay", + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "type": "date" + }, + "yaxis": { + "anchor": "x", + "autorange": "reversed", + "domain": [ + 0, + 1 + ], + "title": { + "text": "Task" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "import copy\n", + "import pandas as pd\n", + "import datetime as dt\n", + "\n", + "today=dt.datetime.now()\n", + "first_task_start=dt.time(hour=12,minute=5)\n", + "time_of_start=today.combine(today,first_task_start)\n", + "\n", + "df = pd.DataFrame([\n", + " dict(Task=\"Acquisition\", Start_time=time_of_start, Duration='00:40:00', End_time=today),\n", + " dict(Task=\"GOES images T-12\", Start_with='Acquisition', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-11\", Start_after='GOES images T-12', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-10\", Start_after='GOES images T-11', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"GOES images T-09\", Start_after='GOES images T-10', Duration='00:05:00', End_time=today),\n", + " dict(Task=\"Assimilation\", Start_after='Acquisition', Duration='00:30:00'),\n", + " dict(Task=\"Model\", Start_after='Assimilation', Duration='00:45:00')\n", + "])\n", + "\n", + "df['Duration']=pd.to_timedelta(df['Duration'])\n", + "\n", + "for ti in df.index:\n", + " if pd.isna(df['Start_time'][ti]):\n", + " for predecessor in df.index:\n", + " if df['Task'][predecessor] == df['Start_after'][ti]:\n", + " df.loc[ti, 'Start_time'] = df['End_time'][predecessor]\n", + " elif df['Task'][predecessor] == df['Start_with'][ti]:\n", + " df.loc[ti, 'Start_time'] = df['Start_time'][predecessor]\n", + " else:\n", + " pd.to_datetime(df['Start_time'][ti])\n", + "\n", + " df.loc[ti, 'End_time'] = df['Start_time'][ti]+df['Duration'][ti]\n", + "\n", + "\n", + "print(df)\n", + "\n", + "\n", + "fig = px.timeline(df, x_start=\"Start_time\", x_end=\"End_time\", y=\"Task\")\n", + "fig.update_yaxes(autorange=\"reversed\") # otherwise tasks are listed from the bottom up\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "f8713f24", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "x=%{x}
y=%{y}", + "legendgroup": "", + "line": { + "color": "#636efa", + "dash": "solid" + }, + "marker": { + "symbol": "circle" + }, + "mode": "lines", + "name": "", + "orientation": "v", + "showlegend": false, + "type": "scatter", + "x": [ + 1, + 2, + 3, + 4, + 5 + ], + "xaxis": "x", + "y": [ + 2, + 4, + 1, + 6, + 3 + ], + "yaxis": "y" + } + ], + "layout": { + "legend": { + "tracegroupgap": 0 + }, + "margin": { + "t": 60 + }, + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "#E5ECF6", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "white", + "linecolor": "white", + "minorgridcolor": "white", + "startlinecolor": "#2a3f5f" + }, + "type": "carpet" + } + ], + "choropleth": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "choropleth" + } + ], + "contour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "contour" + } + ], + "contourcarpet": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "contourcarpet" + } + ], + "heatmap": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmap" + } + ], + "heatmapgl": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "heatmapgl" + } + ], + "histogram": [ + { + "marker": { + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "histogram" + } + ], + "histogram2d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2d" + } + ], + "histogram2dcontour": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "histogram2dcontour" + } + ], + "mesh3d": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "type": "mesh3d" + } + ], + "parcoords": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "parcoords" + } + ], + "pie": [ + { + "automargin": true, + "type": "pie" + } + ], + "scatter": [ + { + "fillpattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + }, + "type": "scatter" + } + ], + "scatter3d": [ + { + "line": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatter3d" + } + ], + "scattercarpet": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattercarpet" + } + ], + "scattergeo": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergeo" + } + ], + "scattergl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattergl" + } + ], + "scattermapbox": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scattermapbox" + } + ], + "scatterpolar": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolar" + } + ], + "scatterpolargl": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterpolargl" + } + ], + "scatterternary": [ + { + "marker": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "type": "scatterternary" + } + ], + "surface": [ + { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + }, + "colorscale": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "type": "surface" + } + ], + "table": [ + { + "cells": { + "fill": { + "color": "#EBF0F8" + }, + "line": { + "color": "white" + } + }, + "header": { + "fill": { + "color": "#C8D4E3" + }, + "line": { + "color": "white" + } + }, + "type": "table" + } + ] + }, + "layout": { + "annotationdefaults": { + "arrowcolor": "#2a3f5f", + "arrowhead": 0, + "arrowwidth": 1 + }, + "autotypenumbers": "strict", + "coloraxis": { + "colorbar": { + "outlinewidth": 0, + "ticks": "" + } + }, + "colorscale": { + "diverging": [ + [ + 0, + "#8e0152" + ], + [ + 0.1, + "#c51b7d" + ], + [ + 0.2, + "#de77ae" + ], + [ + 0.3, + "#f1b6da" + ], + [ + 0.4, + "#fde0ef" + ], + [ + 0.5, + "#f7f7f7" + ], + [ + 0.6, + "#e6f5d0" + ], + [ + 0.7, + "#b8e186" + ], + [ + 0.8, + "#7fbc41" + ], + [ + 0.9, + "#4d9221" + ], + [ + 1, + "#276419" + ] + ], + "sequential": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ], + "sequentialminus": [ + [ + 0, + "#0d0887" + ], + [ + 0.1111111111111111, + "#46039f" + ], + [ + 0.2222222222222222, + "#7201a8" + ], + [ + 0.3333333333333333, + "#9c179e" + ], + [ + 0.4444444444444444, + "#bd3786" + ], + [ + 0.5555555555555556, + "#d8576b" + ], + [ + 0.6666666666666666, + "#ed7953" + ], + [ + 0.7777777777777778, + "#fb9f3a" + ], + [ + 0.8888888888888888, + "#fdca26" + ], + [ + 1, + "#f0f921" + ] + ] + }, + "colorway": [ + "#636efa", + "#EF553B", + "#00cc96", + "#ab63fa", + "#FFA15A", + "#19d3f3", + "#FF6692", + "#B6E880", + "#FF97FF", + "#FECB52" + ], + "font": { + "color": "#2a3f5f" + }, + "geo": { + "bgcolor": "white", + "lakecolor": "white", + "landcolor": "#E5ECF6", + "showlakes": true, + "showland": true, + "subunitcolor": "white" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "#E5ECF6", + "polar": { + "angularaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "radialaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "yaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + }, + "zaxis": { + "backgroundcolor": "#E5ECF6", + "gridcolor": "white", + "gridwidth": 2, + "linecolor": "white", + "showbackground": true, + "ticks": "", + "zerolinecolor": "white" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "baxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + }, + "bgcolor": "#E5ECF6", + "caxis": { + "gridcolor": "white", + "linecolor": "white", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "white", + "linecolor": "white", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "white", + "zerolinewidth": 2 + } + } + }, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "title": { + "text": "x" + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "title": { + "text": "y" + } + } + } + }, + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import plotly.express as px\n", + "\n", + "# Create a simple line plot\n", + "data = {'x': [1, 2, 3, 4, 5], 'y': [2, 4, 1, 6, 3]}\n", + "fig = px.line(data, x='x', y='y')\n", + "\n", + "# Display the plot\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "be2f421c", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05119583", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/Amdahl_Applied.ipynb.txt b/_sources/Contribution/Philosophy/Amdahl_Applied.ipynb.txt new file mode 100644 index 000000000..4190541e6 --- /dev/null +++ b/_sources/Contribution/Philosophy/Amdahl_Applied.ipynb.txt @@ -0,0 +1,656 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "c565b776", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Amdahl's Law Applied\n", + "==================\n", + "\n", + "\n", + "* Ideally: code on 4 cores runs 4x faster than on 1.\n", + "* That is rare. There is a name for it: \"Embarassingly Parallel.\"\n", + "* It should really be called \"Perfectly Parallel.\"\n", + "* Usually, application *Speedup* is limited by how it is designed or implemented.\n", + "* When using large numbers or processes and processors, this hits *HARD.*\n" + ] + }, + { + "cell_type": "markdown", + "id": "bbac9108", + "metadata": { + "slideshow": { + "slide_type": "notes" + } + }, + "source": [ + "\n", + "Speedup\n", + "-------------\n", + "\n", + "If you have a single processor to run code, then assuming perfect parallism (the so-called \"embarrasingly parallel\" case) then your code will run twice as fast on two processors, four times as fast on four, etc... This is the ideal case. Much real-life code has \"dependencies\" that limit speedup.\n", + "\n", + "Speedup of a task, when adding processors is limited by how much of the code can run in parallel. If you had an infinite number of processors to apply to a problem, then performance is limited by the p the parallelizability of your code.\n", + "\n", + "The graph below plots the speedup available against the degree to which the code is parallelizable (from 5% to 95% in steps of 5%.) For example, if the algorithm is only 50% parallelizable, then throwing an infinite number of processors at the problem will give you a 25% speedup ( 1.25 on the plot.) so it isn't worthwhile.\n", + "\n", + "Note that, towards the right edge of the plot, the speedup is nearly vertical. This means that the difference in real life performance near that limit, say between 98% and 99% parallelizable, is huge (as in closer to ten fold than 10%)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "588983a6", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "7bf6e23a", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input", + "remove-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "P = np.linspace(0.05,0.95)\n", + "\n", + "speedup = lambda p: 1/(1-p)\n", + "\n", + "S = np.vectorize(speedup)(P)\n", + "\n", + "f1=plt.figure()\n", + "plt.plot(P,S)\n", + "plt.xlabel(\"Proportion of code that is Parallelizable\")\n", + "plt.ylabel(\"Maximum theoretical Speedup\")\n", + "plt.title( \"Speedup vs. Algorithmic Parallelism\")\n", + "\n", + "plt.show()\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "204608a1", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "What is Amdahl's Law?\n", + "---------------------------------\n", + "\n", + "* Speedup a program can achieve is limited by the level of parallelization of its design.\n", + "\n", + "* If code is 95% parallelized, then 5%, or 1 part in 20, is serial... that means, no matter how many processors are put on the job, eventually you are limited by the part that isn't parallel. It's asymptotic speedup (that it will never reach, is 20x.)\n", + "\n", + "* The more processors you want to use, the more this law bites.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4be08b2a", + "metadata": { + "scrolled": false, + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input", + "hide_code" + ] + }, + "outputs": [ + { + "data": { + "image/png": "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yLXABmAIgIi0xbmddbCqukRkNPApzm2uL6nqL0GMNai+/vpr2rZtS1ZWFoMGDaJbt26cdNJJlbbv1asX3333HQALFy6kbdu2qCpDhw4lMjKSSZMm0bp1632Wu/TSS8vex4wZAzi3+Q4dOpSMjAyKi4vL3f553nnnERsbCzjPi4wePZply5YRHh7Ob7/9VuVnmjdvHrNnz2bixImAc7fYhg0bKm3/v//9j5EjR5adymrZsiXLly+nc+fOHHbYYYBzFPXss88yYMAAOnfuTFpaGgDDhg1jxowZVW63e/fuVcbbGKnHQ/Gff1K0ahWFq1ZR+MuvFK5a5fz6BxAhok0bIpJbEdmpE7F9+hBZurP32fGHt2hhv+AbqWDexfQV/q8lAJzqp/0WYLDP+BxgTk3GtL9f+sHStm1bAJKTk7ngggv44YcfOOmkk2jdujUZGRmkpKSQkZFBcnL56/Wqyr///W/efvttRo8ezQMPPEB6ejpPP/00Dz300D7b8b0+UTp80003cdttt3HeeeexYMECxo8fX9bG91f3k08+SevWrVm+fDkej4eYmJgqP5Oq8t5779G1a9dy0zMzMyttX/H6SVX9oVd2raWy7TZ2WlxM0bp13iODVRT++itFq1c75/sBIiOJ7tKFhIGnENP9cGIO705M166E2ZGXqYKV2giy/Px8du3aVTY8b948jjzySMD5BT9z5kwAZs6cuc+59JkzZ3L22WfTokULCgoKCAsLIywsjILS/+krePvtt8vejz/+eAByc3Np165d2foqk5ubS0pKCmFhYbz22mu493NnxxlnnMEzzzxTtpP/6aefqmx/+umnM23aNFzei5Pbt2+nW7dupKens3btWgBee+01Tj75ZLp168b69etZt24dAG+++eYBb7ch8hQUULD0J7a//jpb7ruPPy68kDXH9GH9BReSce+97HzfudzX7IILSHno33R+/z26LVnMIR+8T9uHHqLlsMuJ693bkoPZL7t1JcgyMzO54IILAOduocsuu4wzzzwTgHHjxnHJJZfw4osv0rFjR959992y5QoKCpg5cybz5s0D4LbbbuOiiy4iKiqq3A7TV1FREf369cPj8ZS1GT9+PEOGDKFdu3Ycd9xxrF+/3u+yN9xwAxdddBHvvvsup5xyyn7P6f/jH//g1ltvpWfPnqgqqampVd6Get111/Hbb7/R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Nj9hwgROPfVUfv/9d0499VQmTHBKgZSW+3744YeZOnUqQNDLfTdmrpwcNl57LZ7CQjq88DxR7duFOiRj6gQrtRFkq1at4rjjjiMuLo6IiAhOPvlkPvjgAwBmzZrFVVddBTiF6j788EPAyn1D7ZX7du/ezcbhIyjJzKTDtGnEeAsHGmMaWamNR394lNXbV9foOru17MZdfe+qdP6RRx7JvffeS05ODrGxscyZM4c+fZyHFjMzM0lJSQEgJSWFrKwsACv3XUvlvj1FRWy6cTSFv/1Gh2enENfbnow2xlejShCh0L17d+666y4GDRpEQkICRx111H57byst9w2wcOHCcuW+IyMjmTRpUrlKsKV8y32PGTMGcMp9Dx06lIyMDIqLi+ncuXNZ+4rlvkePHs2yZcsIDw/nt99+qzLGefPmMXv2bCZOnAhwQOW+ly9fvk+572effZYBAwaUlfsGGDZsGDNmzKhyu9UtraFuN1vG3kHB99/T9vHHSKji6MyYxqpRJYiqfukH07XXXsu11zrX6O+55x7at28POOW+MzIySElJISMjg+Tk5HLLWbnvwLdbHarK1vEPsOuzz2h9z900O/fcA16XMQ2ZXYOoBaWnjjZs2MD7779f9kv/vPPOKyvBPXPmTM4///xyy1m57+CU+85+ajI7332XxOuvp+WVV1Z7eWMai0Z1BBEqF110ETk5OURGRvLss8/SokULAMaNG8cll1zCiy++SMeOHXn33XfLlrFy38Ep97195kxypk+n+SWX0OrWWwJezpjGKGjlvkPByn1buW+o/G+eO2sWW+4aR5NBg2j31JNIeHgIojOmbqmq3LedYjKNwq4FC9hyz73E9etH24mPW3IwJgB2iqmBsHLflStYupTNt44hpmtX2j87xfpyMCZAdgRhGrTCNb+xceQoItu0sfpKxlSTJQjTYBVv2sTG665z6iu98ILVVzKmmuwUk2mQXDk5bLj2WjzFxXR67VWrr2TMAbAEYRoc9XjYMHw4rswsOr70ktVXMuYA2SmmWlBZue/x48fTrl07evXqRa9evZgzx6l4buW+D3yd6vHg3r6dot9+p/3Tk62+kjEHwRJEkFVV7htgzJgxLFu2jGXLljF48GDAyn0fKFWlZNMmtLiYto88TMJJJ4U6JGPqNUsQQVZVue/KWLnvAyv37crIwJ2XR3jTplZfyZga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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "nproc=np.logspace(0,16,num=17,base=2,dtype=int)\n", + "\n", + "# p is the parallelizable part of the code, that can be sped up.\n", + "# n is the number of processors applied to the problem.\n", + "def t(p,n):\n", + " return (1-p)+(p)/n\n", + "\n", + "A = {}\n", + "for p in [ 0.5, 0.75, 0.9, 0.95 ]:\n", + " i=\"%d\" %p\n", + " A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)\n", + " plt.plot(nproc,A[i], label=\"%02d%% parallel code\" %int(p*100))\n", + "\n", + "plt.xscale('log',base=2)\n", + "plt.xlabel(\"number of processors\")\n", + "plt.ylabel(\"speedup\")\n", + "plt.title(\"Amdahl's law\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "338525a3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Large Numbers of Processors Need High Parallelism\n", + "---------------------------------------------------------------------------\n", + "\n", + "In the previous picture, we could see that:\n", + "\n", + "* we could never get more than a 20 fold speedup if 5% of the code is serial. \n", + "* To use higher number of processors, we need higher and higher levels of parallelization. \n", + "* There limit is, of course, 100% parallel code, usually referred to as \"embarrassingly parallel.\" \n", + "* to use larger numbers of processors, the ideal is to write embarassingly (aka Perfectly) parallel code." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0d21732c", + "metadata": { + "scrolled": false, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "image/png": "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hhdeeIH777+fv/3tb/ts7/f7ufrqq5kzZw7p6ekcccQRnHnmmQwa1HBdE/v9frxeb+0rGmMaTaCwkNINGynbsIGyDRso3bCesg0bKdu4EQ1qwvXExxPRsyexo0cT0bMnEb16EtmzJ57o6AaPqTGfhjq/hkUn1rD+VJxO36vOn08r6Xt35cqVjB49mpiYGADGjBnDu+++y80338zq1as57jinW+mTTjqJcePG7Zcs5s2bR58+fejVqxcA5513Hu+///5+yeLiiy8mKiqK5cuXk5mZyYMPPsjpp5/Oxo0b+fWvf01hYSEAjz/+OEcffTRz587lrrvuonPnzixevJgVK1YwceJEtmzZQklJCddddx2XX375Ad/b/fffz6xZsygtLeWss87irrvuapC/mTGtmapSunIlRQsWUrZhPaXrneTgy8zcu5LHQ3h6OhE9exD7s58R0asXET17ENmrF94mvAfbast91OauD5azYlv1TT31NahLAneeMbjG5Ycddhi33347OTk5REdH8/HHHzNq1KjKZbNnz2bChAm8+eabbNmyZb/tt27dSteue2/tpKen8/3331d7rI0bN/Lll1/y008/MXbsWNatW0dKSgpz5swhKiqKtWvXcv7551eWRJk3bx7Lli2rfELphRdeIDExkeLiYo444ggmTZpEUlJStcf69NNPWbt2LfPmzUNVOfPMM/nqq68qk58xZq9AURGF335LwdwvKfjqq8rEsN9VQs+eRPbqSXj37ngiDrWL9UPXZpNFKAwcOJBbbrmFk046ibi4OIYNG0ZYmPNP8MILL3Dttdfy17/+lTPPPJOIaj4c1fU9UtO3inPOOQePx0Pfvn3p1asXq1atomfPnlxzzTUsXrwYr9fLmjVrKtc/8sgj93mU9dFHH+Xdd98FYMuWLaxdu/aAyeLTTz/l8MMPB6CgoIC1a9dasjDGVbZli5McvvySonnz0LIyPLGxxB5zDHHHH0/sMUcTlpLSrB/+aLPJ4kBXAI3p0ksv5dJLLwXgtttuIz3dedhrwIABfPqp8xTDmjVr+Oijj/bbNj09fZ8rjoyMDLp06VLtcap+6ESEhx56iNTUVJYsWUIgECAqKqpyeWxsbOX43Llz+c9//sO3335LTEwMxx9//AEfdVVVbr31Vq644ora3r4xbYKWl1O0aBEFX35JwdwvKfvpJwAievSg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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "A = {}\n", + "for p in [ 0.99, 0.995, 0.997, 0.999 ]:\n", + " i=\"%d\" %p\n", + " A[i] =np.vectorize( lambda n: 1/t(p,n) )(nproc)\n", + " plt.plot(nproc,A[i], label=f\"{p*100} parallel\" )\n", + "\n", + "plt.xscale('log',base=2)\n", + "plt.xlabel(\"number of processors\")\n", + "plt.ylabel(\"speedup\")\n", + "plt.title(\"Amdahl's law with p>99%\")\n", + "plt.legend()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7653b5af", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Serial Example\n", + "---------------------\n", + "\n", + "Below is a simple example of a python snippet that we want to accelerate. It does a math function and calculates \"tot\" over a range of numbers. " + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "70f5c093", + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "subslide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.21661385363\n", + "CPU times: user 161 ms, sys: 0 ns, total: 161 ms\n", + "Wall time: 160 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "\n", + "def count():\n", + " global tot,counter\n", + " counter=0\n", + " while counter < max:\n", + " tot += 1/tot\n", + " counter += 1\n", + "\n", + "count()\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "511d4722", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "\n", + "So it calculates the answer, and it does it in 122 milliseconds, entirely in user space (0 ns of system time.)\n", + "We want to make it faster, so let's add threading.\n", + "\n", + "* import threads\n", + "* run four threads in parallel.\n", + "\n", + "should finish four times faster, right?\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "a8c41990", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 2449.49161668406\n", + "CPU times: user 404 ms, sys: 3.8 ms, total: 408 ms\n", + "Wall time: 405 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "import threading\n", + "\n", + "max=1000000\n", + "tot=1\n", + "\n", + "def count():\n", + " global tot\n", + " counter=0\n", + " while counter < max:\n", + " tot += 1/tot\n", + " counter += 1\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "84d5090d", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "\n", + "OK, so the answer is wrong, and it took 3 times more cpu time to do it. How come?\n", + "\n", + "* Because the overhead for setting up the threads is much more than the size of the problem. (amortization)\n", + "* But the answer is still wrong. Why?\n", + " * the four tasks are changing the same counters and writing on eachothers work, \n", + " * it is a pile of race conditions. \n", + " \n", + "* How can we get the parallel code to get the right answer?\n", + "\n", + "* We add locks, to serialize access to the variables the tasks are contending for." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9910d980", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.2180280637874\n", + "CPU times: user 2.7 s, sys: 2.3 s, total: 5 s\n", + "Wall time: 2.78 s\n" + ] + } + ], + "source": [ + "%%time\n", + "import threading\n", + "\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "counterLock = threading.Lock()\n", + "totLock = threading.Lock()\n", + "\n", + "def count():\n", + " global counter,tot\n", + " while counter < max:\n", + " totLock.acquire()\n", + " tot += 1/tot\n", + " totLock.release()\n", + "\n", + " counterLock.acquire()\n", + " counter += 1\n", + " counterLock.release()\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )" + ] + }, + { + "cell_type": "markdown", + "id": "f721701a", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "OK, now instead of 3 times slower it is now almost 20 times slower, and spending half the time in the kernel,\n", + "but the answer is correct. Why did synchronizing access to get the correct the answer make the cpu time double? \n", + "\n", + "**locking is expensive** To parallelize code, use locks sparingly. locks are signs of serial code, and serial code fundamentally limits performance. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "f978a69e", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " total is: 1414.2180280637874\n", + "CPU times: user 687 ms, sys: 810 ms, total: 1.5 s\n", + "Wall time: 917 ms\n" + ] + } + ], + "source": [ + "%%time\n", + "\n", + "import threading\n", + "\n", + "max=1000000\n", + "counter=0\n", + "tot=1\n", + "critLock = threading.Lock()\n", + "\n", + "def count():\n", + " global counter,tot\n", + " while counter < max:\n", + " critLock.acquire()\n", + " tot += 1/tot\n", + " counter += 1\n", + " critLock.release()\n", + "\n", + "threadMax=4\n", + "threads = []\n", + "threadNumber=1\n", + "while threadNumber < threadMax:\n", + " t = threading.Thread(target=count)\n", + " threads.append(t)\n", + " threadNumber += 1\n", + "\n", + "for t in threads:\n", + " t.start()\n", + "\n", + "for t in threads:\n", + " t.join()\n", + "\n", + "print( f\" total is: {tot}\" )\n" + ] + }, + { + "cell_type": "markdown", + "id": "28d52604", + "metadata": { + "slideshow": { + "slide_type": "fragment" + } + }, + "source": [ + "Here, instead of using a lock for each variable, we use the concept of a shared lock for when both variables. \n", + "It eliminates about 40% of the execution time, so now it is only 9x slower than serial." + ] + }, + { + "cell_type": "markdown", + "id": "628b9fd7", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## NEWS FLASH: Serial 9x Faster than Parallel!\n", + "\n", + "* Sometimes the thing you are trying to do is inherently serial. \n", + "* Do something else instead.\n", + "* more hardware can slow things down because of co-ordination.\n", + "* locks are hard to get right, and can super easily kill performance.\n", + "* Also: headlines never tell the whole truth.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "9a8ff838", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Summary\n", + "--------------\n", + "\n", + "* Application design limits speedup.\n", + "\n", + "* **Perfectly Parallel** is the gold standard.\n", + "\n", + "* *almost all parallel*, counts for little when things scale.\n", + "\n", + "* Locks, joins, waits... they mean you failed to do the above.\n", + "\n", + "* (Synchronous) API calls... mean you failed to do the above.\n", + "\n", + "* Every small percentage of synchronization or global state, limits ultimate performance. \n", + "\n", + "* As the number of processes and processors rises, these small things dominate.\n", + " \n", + "* locks, joins, waits... are all expensive, in terms of cpu time. They add overhead to any application. If you use them, make sure they are worth it, and amortize them by making them as coarse as possible.\n", + "\n", + "* Ideally, try to formulate the algorithm so there are no synchronization points.\n" + ] + }, + { + "cell_type": "markdown", + "id": "d0f6f04d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Relevance?\n", + "\n", + "Note that on our main cluster (ddsr.cmc) the configuration is 128 processors:\n", + "\n", + "* 16 processors / node\n", + "* 8 nodes in the cluster\n", + "* 600 configurations/node (identical on all nodes)\n", + "* ~900 processes/node = 7200 processes in the application.\n", + "* at 7200 processes, 99.9% parallel isn't good enough to make good use of hardware.\n", + "* At this scale, you really want p to be around 99.99%...\n" + ] + }, + { + "cell_type": "markdown", + "id": "226c39b7", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## So Sarracenia is Not a Parallel App!\n", + "\n", + "\n", + "It is just an ordinary python script that launches processes. It's not a real parallel app.\n", + "\n", + "* it is not based on threads !\n", + "* it does not use global locks, no joins, no synchronization!\n", + "* it is not leveraging multiprocessing!\n", + "* it is launching completely independent processes that never synchronize!\n", + "\n", + "### Yes, Exactly." + ] + }, + { + "cell_type": "markdown", + "id": "f9b00250", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "## THANKS!\n", + "\n", + "\n", + "light reading:\n", + "\n", + "* https://en.wikipedia.org/wiki/Amdahl%27s_law\n", + "* https://jenkov.com/tutorials/java-concurrency/amdahls-law.html\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/CAP_Theorem_Applied.ipynb.txt b/_sources/Contribution/Philosophy/CAP_Theorem_Applied.ipynb.txt new file mode 100644 index 000000000..05d44d8af --- /dev/null +++ b/_sources/Contribution/Philosophy/CAP_Theorem_Applied.ipynb.txt @@ -0,0 +1,583 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b3167c7f", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# CAP Theorem Applied\n", + "\n", + "* Peter Silva, 2023\n" + ] + }, + { + "cell_type": "markdown", + "id": "9160eaed", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "### CAP Theorem \n", + " * Originally proposed by Eric A. Brewer in 2000. \n", + " * Proved in 2002 by Gilbert & Lynch.\n", + " * Definitions from Gilbert & Lynch\n", + " \n", + "### Definitions\n", + "* Consistency - _any read operation that begins after a write operation completes must return that value, or the result of a later write operation_ \n", + "* Availability - _every request received by a non-failing node in the system must result in a response_ \n", + "* Partition (Tolerance) - _the network will be allowed to lose arbitrarily many messages sent from one node to another_ " + ] + }, + { + "cell_type": "markdown", + "id": "1ce1e254", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Cap Theorem\n", + "\n", + "* An ideal distributed system will feature all Consistency, Availability, and Partition Tolerance. \n", + "\n", + "* In reality, the best one can do is 2 out of three. " + ] + }, + { + "cell_type": "markdown", + "id": "0bc777a6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Proof\n", + "\n", + "* walk through: Gilbert & Lynch (Thanks to MWhittaker): https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/ ?\n", + "\n", + "* Real usage of CAP theoream involves looking at what is pragmatic in real-life cases." + ] + }, + { + "cell_type": "markdown", + "id": "bc391fe2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Storage/State\n", + "\n", + "* What is a File System? -- an ordered list of writes (with a path)\n", + "\n", + "* What is a SQL Database? -- an ordered list of writes (with indices)\n", + "\n", + "* What is a File? - an ordered list of bytes, with an id (path.)\n", + "\n", + "* What is an Object (in object storage context) -- an ordered list of bytes, with an id (opaque hash)" + ] + }, + { + "cell_type": "markdown", + "id": "26baf6e2", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## They Are All Units of Synchronization\n", + "* Traditional databases and file systems are just really large files. \n", + "* files and objects are the same thing\n", + "* They are units of synchronization/state.\n", + "* Consistency == order == synchronization == global state.\n", + "* When Distributed...\n", + " * User expectation is coherence/consistency\n", + " * Coherence is the same thing as shared state.\n", + "* Amdahl's Law tells us that scaling global state is really, really hard.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "e53833fe", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch\n", + "\n", + "def three_circles(ax, x, y, box_bg, arrow1):\n", + " patches = [\n", + " Circle( (x,y), 0.3, fc=box_bg),\n", + " Circle( (x+2,y), 0.3, fc=box_bg),\n", + " Circle( (x+1, y-1), 0.3, fc=box_bg),\n", + " FancyArrow( x+0.4, y, 1.2, 0, fc=arrow1, width=0.05, head_width=0.05, head_length=0 ),\n", + " FancyArrow( x+0.25, y-0.3, 0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),\n", + " FancyArrow( x+1.7, y-0.3, -0.4, -0.4, fc=arrow1, width=0.05, head_width=0.05, head_length=0),\n", + " ]\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(x-0.15,y+0.4, 'G1', fontsize=20)\n", + " plt.text(x+1.85,y+0.4, 'G2', fontsize=20)\n", + " plt.text(x+0.70,y-1.6, 'Client', fontsize=20)\n", + " \n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(8, 4.2), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 8)\n", + " ax.set_ylim(0, 6)\n", + "\n", + " x=3\n", + " y=3\n", + " three_circles(ax, x,y,box_bg,arrow1)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "703693f9", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Assuming a file system can do 100000 IOPS\n", + "distance between data centres=600, speed of light=300000 km/s, time to travel (1way)=0.002 sec. \n" + ] + } + ], + "source": [ + "# Proof of IOPS ceiling\n", + "SAN_iops=100000\n", + "time_for_1_iop=1/SAN_iops\n", + "distance=600\n", + "c=300000\n", + "tprop=distance/c\n", + "max_iops=1/(2*(time_for_1_iop+tprop))\n", + "\n", + "print( f\"Assuming a file system can do {SAN_iops} IOPS\")\n", + "print( f\"distance between data centres={distance}, speed of light={c} km/s, time to travel (1way)={tprop} sec. \" )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "id": "4db28587", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "data": { + "image/png": "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pfo5OvJI6++ZU+fdkSNHTPXq1dOsMyAgwGzbts1af9GiRenGUalSpQxn3kTuYjYz5FleXl569NFH9emnn+rYsWNav369mjVrlqLcrFmzZIyRZN+a44gbLS7e3t6qVq2add8SY4z+/fdf/f7774qMjFR8fLxefvllSdJLL72Ual3NmjXTyy+/rIkTJ0qS+vXrp927d6tYsWJ25a5cuaIePXpYV7Z69uyZ6dakrLj5alhWt/n4449bdR07dkzHjx9XaGjoLcWWfDyKJAUEBFjPixQposDAQKtL26RJk9SuXbtcmUmsc+fOql69uqTrx9mNsTy9e/dWoUKF0l13yJAh+uijj6zXfn5+uu+++3THHXcoIiJCa9as0eXLlxUTE6MRI0bo9OnTmjx5coYxGWPUs2dPLVmyRDabTXfddZeqVKkiY4z27t2bpc9l9erV1nMvLy917do103Vk5L333rPr0ujt7a1mzZqpdOnSOn/+vNasWaNz584pMTFRU6ZM0fHjx/X99987tD9jxozRm2++Ken/xsx5enpq586d2r59uyQpPDxcnTp10rZt26zxeKn5z3/+Y9e6VaRIETVs2FAlSpRQTEyMduzYob1798oYoy+//FL//POPli5dmmHXw6ioKLVr104nTpyQj4+PmjRpotDQUF26dCnF1fUDBw6oZcuW1uyMNptNtWvXVrVq1eTr66tTp05p/fr1unTpkv755x+1bt1aP/30U4atYtL1Founn35aklSpUiXVr19fBQoU0MGDB/Xrr7/KGKOoqCh17NhRBw4cSDGz4I2ZCX/44Qf9888/ktKebaxKlSoZxuMqsuu4SExMVPv27bVt2zZrWfXq1a1ZGWNiYhQREaFdu3alOTvnwIEDNWnSJEnSt99+qylTpsjPzy/d+JN3L37kkUdSfH/16tXLrptp+fLlVadOHQUFBSk+Pl5nz57Vnj17dOzYsRR1+/v7W8eFI9+TqR0rOXXeRUdH64EHHtCRI0cUGBio5s2bKyQkRH///bdWr16tuLg4Xbx4UW3bttWff/6p7du3q2vXroqPj1doaKgaNWqkggULat++fdq0aZOk662aPXv21MqVK9PdNnKR8/IowN7NLTPG2N/b44knnkh1vfLlyxtJJjAw0Fy7ds0Y43jLzNNPP22WLl1qrl69mur7CQkJZsaMGcbPz89I11tojh49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aIxRfHy8IiMjtXXrVn300Ue33EVQut660LVrV61evVr//e9/7badm93NkrdsONqdL3m55P/cU3Oju1R2Sn5s3MpNK1OT059HdkzRnR2tDxn9sHHk75YbceTGlOa3i5z6e7zwwgtav3692rZta9cSEx0drRUrVmjEiBGqXLmyWrZsqT179qRb/8CBA63nN3c127Bhg9XjoGTJkmrbtm2a9Tz88MPaunWrHnnkEXl5eVnLr127pnXr1unNN99U/fr1Vb9+fa1fvz79nXZQXjzeOT9cE8kMXEKhQoWsK067d+/Wyy+/bH2JZfbeMje899571vPx48dr8eLF6tmzp6pVq6bAwEC7L3Qp4x9VN4uNjVWPHj3s7l5/9epV9evXL0vx3qrkCUNkZKT+/PPPLNXz+++/272+9957bymunPCf//zHLmncsGFDrm07efclR7vzJS+X0f1rckLyv+GuXbvsjtlb5Qqfx81J1oULF1Ik4Bk90rureVbieOGFFzIdgzHGad2xbkc5eVzce++9Wr58uU6fPq0FCxbo+eefV926de2Sm9WrV6tBgwb69ddf04zxkUcesboVLlu2zO7eNMnvLdOvX78MuzbXqFFD8+fP19mzZ7VkyRK99NJLatSokd0FuW3btqlFixb67rvv0q3LEXnlvIPrI5mBy0ietHz11VeSrg/ey2hAa2r+/vtvHTlyRNL1PrBp3Qjzhujo6Aybs2/20ksvWVfVSpUqZX1x//TTT04ZmH7zj5yvv/46S/XMnTvXeh4aGmp3VT8vuf/++63nad18Lick707k6ODp48ePW8+Dg4OzPaaMJB9QGxsbq++//z7b6naFzyMwMFDe3t7W6xvfDbkt+Q12nRUD/k9uHBfBwcHq2rWrpkyZom3btun06dP64IMPrOP+2rVreuqpp9Jcv0CBAurZs6ek612AZ86cKen6xbcbk6vYbDY98cQTDsfk7++v9u3b6+2339Zvv/2myMhIffXVV9Z3fVJSkp555hlrDFpW5ZXzDq6PZAYuo1WrVipRooTdsoceesjhPvPJJe+bXqlSpRSDJm+2ceNGGWMcrn/FihVWwuLm5qa5c+fa3d34pZdespsUIDeEhYWpTZs21uvPP/880xMBbN++XevWrbNe37iTeF7k4+NjPU/+DzOn1alTx3r+22+/ObRO8iuvGc32lROaNGliN0vPlClTHO5KmRFX+Tzuvvtu6/mKFStyZZs3a9CggfV83bp12dpClt3yS3ec3D4ugoOD9eyzz2rRokXWsn379uno0aNprpNaV7N58+ZZ3+/33XffLY3T9Pf3V58+fbR69WrruzQyMtJu8P4NmT0u8sJ5B9dHMgOX4e7urh49etgty2oXs+RN+VevXs2w/CeffOJw3WfPnlXfvn2t5Ofll19W06ZNNWDAAHXp0kXS9emab+6ClhuGDx9uPT916pRGjBjh8Lrx8fEaOHCgtV+FChWy+yea1+zcudN6nnzq0ZyWvJVj2bJl1lS7aYmIiLDGf928fm6x2Wx2rZNbtmzR5MmTM11PQkJCiqmUk+/Pjh07tGvXrnTruHbtmubNm5fq+jmpQ4cO1vNPP/1UMTExubLd5Bo3bmx1Gbp8+bKmTZuW6zE4KvnFgtt5kgBnHReNGjWym1zl9OnTaZatUaOGGjZsKOl668b69evtxs+kNfA/s8LCwuxmB0stpsweF3nhvIPrI5mBS3nttde0ZcsW65HegMb0hIWFWVeQ9u7dq7/++ivNst9++62WLFnicN1PPPGEIiIiJF2/N0ryaYGnTZumkiVLSrp+v5qXX345K+FnWatWrdS3b1/r9Ycffmg3digtCQkJ6tatm7Zt22Ytmzp1aoYzumWHuLg4DRkyxG46zowcOHDAbmac5PdxyWlt2rSxroLGxsbqhRdeSLOsMUbPPfec4uLiJF2/X1KrVq1yI8wUevbsaffD4uWXX9bnn3/u8PrHjx9Xy5YtrW4uN1SuXFlNmza1Xj/77LPp/sh5/fXXrQTQ398/xQWMnPLUU09ZicTJkyf1zDPPONwaGxkZaTcteFZ5e3vbHS+vvvpqhgPAk0vvB292Sz5986lTp3Jtu7ktu4+LjGbnu+H8+fN2k2dkNBvek08+aT1/5ZVXtHnzZknXu1HfuIiWXpyOSEhIsOuym1pMmT0u8sJ5B9dHMgOXEhgYaM2oUr9+/SzfqyU4ONjq0pGUlKRHHnkkxTSoSUlJ+uijj9SrVy+5u7vbXXFKy8cff2wlPn5+fpo7d67d4MmgoCDNnDnTSqQ++OCDXG9anzp1qmrWrGm9Hj58uLp27ZrqfXaMMVq/fr3uuusuLVy40Frev3//LLeKZdaNv0O5cuXUrVs3LVu2LM2rdwkJCfrmm2/UvHlzqz+3n5+fnn322VyJVbre6vfWW29Zr7/55hs9+eSTKe7fcunSJfXv399uIO3bb7+dqftMZCebzaaZM2eqQoUKkq73v3/qqafUsWNHbdmyJc0fGHv37tULL7ygihUrpjnL0VtvvWWdqxs2bFDXrl1TtFjFxcXptdde06RJk6xlo0aNyvabeKYlICDArjVqxowZ6tixow4ePJhqeWOMNm3apCFDhig0NPSWxw/cMHToUOvq96VLl3Tvvfdq2rRpVsJ7s6ioKH3xxReqV6+e3n333WyJwRHJ71ezYMGCTHXDdSXZfVw8+uijat++vb777rs0u/meOHFC3bp1s/7mFSpUUPny5dONs1u3btY9v5J3/+rZs2eG3WyHDx+uJk2aaObMmWmODT179qyeeOIJK5nx9/dPdRbK5MfF/Pnz092ulHfOO7g27jODfGvcuHFq06aNkpKStGPHDtWoUUONGzdW2bJldfnyZW3YsMH64h4/frw+//xzu4HJNztw4ICGDRtmvZ48eXKqd6pv2bKlXnzxRU2aNEnGGPXt21d79uzJtYHffn5+WrdunTp16mSNf1m4cKEWLlyomjVrqnLlygoICNDZs2e1ffv2FIO2//Of/9j94MwtcXFx+vbbb/Xtt9/K09NTtWvXVpkyZVS4cGElJCTo1KlT+uOPP+z+GXt4eOjLL7/M8KaK2e3RRx/V+vXrrZuFfvHFF/r222/VokULFStWTGfOnNHq1avtZsh74YUXMrz7dE4LCgrSpk2b1LVrV+vYWLJkiZYsWaI77rhD9erVU0hIiNzc3BQREaF9+/alOCdSm32sUaNGeuutt6xujosXL1bp0qXVokUL3XnnnTp//rzWrl1rd4W4c+fO+s9//pODe5tS3759dfToUY0dO1aStHTpUi1btkzVq1dX9erV5e/vrytXrujUqVPasWNHtt+TR7o++9uiRYvUqlUrhYeHKzo6WgMHDtTw4cPVqFEjlSxZUjabTefOndOBAwd06NAha3xTixYtsj2etHTp0kUjRoyQMUZLly5VzZo1dc8999j9/bt165Zr93fKSdl5XCQlJWnZsmVatmyZPD09Vb16dVWsWFEBAQG6dOmSjh8/rs2bN1t/U3d3d33wwQcZxujr66vHH388RZfoAQMGZLiuMUYbN27Uxo0b5e7urkqVKqlq1arWjWZPnjyp3377zS6hfu+991Kdbrxr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2rt3b5rbL1WqlAIDA3NyFwEgTTaT1i2BASCPWbx4sYYOHaojR444VL5x48Z6++231bhxY7vlx44dU1hYmMPbHTVqlEaPHp2ZUIFclx3nx+jRozVmzJhMbXfGjBnq27dvptYBgOxCNzMALqNjx446cOCAFi5cqIEDB6pGjRoKCQmRh4eHChUqpNDQUD3wwAMaPXq09u3bp40bN6ZIZHKaI3dSz4r33ntPlSpVUtWqVTV58mRJkjFGgwYNUvny5VW/fn399ddfVvk333xT5cuXV7Vq1ayr8GvXrtXDDz+cI/HdipiYGHXp0kXly5dXixYtFBkZmaJMZvd1586datiwoapXr6569eo51PUqL8nKceQK5wcAZDsDAMg2zZo1S/f9hIQEEx0dnak6V65caR544AETFxdnjDHm9OnTxhhjFi1aZLp27WqMMebHH3+0nu/evds0aNDAxMfHmx07dpj69esbY4xZs2aNVSYv+eCDD8zQoUONMcZMmTLFep5cZvf18OHD5s8//zTGGHPgwAETFhaWG7vikAsXLpjExMR0y2R0HAEArqNlBgBywd9//63Ro0erUqVK2rFjR6bW/eyzzzRixAh5enpKkooWLSrpereiXr16Sbp+Vf7XX3+VMUaLFy9W9+7d5eHhodq1aysuLk7//vuvXZ2rV6/WPffco6ioKPXt21dDhgxR8+bNVaFCBW3atEndunVTxYoVNWLEiGzY+/Ql34/evXtr8eLF6ZZxZF8rVKigcuXKSZIqVaqky5cvKzEx0a7OxMRE9ezZU1WrVlWNGjU0Y8YMSdIff/yhJk2aqG7duuratasuX74sSfrtt9/UoEED1apVSy1btszy/m7YsEFVqlTRhAkTFBERkeV6AAB0MwOAHJOYmKglS5aoY8eO6tixowoXLqw//vhDTZs2zVQ9R44c0S+//KK7775bbdq00eHDhyVJ//zzj0qWLClJcnNzU1BQkKKiouyWS9cHaJ86dcp6vWrVKo0cOVKLFy+2brp46dIlrV27VqNHj1bHjh319ttva+/evZo3b16q3b46d+5sTYyQ/LF161ZJ0uDBg1N9f9GiRSnqSh5v4cKFdeHChXTLZGZfJemHH35QvXr15O7ubrd8586dCg8P1/79+7Vnzx516dJFcXFxGjZsmBYtWqTt27erYcOGmjp1qmJjY9WzZ09Nnz5du3bt0nfffZfGXytjHTp00IYNG+Tp6alWrVrp4Ycf1sqVK2UYwgoAmcZsZgCQQzp37qzdu3dr5syZatasWZbriY+P19WrV/XHH39oxYoV6tevn9UycTObzZbmcknatWuXhg4dqlWrVlmJjCQ9+OCDkqQaNWqoQoUKCg0NlSRVqFBBf//9d4qbJf7www/pxnxjumBHOPIjPiv7Kkl//fWXXn75Zf30008pypUtW1b//POPBg8erIceekht2rTRnj17tHv3brVo0UKSFBcXp+bNm+vQoUMqW7asqlevLkkKCgpyeP9SU7RoUQ0fPlzDhw/XihUr1KdPH7Vq1UqzZs26pXoBIL+hZQYAcsjEiRPVoUMHPfnkkxo+fLgOHTpkvXfs2DGrtWLBggV65ZVXVLt2bbVq1SpFPaVKlVKXLl0kSW3btrVmqypZsqTVCpGUlKRz584pKCjIbrkknTx5UsWLF7fWsdls2rdvn902vL29JV1v9bjx/Mbrm7tnSdnbMpM83vPnz6c6zW9W9vXcuXPq1KmTPvvsM5UvXz5FnYULF9aePXvUvHlzTZo0ScOGDZMxRnXr1tXOnTu1c+dO7d+/Xx9//HGKdTPSqlUr1a5dW6+88ooWLFhg7f+xY8esMnv27NHzzz+vZ599Vt26ddPIkSMzvR0AyPecNloHAG5DqQ3cvnLlivnyyy9No0aNTNOmTc3BgwczVefUqVPNhAkTjDHG/PHHH6ZevXrGGGP+97//2Q2K79KlizHGmF27dtkNir9R/sYEAMePHzfVqlUzO3bsMMYY06dPH7N48WJjjDF79uyx24e2bduaLVu2ZCrezHr//fftJgB48cUXU5TJ7L7Gxsaapk2bmi+//DLN7Z49e9ZcvHjRGGPMxo0bTcuWLU1sbKwpW7as9dlcvnzZHDlyxMTGxppy5cqZPXv2GGOMiYqKyvL+7tixwzRq1Mg0a9bMzJ0718TExKQowwQAAOAYupkBcBnGGMXGxiomJkaxsbF2j4SEBCUlJVndjmw2m9zc3OTh4SFvb2+7h4+Pj7y9ve26I+UkX19f9evXT/369dOePXvk4ZG5r94BAwaod+/eql69uvz8/DRt2jRJ18deLFmyROXKlVNgYKDmzZsnSapZs6batWunSpUqycfHR9OnT7err3Tp0vruu+/08MMP63//+1/27OQtePLJJ9W9e3eVL19eJUuWtG5oumjRIm3dulVvvvlmpvd1/vz52rx5sy5evKj3339fklJ0rTt16pT69u2rpKQkeXh4aMqUKfLy8tK8efP0zDPPWAP/33nnHZUvX15z5szRE088odjYWBUtWlQrV67Up59+KkkaNGiQw/vr4+OjGTNmqFKlSrf+4SXjqucHANwKbpoJIE9LTExUdHS0Ll68qOjoaMXHx2dLvZ6envL391dAQID8/f1TDA7PqubNm7vcPU2Q9zh6HLna+QEA2Y2WGQB5zo0xEVFRUbpy5UqOzPIUHx+vqKgoRUVFyWazyc/PT0WKFFFQUJDc3BhOiLyL8wMA/g/JDIA8IzExUWfPntWZM2ey7QqzI4wxunz5si5fvqx//vlHRYsWVUhISJauRvft2zf7A0S+k9pxdDucHwCQ3ehmBsDpEhISdPr0aZ09ezbVmbOcwd3dXSEhISpWrFimx7gA2YnzAwDSRjIDwKnOnz+vEydOKCEhwdmhpMrDw0OhoaGpThcM5DTODwBIH8kMAKdITEzUiRMndO7cOWeH4pCgoCCVLl2arjXIFZwfAOAYkhkAuS4mJkZ//fWXYmJinB1Kpvj4+KhcuXLy8fFxdii4jXF+AIDjSGYA5KrY2FgdOnQoVwcwZydPT09VqlRJ3t7ezg4FtyHODwDIHOZXBJBr4uLidPjwYZf9oSZdn7L28OHDiouLc3YouM1wfgBA5pHMAMgVt9OPnNvhRyfyFs4PAMgakhkAuSI8PFyxsbHODiPbxMbGKjw83Nlh4DbB+QEAWUMyAyDHnTt3TpcuXXJ2GNnu0qVLLjPbFPIuzg8AyDqSGQA5KjExUSdPnnR2GDnm5MmTeeZGhnA9nB8AcGtIZgDkqIiIiNu673x8fLwiIiKcHQZcFOcHANwakhkAOcYYo8jISGeHkeMiIyPFLPfILM4PALh1JDMAcszVq1eVkJDg7DByXEJCgq5eversMOBiOD8A4NaRzADIMRcvXnR2CLkmP+0rskd+Omby074CyF0ezg4AwO0rOjra2SHkmujoaJUoUcKhsidOnMgX3Yvyq+DgYJUuXTrDcpwfAHDrSGYA5JiYmBhnh5BrHN3XEydOqHKVKrpGt5vbVgFfXx08cCDDhIbzAwBuHckMgByTlJTk7BByjaP7GhkZqWtXr+qxsXNUNKxKDkeF3HYm/IC+fb2nIiMjM0xmOD8A4NaRzACAExQNq6KSles6OwwAAFwayQyAHOPm5pZvbpjn5pa5+VTOhB/IoUjgTJn5u3J+AMCtI5kBkGN8fHx05coVZ4eRK3x8fBwqFxwcrAK+vvr29Z45HBGcpYCvr4KDgzMsx/kBALeOZAZAjvH39883P9b8/f0dKle6dGkdPHCA2cxuY47OZsb5AQC3jmQGQI4JCAjQv//+6+wwckVAQIDDZUuXLu3Qj13c3jg/AODW0YkVQI7x9fWVh8ftf83Ew8NDvr6+zg4DLobzAwBuHckMgBxjs9kcGjvg6oKDg2Wz2ZwdBlwM5wcA3DqSGQA56o477pCnp6ezw8gxnp6eKl68uLPDgIvi/ACAW0MyAyBHubu7q1SpUs4OI8eUKlWKaWeRZZwfAHBr+IYBkOOCgoJUqFAhZ4eR7QoVKqSgoCBnhwEXx/kBAFlHMgMgV4SFhcnb29vZYWQbb29vhYWFOTsM3CY4PwAga0hmAOQKT09PVaxYUV5eXs4O5ZZ5eXmpYsWKt/VYB+Quzg8AyBqSGQC55nb4kXM7/ehE3sL5AQCZZzPGGGcHASB/iYmJ0V9//aWYmBhnh5IpPj4+KleunHx8fJwdCm5jnB8A4DiSGQBOkZiYqBMnTujcuXPODsUhQUFBKl26tNzd3Z0dCvIBzg8AcAzJDACnunDhgo4fP66EhARnh5IqT09PlS5dWoGBgc4OBfkQ5wcApI9kBoDTJSQk6PTp0zp79qwSExOdHY6k6/f/CAkJUbFixeTh4eHscJCPcX4AQNpIZgDkGYmJiTp79qzOnDmj+Ph4p8Tg6empokWLKiQkhC4zyFM4PwAgJZIZAHlOUlKSzp07p3Pnzuny5cvK6a8pm82mggULKigoSEFBQQ7fsTwxMZEfdMi0Wz1uXOX8AIDcQDIDIE9LTExUdHS0Ll68qOjo6Gy7Iu3p6Sl/f38FBATI398/0z8uN2/erF69e6tN6zZ699135Ovrmy1x4fZ19epVDRs2XCt/Wak5s2erQYMGt1xnXj0/ACC3kMwAcBnGGMXGxiomJkaxsbF2j4SEBCUlJVlXqW02m9zc3OTh4SFvb2+7h4+Pj7y9vWWz2TIdQ3x8vMaPH69x48apaNmqijxxRGFlyuibr+eqbt262b3LuE1s27ZN3Xs8rmPHjyu4dAWdObpfI0eO1MiRI7NtzEleOD8AILeRzACAg86fP682bdtp+/Ztuq//62rxxGuK+vuI5r/+uCL+3KMxY8bo5Zdf5io2LImJiXrrrbc0evRo3VGhph59c46K3Fleq6eP15ovx6lu3Xr6ecVyFS5c2NmhAoBLIpkBAAcdOXJEFStWVMnKddVn8mL5h5SQJCXEx+mXz0dr/cy31aBhI82dM1thYWFOjhbOdvToUT3es5f++H2zmvV5RS0HjpKHp5ckKfrsP5r5n446dXC7Dh8+rAoVKjg5WgBwTYziAwAHVahQQcuXL1fChX/1frca2rPqe0mSh6eX2g2eoIGfr9PhY6dUo2ZNffXVVzk+MBt5kzFGM2bMUM1atXTkxL8a+Pl6tR083kpk9vyyQO93q6GEC/9qxYoVJDIAcAtomQGATIqKitLAgU9p4cLvVbdDHz047AP5FPSXJMVcjtbiSc9r2+Kv1LlzF33++WcKDg52csTILZGRkXryyYH68ccfVK9jX3Uc+r7dsbHovee0fclMde36sD777FMVKVLEyREDgGsjmQGALDDGaNasWRry7LPyLlREj7w5W2Vq32u9v2fV9/pxwkD5+Xhp1syv1LZtWydGi9ywfPly9enbT1dj49Xp1c9V/b4u1nvhOzZowajeir0UpY+mTlWvXr0YYA8A2YBuZgCQBTabTX369NHuXbtUKayUPh/YTMs/elUJ8XGSpBotu+r5eXtUuGwttWvXTkOGPKurV686OWrkhKtXr2rw4CG6//77VbhcbT03b4+VyCTEx2n51BH6fGAzVQorpT27d6t3794kMgCQTWiZAYBblJiYqHfffVevv/66NWNV0bAqkq634Gya/5F++mA4UzjfhrZt26Yej/dU+LFjeuD599TwkWesROX00f367o2eOv3XXr355psaPnw4M90BQDajZQYAbpG7u7teeeUV/f777/Iz1/Rhz7r67dupMsbIZrPpnseG6Nk523XVVkANGjTQxIkTlZiY6OywcQsSExM1YcIENWzYUFfdfPXsnO1q9Ohg2Ww2GWP067wPNbVXPfkpRr///rteeeUVEhkAyAG0zABANrp27ZpeeullTZ36oSo1aquub3yZ6hTODRvdozmzZzGFswsKDw/X4z176ffNm1KdcnnBmH46vPlnDRnyrN55520VKFDAyREDwO2LZAYAcsCKFSvUp28/Xb4Wq06vfq4aLbta7x3buVHfvdFL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aZK1/po1a4wks2bNGrvthIeHG0lmxowZ1rI+ffoYLy8vU6VKFfP+++/b7feIESNMxYoVzfTp082KFStMhw4djCSzdevWDD/nm/fp4MGDplChQqZcuXJm1qxZZunSpaZ79+5Gknn77bdTxH7nnXeahx56yCxevNjMmTPHlC9f3vj7+5u//vrLKlupUiVTvnx5M3v2bLNu3Trz/fffm6FDh6bY7+QuXbpkPD09zYQJE6xlgwYNMgUKFDB+fn4mLi7OGHP9KrjNZjMff/yx3T6VKVPG3H333Wb+/Plm2bJlpnnz5sbDw8Murn379pmAgABTo0YNM2vWLPPzzz+boUOHGjc3NzN69OgU+1qmTBnz+OOPm6VLl5pvvvnGlC5d2lSoUMEkJCSk+xnfd999JigoyCxfvtzUqlXLuLu7m5CQEPPUU0/ZtcAcPHjQSDIfffRRijqGDRtmbDabuXbtmjHGmFdeecVIMpcvX05RNjg42HTv3j3dmG5umdmzZ4+58847TaNGjczZs2etcjl13PXt29dMnz7drFy50qxcudKMHTvWFChQwIwZM8auXGhoqClZsqQpXbq0+fLLL82yZcvM448/biSZd9991yqXmfNdkilZsqSpWbOm+frrr83q1avNzp07zfLly40k079/f7Np0yazadMm8+eff9qt16xZswz3LTUXLlwwAQEBpnPnzinemzZtmgkICDCnTp2ytnNzy0x8fLypV6+eqVatmvnjjz/MpUuXzLZt20zt2rVN3bp1rfMBgPOQzACwcyOZSf7j3xhjateunaJbRXx8vAkJCTFdunRJs76EhAQTHx9v+vfvb+rUqWP3XkxMjKlTp44JCwsz+/fvN8WKFTPNmjVL8SM1rWTm2WeftSvXqVMnI8n897//TRF73bp1rdeZTWZuTthu7Lcks337dmt5VFSUcXd3Ny+++GKan0da+9StWzfj7e1tTpw4YVfu/vvvN76+vubChQt2sdetW9ckJSVZ5W50iRowYIAx5np3KElmypQpGcZys3vvvdfcd9991uvy5cub4cOHGzc3N7Nu3TpjjDFz5841kszhw4ft9qlYsWImOjraWhYREWHc3NzMxIkTrWVt27Y1pUqVsksojDFmyJAhxsfHx5w7d85uXx944AG7cvPnzzeSzKZNm9Ldj0qVKhkfHx9TqFAhM2HCBLNmzRrzzjvv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "create_base()\n", + "plt.text(1.5, 4, 'Max IOPS for Coherent Filesystem',fontsize=24)\n", + "plt.text(3.3, 3.3, '|<-- 600km == 0.002 sec. -->|', fontsize=7)\n", + "plt.text(2.5, 1.0, f\"maximum iops when {distance} km apart: {int(max_iops)}\", fontsize=12)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6ea5f011", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Pick any 2, Which two?\n", + "\n", + "* CA: Consistency and Availability\n", + "* AP: Availabilty and Partition Tolerance.\n", + "* CP: Consistency and Partition Tolerance" + ] + }, + { + "cell_type": "markdown", + "id": "26bebd15", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing CA:\n", + "* At best, you need a quorum system.\n", + "* quorum means obtaining a consensus among the existing nodes about the current state.\n", + "* usually means voting for primaries (at best, sharded masters) and triggering a pause to vote for a new primary whenever it goes down.\n", + "* The system as a whole, cannot respond to queries while voting is in progress, and so is unavailable.\n", + "* but at least it is up after the vote is done, in spite of the formerly primary node dying.\n" + ] + }, + { + "cell_type": "markdown", + "id": "b843815d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Quorum and Voting:\n", + "* Multiple Algorithms:\n", + " * Paxos: \"Paxos is exceptionally difficult to understand\"\n", + " * \"few people were comfortable with Paxos even among seasoned researchers\"\n", + "\n", + "* papers like \"In Search of an Understandable Consensus Algorithm (Extended Version)\" resulted in RAFT\n", + "\n", + "* Things you want:\n", + " * quickly convergent?\n", + " * a single leader elected?\n", + " * low overhead once leader in place.\n", + " * quick detection of loss of leader.\n", + " * known behaviour when: flapping, partitioning, WAN.\n", + "\n", + "* If it partitions, tradeoffs:\n", + " * AP: both sides will elect new leaders and \"fork\" the file system (writing inconsistently thereafter.) OR:\n", + " * CA: One side will know it has a minority of nodes and shutdown (losing a lot of nodes, but hoping the other side is still up.) OR:\n", + " * CP: both sides refuse to write. (no A for writing.)\n", + " \n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "1a58bd02", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing AP:\n", + "\n", + "* incoming requests are routed to the nodes that remain up. They provide the data they have.\n", + "* you don't need a master. nodes receive updates, and tell the others, \n", + "* changes propagate through the cluster... eventually consistent.\n", + "* no voting, no loss of availability." + ] + }, + { + "cell_type": "markdown", + "id": "9499457e", + "metadata": { + "slideshow": { + "slide_type": "skip" + } + }, + "source": [ + "### Voting Failure Modes\n", + "\n", + "\n", + "| Number of Masters | < 1 | 1 | >1 |\n", + "|-------------------|----------------------------|--------------------------|--------------|\n", + "| All Talk | Down wait | Up OK | Bickering |\n", + "| Partitioned | Unstable LR | Unstable LR | LR |\n", + "\n", + "(LR... partitioned into left and right clusters.)\n" + ] + }, + { + "cell_type": "markdown", + "id": "c2fb9b08", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "### Choosing CP:\n", + " * as long as the backups can reach the primary, all can answer read requests.\n", + " * when a write is received, the backup passes it to the primary.\n", + " * when a backup loses access to the primary, for coherency, it must stop answering. It no longer knows.\n", + " * A Manual intervention would be required to tell the backup that it is now the primary in order for availability to be restored, because there is no way to tell the difference between a primary failure and a network partition.\n" + ] + }, + { + "cell_type": "markdown", + "id": "a1b6bb55", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Bringing Amdahl & CAP Together\n", + "\n", + "* Amdahl's Law is about dealing with large numbers of processors sharing resources.\n", + " \n", + "* CAP is about tradeoffs inherent using different methods of sharing resources." + ] + }, + { + "cell_type": "markdown", + "id": "f335b812", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Almost-Coherent File System or DB Performance\n", + "\n", + "* A bandaid on Traditional single node \n", + "* a synchronized file system is going to have ordered operations, it is going to be a 1:1 connection with shared state.\n", + "* usually a \"journal\" to sync between sides.\n", + "* in the meantime, readers on the wrong side get the _wrong_ data. \n", + "* sometimes you establish an upper bound (if you hit it, you hang so that it doesn't get too out of sync.)\n", + "* synchronization is a struggle against Amdahl's Law.\n", + "* will not scale." + ] + }, + { + "cell_type": "markdown", + "id": "58fa5584", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Universal Write Scaling\n", + "\n", + "* obtain delegation for a subdomain, write locally.\n", + "\n", + " * write behind cache ... the write will get to the other side eventually, but G1 and G2 will be out of synch for a while. \n", + "\n", + " * sharding (dividing domain so every node is writing locally.) \n", + " * route requests to appropriate shard.\n", + "\n", + "* locks over smaller subsets --> more write parallelism.\n", + "* writes must be localized (no solution for scaled distributed writes.)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "3df85e5c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Non-Shared Approaches.\n", + "\n", + "* the only thing that does scale.\n", + "* transfer files or objects, rather than synchronizing file systems. maximally distributed synchronization.\n", + " * explicit sharding.\n", + "* any number of transfers can occur in parallel.\n", + "* the underlying file systems are local stores.\n", + " * are not the same, they just contain the same files, eventually.\n", + " * are not synchronized at any point, they receive a sum of changes that add up to the same stored state.\n", + "* lag can occur, but can run an unlimited number of processes to do transfers, none limited by propagation delay. Transfers occur in parallel to the degree desired.\n", + "* Because the transfers are independent, no fundamental performance limit, beyond the perfomance of hardware on each end, and the pipe between.\n" + ] + }, + { + "cell_type": "markdown", + "id": "4b5061d6", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "## Examples of Non-Shared Approaches\n", + "\n", + "### Object Stores\n", + "\n", + "* An object is a self-contained item that can be distributed.\n", + "* Every object is equivalent to a db or a file system. \n", + "* with many objects, state is distributed among subsets of nodes, not global.\n", + "* distribution of objects (with lag) is much easier to achieve than a coherent view.\n", + "* Different Object Stores lean in different directions in CAP.\n", + "* requires application adaptation, perhaps profound.\n" + ] + }, + { + "attachments": { + "CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg": { + "image/jpeg": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUFBQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh7/wAARCAIiAtgDASIAAhEBAxEB/8QAHAABAAIDAQEBAAAAAAAAAAAAAAUGBAcIAwEC/8QAWhAAAQQBAgQDBAYEBg0HCwUAAQACAwQFBhEHEiExE0FRFCJhcQgVMkKBkSNSYqEWM3JzgrEJJDQ3Q1N0g5KisrPBFzhjdZOj0iU2REVXdpSVtNHwGDVUwtP/xAAbAQEAAgMBAQAAAAAAAAAAAAAAAQMCBAUGB//EADQRAQACAgEDBAAEAwcFAQAAAAABAgMRBBIhMQUTQVEGImGBFDJxFSMzQpGhsVLR4fDxJP/aAAwDAQACEQMRAD8A4yREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREF+4HcLs9xY1kNP4WSKtHFH4925MCY68W4G+w+04k7BvTc+YAJG989gvokcM8k/T2fbndYZattFcdBPI5scn3usb42Ag92guLex6hS/0dJXcPvoX6y19j2Mgy9185htbe8OXlgh/wBGRzyPi4rjVzi5xc4kknck+aDoXivoXgZmeGuT13wl1RPTtYsxOtYK9IS8sfK2P3BJ7+4LwdwXtO23QrnhFeOBWjXa94tad0sYy6vattdb28q7Pfl6+XuNIHxIQdT8J/o08OMnwkwtbVNZ7NbZ3EzZCCU3JWOhaeXlIiDg0iMTQ8wIPVx/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hMhmPB1FkLsslSl7PK4ytcIADzhpYPsO7kdviFo7RlytjtYYXIXH+HWq5CCaZ+xdysbI1zjsOp6A9kHQH9kZAHHDE7DbfTkG/wD8RZXM63n9NXXeleIXFXHZnSGVGToQYWGrJMIZIgJRNO8t2e1pPR7eu23VaMQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQERTeldKal1VadW03gcllpGDeQVKzpBGPVxA2aPidghraFRbgw3AbOH39TaiwOBaDs6Bs5u2Nvg2AOYD8HPaVbMZwl4aY4A3LGpdQStP2jJFQiPzYBK4j5PCqtnx18y3sPpvKzfyUlzn5ourqGE0RjC04zh9p2N7dvftMluFxHmRPI5n5NA+Gyl62YsUz/5Op4jG9Nv7QxVat09P0bG+g/IKmebjjw6WP8Ocq380xDj+OCeRu8cMjx23a0lZowOdI3GFyRHwqv8A/suvn6w1Y/vqfM/hekH9RWMdQ59zi52cyZJ6km2/r+9YTzq/EL6/hnL83hyDYx9+u8ssUrMLmjctfE5pH5hY23w2XZsWrNUxNDY9SZhjW9mi7IB+XMvzb1Lmro2yNwZEbbEXoWWRt8RI126n+Or9MZ/DOf4tDjT8UXWt6ppvIsDMpofSloDrvHi2VHH5ureGT+agMnw34XZMk/UOYwjyOhxuT542n1LJ2vcR8PEHzVleXjlq5PQOZTxET/SXNP4pst25bgRVmHNpnXNGZ2xPs+XqPpvJ8mtewyxn5ucwKhav4Za60rWdczGnbYoN6e31i2zV/wC2iLmA/Anf4K+uStvEuXl4ubDP95WYU5ERZKBERAREQERe1StYuWY6tSvLYnkPKyOJhc9x9AB1JQeS+LZeL4Ka3mAkzkWP0rCRuTm7IglHTf8AucB0/wD3asNDhboShscxqnMZqQfaixdNtWI/KaYud+cIVd81KeZRMxDSaLoitiOHePj5KHD6pZcO02VyFixJ+Ubo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jOzmOHmFfgz2xT+iYnTnNfFubjHoullcZY1/papHW8NzTnsXC3ZtZ7jt7VC0DYQPcQC3/BvO32XN20yu1S8XjcLYnYiIsgREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERB9CeSLbf0d9F1snkJ9aZ+rHYwmFka2GtMzdl+6RvHCR2LGj9I/4BrT9sKLWisblZixWy3ilY7yvfB7R/wDAHTjM7fjaNVZitvC1w9/GU5B/qzStPXzbGduhe4CeXtetWL1ya5bldNPO8ySPd3c4ncleK4ubLOS25fR/T+DTh4YpHn5kREVTfEREBERAREQEREBERAREQEREBEWbgsZYzGVgx1Ysa+V3V7zs2NoG7nuPk1oBJPoE8sb2ilZtbxCU0jia0rJc3lo3OxdNwaYwdjZlPVsQPp03cfJo9SF+8tfs5PIS3bTgZJD2A2a0AbBoHkAAAB5ABZeobtWV8OPxgczF0WmOsD0Mh+9K79p56n0Gw7AKKWtmybnpjw8fy+VPJydU+PgREVDVEREBERAREQfqWvWyNGTF3jywyHmil23MEnk/5eRHmPiAtd5KnYx16albj8OaF3K8f8R6g9wfMLYSw9UY4ZfEmzG3e/Rj36d5YB1I+be4/Z39Arsd/iXP5vH3HXVQURFe5YiIgIiICIiAiIgIiICIiAiIgIiIM7B5Szh8lHeqiN5aHMkilZzxzRuBa+N7T0cxzSWkeYK1jxo0RW07frZ/TzXu0xmC51QFxe6nMOslSQ/rM3BaT9pha7vzAbAUpiDi79C9pfURP1HlmtZNIG8zqkzd/CssH6zCTuB9pjnt+8tri5/btqfEsqzpzMil9YaeyWldTX9PZeNrLlKUxvLDux47tew/eY5pDmnza4HzUQuysEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQSemcLkdR6hoYHEwePevzsggZvsC5x23J8gO5PYAErqyerjMNjKGl8E7nxWHjMMMvLsbUpO8thw9Xu6j0aGN+6tefRwwAw+nMjruzGBcumTF4guHVjOUe0zD091zYgf25PMK8rn8zL/kh678O8HUTyL/ALCIi571giIgIiICIiAiIgIiICIiAiIgIiICuNKI4DSzT9nJZmPmd5GKpv7o+chHN/Ja39ZQ2kMX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ucDuBJKGck9Jx7R2Y9z4bvIO3LHfdceoEQt+totG4cS9LUnVoERFkwEREBERAREQEREBERAREQEREBERAREQFn4PE381fFLHwiSQMdJI5zgyOKNo3dI97iGsY0dS4kAL3xuGY7Fy53NX4cLgIHcsuQsAkPd/i4WD3ppf2G9u7i0dVrviPxKdmMfJpnStWbD6ZLwZWPcDayDmndr7Lx0I36iJvuN6facOY0Zc8U7R5bnG4lss7ntCa4jcQqGJxtrSOg7ZmbZjdBmM41pa640nrBX36sr9OrujpPPZvunTy+r4tC1ptO5dulK0jVRERYsxERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQTGlNSZzSuYZltP5Kahba0sLmbFsjD3Y9p3a9h82uBB8wtw6f1ho7WobDcNTR+oXdw9xGLtu/ZcdzWcfR28f7TBsFodfFnS9qTuFWXDTLGrQ6DzeIyWFueyZSnLVlLQ9ocNw9p7Oa4dHNPk4Eg+RWCqFojibqTTFEYh3s2awXNzHFZNhlgaT3MZBD4XdftRuaT579lsbD5zQOq+UYrLnTORf3x2alHs5d6R2wA3b+dbHt+s7utzHyIt5crNwL0717wxkUjnMFl8JLHHlcfPVEreeF7hvHM39Zjxu17fi0kKOWxExLQms1nUiIilAiIgIiICIiAiIgIiICLOwmHyubueyYjHWb0+25ZDGXco9Tt2HxPRfMxf0RpRp/hFnxlsg3/1VgpGTEH0ls9Yo/6HikeYCwvkrXzK7Hhvkn8sPxicdfy1+OhjKc9y1IfcihYXuP4Dy+K+5/UGjdCgtvS19VagZ2xtObejWdt/6ROw/pCD3jiO3QgyNPRa/wBZcVc/m8fPhcTDBpvAzdJKGOLgbDfLx5SS+b5OPID1DWqgFaeTkTbtXs6mDgVp3v3lP621hn9ZZRt/PXjOYmeHWgjaI4K0flHFG3ZrG/ADqep3JJUAEXxazoeBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREBERAREQEREFq0dxA1fpKJ1bB5ueKjId5aEzWz1JT6ugkDo3H4lu/wAVecbxV0pk+WPVGj5MbKdgbun5yB8XOrTFzXH4NkjHwWnUHdZVvaviVd8VL/zRt0JQGkM0R/B/XeHe8jcVstvjph8zJ+g/KUrNvaP1PTq+2SYS5JTJ2Fuuzx4HfKVnMw/gVzcVnYbL5bC3G3MPlL2OsMO7Zqlh0Tx8nNIKvrybR5al/T8c+OzcxBBII2I7goqXU4y8SIf7p1I7LAjY/W9SDIEj52GPKz6/GfKkk5DSGjciTv1fj5K/XfffavJGPX4dfltbHKj5hrT6bb4ssqKBPF7HSAeLw3wDCPOvdut3+fPM792y+f8AK1iv/Z3i/wD5ha/8an+Kow/s7L9wn0UCOLuNYCY+G+Ckcf8AH3rjmj8GytO/4ryscZr+22P0To7Hnyc2pPYPcd/HmkHlt28yn8VVMenZPtY1NY3SepsjXNqngsg+qPtWTA5sLfi6R2zR+JWs7XGjiNIA2pnYcQBvscTjq1Fw/pwRtd+9VDPagz2oLPtOdzeSys/+Mu2nzO/N5JWE8r6hdX03/qs3nfr6Zwu/8I9c4Cm9vevRmORnPwHs/NGD8HyNVfyHE/ROKaW6e0rdztgdrOcn8GH5+zwO5vzmI+C0yiptnvb5bWPh4qfG1v1jxJ1lqmp9X5LLuhxYO7cbRiZVqA+RMMQa1x6/acC74qnr6iq22YiI8PiIihIiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIgIiICIiAiIg/9k=" + } + }, + "cell_type": "markdown", + "id": "5c040b89", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "![CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg](attachment:CAP-Theorem-is-a-concept-that-a-distributed-storage-system-can-only-have-2-of-the-3.jpg)\n", + "\n", + "* Source: Fotis Nikolaidis from \"Tromos : a software development kit for virtual storage\n", + "systems\" (PhD thesis 2020/01/17)" + ] + }, + { + "cell_type": "markdown", + "id": "2f097463", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "## Cloud Solves This!\n", + "\n", + "* cloud vendors talk about \"availability zones\" there is no HA or synch across availabilty zones, they talk about times like 15 minutes, to do periodic synching. Object syncing takes time (and money.)\n", + "* 2006, HP DC Consolidation project established 200 miles as the longest distance between Data Centres. So that would give them 500 IOPS or so on a synchronous store.\n", + "* Note, putting a file system or a db inside a single object store means no scaling. \n", + "* Individual objects don't scale, using many objects does." + ] + }, + { + "cell_type": "markdown", + "id": "d4c68303", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "from: https://aws.amazon.com/rds/features/multi-az/\n", + "\n", + "* *Failover time depends on length of replica lag*\n", + "* *Any data updates that occurred after the latest restorable time (typically within the last 5 minutes) will not be available*\n", + "\n", + "From Google: https://cloud.google.com/compute/docs/disks#repds\n", + "* *Regional persistent disks have storage qualities that are similar to zonal persistent disks. However, regional persistent disks provide durable storage and replication of data between two zones in the same region.*\n", + "* *Zonal SSD PD multi-writer mode Throughput per GB (MB/s): 0.48*\n", + "\n", + "\n", + "the google performance numbers are within a zone (multi-DC but not too far apart.) not multi-zone.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2f016ef3", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "### Sarracenia: File Systems Flying in Formation\n", + "\n", + "* Another distributed approach\n", + "* transfers files between file systems without synchronizing.\n", + "* Every file is equivalent to an object, a db or a file system.\n", + "* no locking, architecturally unlimited scaling.\n", + "* generalized buffering and parallelism of transfer at scale.\n", + "* file systems at all nodes are totally independent, limited only by local hardware on each node. \n", + "* transfers limited by bandwidth, not latency.\n", + "* works with legacy code un-changed, or new (agnostic.)\n", + "* In terms of CAP--> heavily AP.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7ee8541d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Thanks!" + ] + }, + { + "cell_type": "markdown", + "id": "23f98a0c", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Sources:\n", + "\n", + "* https://mwhittaker.github.io/blog/an_illustrated_proof_of_the_cap_theorem/ (An Illustrated Proof of the CAP Theorem, referring to Gilbert & Lynch's paper which is a dead link. )\n", + "* https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing\n", + "* https://www.youtube.com/watch?v=eWMgsk7mpFc&ab_channel=IBMTechnology\n", + "* https://eax360.com/distributed-coordination-algorithms/ - Distributed Coordination Algorithms\n", + "* https://raft.github.io/raft.pdf In search of an Understandable Consensus Algorithm (Extended Version) Diego Ongaro and John Ousterhout.\n", + "* https://www.cs.ubc.ca/~bestchai/teaching/cs416_2020w2/lectures/lecture-mar23.pdf - fun practical talk about practical CAP.\n", + "* https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf - Impossibility of Distributed Consensus with One Faulty\n", + "Process - MICHAEL J. FISCHER, NANCY A. LYNCH, MICHAEL S. PATERSON\n", + "\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/PDS_Algorithm.ipynb.txt b/_sources/Contribution/Philosophy/PDS_Algorithm.ipynb.txt new file mode 100644 index 000000000..7e58ed10c --- /dev/null +++ b/_sources/Contribution/Philosophy/PDS_Algorithm.ipynb.txt @@ -0,0 +1,246 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0cc09567", + "metadata": {}, + "source": [ + "PDS Algorithmic Design\n", + "====================\n", + "\n", + "The PDS (Product Distribution System) is an obsolete data transfer engine gradually being replaced, but still deployed in a few Storm Prediction Centres (offices of Environment and Climate Change Canada.) other systems place files in input directories, and PDS has an array of configurations identifying which other destinations files need to be sent to.\n", + "\n", + "It is a sort of inspiration for Sundew. The lack of source code meant that it's functionality was deduced and reverse engineered to to produce later products.\n", + "It is a design of it's time, the 1980's for use on large UNIX servers.\n", + "\n", + "\n", + "* pdsReceiver, scans a directory feeds things to dispatcher.\n", + "* pdsDispatcher, scans sender configurations, and queues files for senders.\n", + "* pdsSender... does the sending.\n", + "\n", + "The initial deployments were on HP-UX HP-9000 UNIX servers, but it was ported to Linux in the early 2000's, without substantial changes." + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "id": "b43b0c60", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU5f7+8XvTQ4CEBAKhWpCmKCBNDlIEQYoewQLqUfGIHewNC4JIs4EH7HpEj2JBRAELoPQmVVCqlNDSE9KzKZv5/cGX/Ai7CbvJJjNJ3q/ryqU8uzPz2c2k3fPM57EZhmEIAAAAAAAAAABYio/ZBQAAAAAAAAAAAGcE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAAAAAABYEAE+AAAAAAAAAAAWRIAPAAAAAAAAAIAFEeADAAAAAAAAAGBBBPgAAAAAAAAAAFgQAT4AAAAAAAAAABZEgA8AAAAAAAAAgAUR4AMAAAAAAAAAYEEE+AAAAAAAAAAAWBABPgAAAAAAAAAAFkSADwAAAAAAAACABRHgAwAAAAAAAABgQQT4AAAAAABAEyZMkM1mK/YBAADM5Wd2AQAAAFVFWlqatm3bpmPHjiktLU3p6eny9/dXSEiI6tWrpxYtWui8885TkyZNzC7VLYcPH9b+/ft19OhRpaWlyW63F72WevXqqU2bNmrVqhUBDgAAAACYhAAfAIAaLDo6Wueff77bzw8KClJoaKhCQ0PVqlUrXX755bryyivVt29f+fh4dmOfu6Gwr6+vAgMDFRgYqPDwcEVGRqp58+Zq3bq12rdvr3/84x+Kiory6NieOH78uD755BPNnTtX+/btk2EY59wmPDxcl19+ubp27aoBAwaoR48e8vMz/9eu/Px8/fDDD/r222+1fPlyJSYmnnOb0NBQde7cWf/85z81cuRINWjQoBIqBVCT9enTR6tWrTrn82w2W9HPh9DQUEVGRqpx48Zq1aqV2rVrpyuuuEJt2rSphIoBAAAqjs1w569QAABQLXka4JfkvPPO05gxY/TII4+4HVR7c1Z3mzZtdOONN+r2229Xq1atvLLP7OxsjR8/XjNnzpTD4SjXvurWratFixapV69eXqnNU/n5+Zo9e7Zef/11xcTElHk/fn5+GjJkiCZMmKAOHTp4r0AApho1apQ+/fTTon+3aNFC0dHRptXjboDvjsjISA0bNky33nqrad+Dq5IJEyZo4sSJxcaIDAAAMBc98AEAQLlFR0frySefVPfu3fX3339X+vH37t2rV155RW3atNE///lP/fXXX+Xa39GjR9WhQwe98cYb5Q7vJSk9PV0pKSnl3k9ZbN26Ve3bt9fjjz9ervBekgoKCvTDDz+oU6dOuv3225WcnOylKgGgYiQkJOj9999X79691alTJy1atMjskgAAADxi/r3cAADAUkJCQtSyZUuXj2VnZyspKUknT550+fjWrVvVv39/rVu3Tk2bNvXouP7+/mrXrp3Lx7KyspSWlqa0tDTl5eWVuA/DMLRw4UL99NNPevLJJzVp0iSPW9fExMSoT58+Onz4sNNjPj4+uuKKK9SlSxe1atVKoaGh8vf3V0pKipKSkrRz505t2bJFhw4d8uiYFeV///ufRo8eXeJ7Vrt2bfXu3VsdOnRQgwYN1KBBA/n6+io9PV2HDx/Wjh07tHbtWqWnpxfbzjAMff7557r77rvVp0+fSnglAGq6tm3bKiAgwGk8Ly9PJ0+eVFpamnJyckrdx/bt23Xddddp6NCh+uSTT1S/fv2KKhcAAMBrCPABAEAxnTt31sqVK0t9zsGDB/XVV19p5syZSkpKKvbY0aNHddNNN2nDhg0eHbdx48b6448/zvm8kydPavPmzdq0aZNWrlyp5cuXO93eX1BQoGnTpun333/XwoULVbt2bbfrePDBB53Ce5vNpvvuu08vvPCCWwvUHjlyRN99953mz5+vdevWuX1sb/rggw90//33u2x90L17d7300ku66qqrXAZiZ8rPz9eSJUv07rvv6ueff6aVAgBT/PTTTzrvvPNKfY7dbtcff/yhTZs2af369frhhx9kt9udnrd48WJ16tRJy5YtU+vWrSuo4qppwoQJmjBhgtllAACAM9BCBwAAeOzCCy/U888/rz///FNdu3Z1enzjxo2aN29ehRy7Xr16GjBggF544QX9+uuv2rNnj8aMGSN/f3+n565YsUJDhgwpddb+mVauXKkffvih2JiPj4++/PJLvfvuu26F99Kp/tGPPfaY1q5dq507d+ree+9VSEiIW9t6wy+//KIHH3zQKWyvU6eOvvnmG23YsEHXXHPNOcN76dSdEUOHDtWPP/6oLVu2qGfPnhVVNgCUS1BQkLp3766HH35YX331lY4fP65p06a5XHz72LFjJd5tBQAAYCUE+AAAoMwaNWqkxYsXq1GjRk6Pvf/++5VSQ+vWrTVr1iytXr1aLVq0cHp89erVevLJJ93a1xdffOE0NmbMGI0YMaLM9bVv317vv/++rr766jLvwxMJCQm64447nHr3N2rUSKtWrdJNN91U5n136tRJq1ev1muvvebyggkAWElERISeeeYZ7dixQ3379nV6PC4uTjfddJPbF3kBAADMQIAPAADKpUGDBnr66aedxteuXavs7OxKq6N79+7avn27WrVq5fTY7NmztXHjxnPu4+eff3YaGzt2rFfqqyxPPvmkEhMTi40FBATol19+UceOHcu9f5vNpieffFK//PKLQkNDy70/AKhoUVFR+vXXX11ejN26datmzJhhQlUAAADuoQc+AAAot+HDh+vxxx8vNpabm6tdu3apS5culVZHvXr1tGDBAnXr1k2ZmZlF44Zh6Omnn9bq1atL3LagoEAxMTHFxurWrVvigr5W9Ndff7m8i2DSpEm67LLLvHqsq666qszb5uXladOmTTp+/LgSEhKUlZWliIgIRUZG6uKLL9ZFF13kxUpL9vfff2vbtm06fvy47Ha76tatq7Zt2+qKK65wu+WRYRjasWOHduzYoYSEBDkcDjVs2FDt2rVT165dZbPZKqz+goICbdmyRbt27VJSUpJ8fHwUFRWl888/X927d5evr6/Xj+lwOLRt2zYdPnxYiYmJSktLU3h4uBo0aKBWrVqpffv2Xj+mK4mJidq4caMOHTqkzMxMhYaGKjIyUt26dXN5J443FBYWavv27YqOjlZiYqJSUlJUt25dNWjQQC1btlTHjh3l41Ox86Py8/O1adMm7d69W0lJSfL39y9677t27Vohn/PqwsfHRx9//LF27dqlv/76q9hjkydP1v3331/ui5JJSUnasmWLEhISlJiYKIfDofr166thw4bq3r27IiIiyrX/MzkcDv3999/6888/lZiYqPT0dDkcDtWqVUt169ZVs2bNdP755+vCCy+s8POyNDk5OVq3bp2OHz+uuLg4+fr6qlGjRmrfvr0uu+yyCv0e6crff/+trVu36sSJE8rNzVVERIQaN26snj17ql69epVaCwAAbjMAAECNdfjwYUNSsY/evXuXaV8hISFO+/rxxx9LfP7Zz23RokXZXoQLb7zxhtP+JRlbtmwpcZuYmBin50dFRXmtpsowevRop9dw0UUXGQ6Hw+zSDMMwjGXLlhnXXXedUbt2bZefn9MfF1xwgfH4448bsbGxZTpOixYtiu3vzjvvLHqsoKDAeO+994w2bdqUePzatWsbY8eONZKTk0s8Rnp6ujFx4kSjSZMmJe6nYcOGxuuvv27k5+d7VL+rr8tPPvmk6PG4uDjj0UcfNcLCwko8dmRkpDFmzBgjISHB07fPpc2bNxsjR440wsPDS/3cNW7c2LjnnnuMAwcOlOk4vXv3LvX70YoVK4yrr77a8PHxKbGGdu3aGZ9//rlRWFjohVduGKtXrzZGjBhxztceHh5u/Otf/zJ27drl8TE++eQTp/0dPny46PHY2FjjkUceMUJDQ0s8flhYmPHoo48aSUlJbh3z7K+TsnysWLHC49fqjrPPg7Pfj/LYunWry9fyxhtvlGl/aWlpxuTJk43OnTsbNputxPfKZrMZnTp1Mt5++20jNze3zPVv2bLFuPvuu0s9F878qFu3rtG/f39jxowZxrFjx865/5deeslpH2Xx119/GTfddJPL3w3O/H7x8ssvG5mZmeU+/tnbvPTSS0WPORwO47///a9xySWXlFiLr6+v0a9fP2PDhg1ler0AAFQkAnwAAGowbwb4jRs3dtrXF198UeLzz36uNwP89PR0l+HGY489VuI2KSkpTs/38/MzsrKyvFZXRcrJyXEZjJc1lPKmgwcPGgMGDPA4HAwJCTFefvlljy9AlBTgnzhxwujWrZvbx2/atKmxY8cOp/2vW7fOaN68udv76datm3Hy5Em36y8twP/xxx/PGSSf+REREWF8+eWXHr1/Z0pMTDRGjBhRajDp6sPf3994+OGHDbvd7tHxSgrw7Xa7ywtUpX0MGDCgWDDoqX379hmDBg3y+Lz18fEx7r77biMnJ8ftY5UW4H/77belXqxx9Tl3J4SsqQG+YRhG3759nfbfsWNHj/ZRWFhozJw504iIiPD4fWvRooXx22+/eXQ8u91u3HvvvaVevHLne9G5lDfAdzgcxnPPPWf4+/t79H6cvsDu7QD/2LFjxhVXXOHR+/Tcc8959JoBAKhotNApg61bt5pdAizm8ssvN7sEADBdWlqa01hYWFjlFyKpTp06uuuuuzRz5sxi44sXL9abb77pcpuwsDAFBgYqNze3aKygoEALFizQbbfdVpHlesXy5cuLtQ2STvW+HzVqlDkF/Z+tW7dq8ODBSkhI8HjbrKwsjR8/Xjt27NDnn3+uoKCgMtcRExOjf/zjH4qOjnZ7m+PHj2vAgAHavHmzmjVrJklaunSprr/+euXk5Li9n99//13XXHON1q5dKz+/sv/6vWjRIg0fPlwFBQVub5OcnKxbb71V6enpuvfeez063qFDh3TNNdfo77//9rRU5efn6z//+Y+2b9+uH374oVytKex2uwYPHqwVK1Z4tN3SpUs1ePBgLV++3OPWMr/99ptuvPFGpaamerSddKrVzscff6w///xTCxcuVMOGDT3ex2nvvfeeHnzwQRmG4fY2ycnJ6t+/v9auXasOHTqU+djV2SOPPOJ0Pm3fvl0nTpxQkyZNzrm93W7XnXfeqW+++aZMxz9y5IgGDhyod955R/fcc885n5+Xl6chQ4bot99+K9PxKkthYaFGjRql//3vfx5td+TIEfXu3VsrV670aj2HDh1Sr169dOLECY+2mzJlimw2m1555RWv1gMAQFkR4AMAgHI7cuSIsrKynMYbNGhgQjWn9OvXzynA//vvv5WYmOiyLpvNpiuuuMIpQHjyySfVtWvXSuvLXlauws0OHTooPDzchGpO2bNnj/r06eN0YUGSLrroIg0bNkwtW7ZU3bp1FRcXp02bNmnhwoVOz58/f75yc3O1cOHCMvVLLigo0PXXX18U3ttsNl155ZW6+uqr1axZMwUGBur48eP65ZdfnAKy+Ph43X///frxxx+1Z88e3XjjjUXhfXBwsAYMGKBevXqpUaNGcjgcOnjwoL799lvt2rWr2H5+//13zZgxQ0899ZTH9UvS4cOHNXbs2KLw3mazqUePHho8eLCaNm0qm82mY8eO6eeff9a6deuKBb6GYej+++9XRESEbrjhBreOl5CQoJ49eyo2NtbpsaZNm2r48OFq27atwsPDlZCQoB07dmjBggVKTk4u9tw1a9aof//+Wr9+vQIDA8v02v/9738XO79bt26tQYMGqU2bNgoPD1daWpq2b9+u+fPnKz4+vti2q1ev1owZM/Tkk0+6fbxFixbphhtuUH5+frHxgIAAXXXVVerWrZuaNWum0NBQZWZmKjo6WsuXL9eaNWuKPX/Tpk26/vrrtXr1avn7+3v8un/++WeNGTOm6HMZGhqqAQMGqEePHoqMjFRhYaGio6O1ePFi/f7778W2zcrK0l133aXNmzeXeNGoXbt2RRdZjx49qpMnTxY95u/vr3bt2p2zxtq1a3v8uqygb9++8vHxUWFhYbHxDRs26MYbbyx12/z8fA0YMMDp8y1JF154ofr27atLL71U4eHh8vPzU1JSkjZv3qyffvqp2ALjBQUFuu+++9SoUSNde+21pR5z6tSpLsP7Zs2aacCAAWrXrp0aNmyooKAgZWdnKz09XQcOHNBff/2lDRs2uPz5XBEef/xxl+F9SEiIBg8erB49eqhRo0bKycnRkSNH9OOPP2rLli2STp2z119/vW666Sav1JKRkaFBgwYVhfenv2f2799fzZs3V+3atZWYmKh169ZpwYIFstvtxbafOnWqrr32WnXr1s0r9QAAUB42w5PpHJDEDHw4YwY+gKoqOjpa559/frGxssyCmzFjhtMitgEBAUpJSSlxMdCzg9gWLVp4NDv6XJKSklwG9UuWLNGAAQNcbvP2229rzJgxTuMhISF64okndN9996lx48Zeq9Gb+vXrp+XLlxcbGzNmjGbNmmVKPbm5uerWrZt27NhRbDw8PFxvvfWW/vWvf7nc7uTJk3r88cc1Z84cp8dmzpypRx555JzHPu+883TkyJGif595Z0XHjh314Ycflvize8mSJbrhhhucAq/Vq1frkUce0fbt2yVJI0eO1JtvvqmoqCinfRQWFmrq1Kl64YUXio2HhoYqNjZWwcHBpdbv6usyKCioKGBq06aN5syZU2KwtGnTJo0aNUp79uwpNt6gQQPt3r1b9evXL/X4hmFoyJAh+vnnn4uNBwcHa/LkyXr44YddzmjPycnRSy+9pDfeeMMpGH300Uc1Y8aMUo8rSX369NGqVauK/n3m627UqJFmzZpVYsCamZmphx56SJ999lmx8bCwMMXExJzzfZdOXSjp1KlTsZn3fn5+euyxx/TUU0+VelHyjz/+0OjRo53+Vnj88cf1xhtvlHrcOXPm6K677io2dvq122w2Pf7443rhhRdKvKtp3rx5uuOOO5xCyC+//FIjR44s9diSNGrUKH366adF//b292NPnX0eSKc+N+edd57XjtG+fXunxWzHjRunKVOmlLrdI488ov/85z/Fxtq1a6cZM2bo6quvLvEiY05Ojt566y2NHz++2MWhsLAw7dixQ82bN3e5nd1uV4MGDYpd2KxVq5Zmz56tO++885wL1Obm5mr16tX65JNPdOLECaf39WwTJkzQxIkTi425ExmsWrVKffv2dXrubbfdppkzZ5b4fWflypUaPXq0Dh48KOnU95mz73By5/hnv+9nfu/o1q2b3nnnHXXq1MnlttHR0brhhhu0bdu2YuMDBw7UL7/8cs5jAwBQ0cxbjh4AAFQLSUlJevXVV53Ge/bsWWJ4Xxnq16/vFIJKp26pL8no0aOLWqWcKSsrSy+//LKaNm2qbt266dlnn9X333+v48ePe7Xm8ti3b5/TWOfOnU2o5JQ33njDZXi/YsWKEsN7SapXr54++eQTPffcc06PPfPMMx63QpBUFN736tVLq1evLvXC+8CBA/Xhhx86jd94441F4f1TTz2lL7/80mV4L0k+Pj56/vnnNXr06GLjaWlpWrBggcf1SyoKoi6++GKtXbu21FmhXbt21Zo1a3TxxRcXG09MTNQzzzxzzmN9+eWXTuF9UFCQFi1apMcee6zEdjTBwcF69dVX9e677zo99tZbbxXNtPXE6dd9wQUXaOPGjaXOjq5du7bmzJmjgQMHFhtPTU3V/Pnz3TrebbfdViy8r1WrlpYsWaJXX331nHcUdejQQevXr9fVV19dbHzWrFk6duyYW8c/0+nw/tNPP9Xrr79eakuym266SR9//LHT+EcffeTxcWuKLl26OI2V9vNBOnWB7+zw/p///Ke2b9+uAQMGlHqHUHBwsJ599ln9+OOPxe7ISE1N1dSpU0vc7rfffnO6K+mdd97RXXfddc7wXjp1AfPqq6/W3Llznb6uvcUwDD3wwANOQfvjjz+uzz//vNSLhn369NHatWvVqlUrSfKoPVlpTn/vGDp0qFauXFlieC+duui7bNkyp3ZXy5Yt09GjR71SDwAA5UGADwAAyiwhIUHXXXed4uLinB5zp69vRYuMjHQaKy10DwwM1Lx580qcqWsYhjZt2qTp06dr2LBhatasmRo1aqQhQ4Zo8uTJWrNmTbEe+pXF4XC4/Bw0atSo0muRTrWYmD17ttP4nDlzdOmll7q1j8mTJ+uaa64pNpabm6u33367TDVFREToq6++cqvlxy233OIUfp/u4d+7d29NmzbNrWNOnDjRKWArT4AWEBCg7777ThEREed8bkREhL777jsFBAQUG//iiy+UlJRU6rau1ol49dVX1a9fP7fqvPfee3X//fcXGzMMo8T1J87F399f33zzjVq0aHHO59psNpfHWbJkyTm3XbZsmTZs2FBs7L///a+uuuoqt2sNCAjQvHnzigWW+fn5ZX7tjz32mG6//Xa3nnvrrbeqa9euxcZWrVrlNCsfp3j680GSXn755WL/vvTSSzVv3jynr7PSXH311XrppZeKjX3yySdO7Z9OO/uiQnBwcJnXZalVq1aZtjuX5cuXO93x0717d73++utubd+oUSN9/fXXHq9VcS7nnXee2+unhIeHO31eCgsLtWzZMq/WBABAWRDgAwAAjx06dEjTpk1T+/btnQIv6dTMxhEjRphQWXGuZqymp6eXuk23bt20ZMkSt1vlxMfH66efftILL7ygXr16KTIyUnff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680336ywY7pydhuKzMxM/fLLL2Xa17x58865/5pgz549ZWq/tH//fu3atavYWJcuXeTj4/rPAFfv7bfffuvxcaWq9bm7+uqrnca+/vprFRQUmFBN5Tv7wmJZL7hZXWZmpkaNGuU0HhYW5nL8TP379y/277y8PH3zzTderM77WrdurbvvvrvYWHZ2tg4ePOi1Y4SEhKh9+/bFxhYvXlymixs16WsOAABPEOADAIBqIzk5WcOHD1d2dnaxcR8fH7366quVWkt2drZ+//13p/ELL7ywwo7Zvn173XbbbU7jL7zwQrlaGriyfPlybd++3eVjAwcOdBp77733PD7G4cOHnYL/+vXrV8pCxFb00UcfebzNhx9+6DQ2aNCgEp/v6nP3wQcfeHwXR1pamubOnVtszN/fX3379vVoP5WlSZMmTncHHD58uNJbUJnl7Fnaru5gquocDofuuusul22hxo8ff84Flv/5z386jU2ePFl5eXleq7EitGnTxmmsPGtquDJixIhi/87Oztbbb7/t0T7y8/NdtjYCAAAE+AAAoJrYsGGDOnXqpAMHDjg99thjj7nVuuP999+X3W73Sj1vvfWWsrKyio1FRka6bNXhTW+88YYaNGhQbCw3N1fXXHONV3oLG4ah1157Tddcc02JIdCwYcPUuHHjYmOLFy/WTz/95NGxxo4d6zQb87777pO/v79nRVcTs2bNcnl+l+TAgQOaNWtWsbHAwMBSZxp37drVae2GnTt3enwB5vnnn1dKSkqxsZtvvtll732reP75553GnnzySa8v+mlFZy9EmpqaWukLYFekmJgY9evXz+XdJN27d9eYMWPOuY8hQ4aoQ4cOxcYOHz6sRx55xFtlVojY2FinsbN/RpTX3XffrYCAgGJjkyZN8uiuocmTJ2v37t1erQsAgOqCAB8AAFRp+/bt05gxY9SrVy+nBTOlU4u7Tps2za19PfLII7rgggv01ltvlWsG6vz58zVhwgSn8ZtvvrnE1iXeEhkZqU8//dSpJ3FMTIx69eql7777rsz73rp1q3r27Kmnn35a+fn5JT7P39/fZSB25513uh3QjB8/Xj/++GOxsaCgID344IOeFV2N5Obmavjw4W4FqydPntTw4cOdel3feuut51xI9vHHH3cae/LJJ7Vq1Sq36vzvf/+rd955p9iYzWbTY4895tb2Zhk2bJjTxYu0tDQNGjTIqQ2RuzIyMvTaa6/p888/90aJFebsFiiSPL7gZkXJycmaPn26OnTo4PL8bdq0qb755hu3LgrabDZNmjTJafy9997TuHHjyrzWyIYNG3TrrbeW+PiMGTO0bNmyMu07PT3d6S6S0NBQNW/evEz7K0mDBg10//33FxvLzs5W//793eppP2PGDJet7wAAwCkE+AAAoEpJTU3Vr7/+qsmTJ6tfv35q27at3n77bZd9cwcMGKCFCxd6tHBsbGysHn30UTVs2FC33Xabfv75Z7dn5R8+fFijR4/WzTff7NRWISIiwmWoXxEGDRqkd955x6kfflpamm644Qb94x//0JIlS0oN4U/Lz8/Xjz/+qKFDh6pLly5av369WzU88cQTTrNVk5KS1KdPH3311VclbpeamqrRo0e7DMqmT5/uNLO/pggKCpJ0ao2Hnj17atOmTSU+d/Pmzbryyiud2iY1aNBA06dPP+exbrnlFg0ePLjYWE5OjoYMGaLZs2eXGFTa7XaNGzdO99xzjwzDKPbYo48+atkFbM/05ZdfKjw8vNjYoUOH1K1bN02ePNmt1iOFhYVasWKF7r//fjVv3lxPP/204uLiKqpkr+jevbvTxcUnnnhCP/zwg1vfJ6zCbrfr999/16xZszRy5Eg1bdpUzz77rBITE52ee/7552vFihVq1qyZ2/sfOnSoxo4d6zQ+bdo09e3bV6tXr3ZrP7GxsZo1a5Z69OihHj16aOHChSU+d9WqVRowYIAuueQSTZkyxe1FXnft2qX+/fvryJEjxcZHjBjhNFveG6ZMmaILLrig2Njx48fVoUMHjRs3zqnuvLw8/fzzz+rXr1+xi4bdu3f3em0AAFR17v81CwAAUIFiYmKcAt/TcnJylJqaqrS0NKcZxa74+/vr2Wef1UsvveQ0E91d2dnZmjt3rubOnSt/f39ddtll6tatm5o3b66IiAiFhYXJbrfr5MmT2rt3rzZt2qTNmzeXWM/HH3+siIiIMtVSFvfee6+CgoJ0zz33OF1MWL9+va655hrVrl1bffv2VYcOHVS/fn01aNBAfn5+Sk9P1+HDh/XHH39o7dq1ZeqXHBAQoLlz56pr167F7mZITEzULbfcookTJ+r6669Xy5YtVadOHcXHx+v333/XwoULXS5+OGTIEJfBWU3x9NNP680331RmZqZ2796t7t27q2fPnho0aFBRAHns2DH98ssvWrNmjVOAbrPZ9O6777rdOuOTTz5Rhw4dirXfyMrK0tixY/Xaa69p+PDhatu2rcLCwpSUlKQ//vhDCxYsUFJSktO+OnXqpKlTp5bj1Veeli1b6ptvvtHgwYOLfd1kZWXphRde0NSpU9WzZ0/94x//UFRUlMLCwpSdna3U1FQdO3ZM27Zt07Zt25SammreiyiDqKgoXXPNNcVm3cfHx+v6669XQECAmjVrppCQEKeLgh999JHTXQsVZfDgwS6D57y8PKWlpSktLc2pbVlJhg0bpo8//tipdZA7ZsyYof3792vJkiXFxlevXq3evXurdevW6tOnjy6++GKFh4fL399fqampSklJ0a5du7R161bt27fP4xn7u3bt0vPPP6/nn39e5513njp27KjLLrtMDRs2VFhYWNH37gMHDmjNmjVat26d0/eBiIiICpvpHhISoq+//lr9+/cv9jMjNzdX06ZN07Rp0xQWFqZGjRopJydHcXFxTj/PH3zwQTVo0EAbN24sGqvou9YAAKgKCPABAIAl5Ofna8eOHeXah4+Pj6677jpNmTJFbdu29VJlp2rbsmWLtmzZ4vG2tWrV0pdffqnrrrvOa/W464477lC7du30r3/9y2Uv4szMTC1atEiLFi3yaL++vr66++67S7zgclrbtm21YsUKDRkyRAkJCcUe27t3r9utjYYPH64vvvjCKTysSc4//3x98cUXGj58uBwOhwzD0Jo1a7RmzZpzbmuz2fTee+/phhtucPt4kZGRWrt2ra655hr9/fffxR47evSoZs6c6dZ+evbsqYULFyowMNDtY5utX79+WrNmjW688UYdO3as2GNZWVlasmSJU3hbHbz22mtatWqVUwiel5engwcPutymMhe7dbX4rKcuv/xyTZo0qdSFnM/F19dXixYt0uOPP67Zs2c7Pb5v3z6Per+XRXR0tKKjo7VgwQK3twkNDdV3332nRo0aVVhdnTt31rJlyzRw4ECXrb5SU1NLvLg1cuRIvfXWW053qtWtW7cCKgUAoGrhcjYAAKjy2rVrpxdffFH79u3TggULyhzeT58+XVdeeaXXZvwNHz5ce/bsMSW8P61z587auXOnXn/9dUVFRZVrX4GBgRo5cqT++usvvf/++woLC3Pr+Bs2bFD//v09Pl5ISIgmTpyoefPmFbWQqcmuu+46ff/9926976eFh4friy++0L333uvx8S644AKtW7dON998s8cXT/z9/TV27FgtW7asTLOczda1a1dt27ZNd911V7kWTbbZbOrTp4+uvPJKL1ZXMdq1a6dly5apZcuWZpfiVY0aNdL999+v1atXa8uWLeUK70/z9/fXrFmzNG/ePLVq1apc+4qMjCx1bQ9vBO49e/bU+vXr1atXr3Lv61y6dOmiXbt2aeTIkW49v27dupoxY4bmzp0rPz8/p+A/NDS0IsoEAKBKYQY+AACwNB8fHwUEBCgoKEjh4eGKjIxU8+bN1bp1a1166aXq2bOn12YUPvLII3rkkUeUmJioZcuWac2aNVq7dq12797tVrsDX19ftWnTRsOHD9dtt92m1q1be6Wu8goICNATTzyhsWPH6ocfftC3336r5cuXu2x3crZ69eqpS5cuGjZsmEaMGFGmMPaCCy7QsmXLtGzZMs2aNUvLly8vtdXFBRdcoH/+8596+umnK3S2aFU0dOhQ7d69W5MnT9b//vc/paenu3xegwYNNGLECI0fP97ttjkl7efrr7/WU089pddff11Lly4tdRHdqKgoDR06VM8884wuvPDCMh/XCurXr6///ve/mjBhgmbNmqWff/5Zu3fvdmpLcrY6deqoV69e6t+/v4YNG6YWLVpUUsXld8UVV2jv3r1aunSpFi9erJ07d+rQoUNKT09XdnZ2mRdqrSg2m03+/v4KDAxUaGioIiMj1aRJE7Vq1UoXX3yxevToUaHfh2+88UYNHz5c3377rf73v/9pzZo1brUca9u2rfr3769rrrlGAwYMKHWdltOL5P74449asWKFNmzYoBMnTpzzGMHBwRoyZIjuuOMOXXvttR69rvKKiorSl19+qRdeeEHz5s3T0qVLdfz4ccXHx8vX11cNGzZU+/btNWjQIN16663FQvqz14s4e00KAABqIptxrt9A4WTr1q1mlwCLqQqLsgEAyi43N1cHDx7UgQMHFB8fr4yMDGVnZysoKEh169ZV3bp11bJlS1188cUKDg42u1y3HTx4UPv379fRo0eVnp4uu92ukJAQ1atXT+Hh4WrXrp1atmzp9dY1eXl5+v3333Xs2DElJiYqKytLERERatCggS655JJyz2it6qKjo3X++ecXG/vkk080atSoYmP5+fnavHmzdu3apeTkZPn4+Cgq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LwEAsBrDMBQfH292GbA4wzCUkJBQ7O5OAAC8gR74AHAOBzYv1/6NSzVt6pQSe8CfraoFpZ07d9bgwYNlGIZeffXVohD9zMD+wIEDHu/39CKvMTExbm8TGBio2rVrl/mY7jgd0B89elR//vmnJGnZsmUqKChQvXr1dMUVVzhtc/q9qKiaKkNVOy8BALCCtLQ0t+6+AxITE4tNgAEAwBsI8AGgFIZhaOnb49Slazddf/31bm/n6+tbcUVVkHvuuUe+vr46fPiwFi9eLEm68MILFRISIklaunSpx/u89NJLJUm///67R3/4XnbZZZKkX3/9tUL+CGrWrFlRbafb5pz+78CBA11eqDld019//VVif3urc/cCFAAA+P8SEhLMLgFVhMPhKHG9KQAAyooAHwBKsWvFAh39a5NenT7No9nLVTEobdq0adHCrR9//LEKCgrk5+en6667TpK0ePHiooVcS3J6wdvTrr32Wvn6+iotLU3vv/++27UMGzZM0qkZ8p999lmpz83JyVF+fr7b+z5t8ODBkk5dmDh06FDRTHxX7XNOPz8wMFAOh0PTp08vtQ9uYWGhMjIyPK6polXFC0sAAJgpOzvbkj/TYV3x8fEyDMPsMgAA1QgBPgCUwFFQoGXvPKcBAwaqT58+Hm1bVYPSUaNGyWazKSYmRt9//70kafTo0WratKkcDofGjh2rzz//vNiCtpmZmVq/fr1eeukl3XPPPcX216xZM91+++2SpM8++0yTJk3S0aNHix4/efKkli5dqieffLLYdn369FHfvn0lSbNnz9bUqVN15MiRosfz8/P1119/6T//+Y+GDh2qlJQUj1/rgAED5O/vr7S0NE2YMEGSdP7556tt27Yun1+/fn2NHTtWkrR27Vo99NBD+uOPP4qCfMMwFB0drc8//1wjRozQmjVrPK6polXV8xIAALPQ+x6eys3NdZrUAgBAeVS9KaIAUEm2Lp6j+Oh9mvbdlx5vWxVn4EtSy5Yt1atXL61atUqffPKJrrvuOoWGhurtt9/WU089pf3792vmzJmaOXOm6tSpo8LCQmVlZRVt36xZM6d9PvDAA8rKytK8efP0ww8/6IcfflCtWrVUWFgou90uSUU97880adIkvfzyy1q6dKnmz5+v+fPnKzg4WP7+/srMzCzWWqcsvd3r1q2rnj17asWKFdq9e7ekkmffnzZy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bDZFRkaaXQYAAFWWlYLSo0ePas2aNZKkBx54QKNGjSr2uJ+fny666CJddNFFuvPOO2W3202oEpXBSuclAMB6CPABwIIaNmxIgA8nERER8vf3N7sMAADgBfv37y/6/969e5/z+UFBQS7Hjx8/rrlz52rTpk2Kj49XYWGhoqKidMUVV+i2225To0aNnLZZtGiRJk6cqKioKC1atEh79uzRp59+qu3btys9PV2RkZHq3bu3Ro8erbp165ZY059//qk5c+bojz/+kN1uV8OGDdWvXz/dddddbrwDUmZmpr7++mutXr1aR48eld1uV3h4uC677DLdcsstat++vdM2MTExuu666yRJCxcuVGFhoT799FP9/vvvSkxMVP369bVo0SK3jg8AQFVAgA8AFhQaGsosfBRjs9kUFRVldhkAAKACxMfH6/zzz/d4uwULFmj69OkqKCiQJAUEBMhmsyk6OlrR0dFauHChpk+fru7du5e4j19++UUTJkxQQUGBateuLYfDoRMnTmju3LnauHGj5syZo1q1ajlt98MPP2jy5MkqLCyUJNWuXVuxsbH65JNPtGLFCg0bNqzU2v/66y898cQTSk5OlnSq939QUJDi4+O1dOlSLVu2TA8++GCpFwN27typKVOmKDs7W0FBQfLzI+IAAFQ/NNEFAItq0qSJ2SXAQho2bKiAgACzywAAAF7Srl072Ww2SdLMmTN15MgRj7ZfuXKlJk+eLEkaNWqUFi1apHXr1mnt2rX69ttv1b9/f2VlZemZZ55RXFycy32cPHlSL7/8soYOHarFixdr5cqVWr16tZ5++mn5+fnp0KFD+uyzz5y227t3r6ZMmaLCwkJdfvnl+vbbb7Vy5UqtWbNGkydPVnJysj766KMSa4+JidHYsWOVnJysfv366fPPP9e6deu0atUqLV26VKNHj5aPj4/efvttrVy5ssT9TJkyRRdccIE+++wzrV27VmvWrNHs2bM9eh8BALA6AnwAsKiQkBDVq1fP7DJgAX5+fi5vfwcAAJ45HZhbQePGjXX99ddLkg4cOKAbb7xRt912m6ZPn64ffvhBBw4cKLE3en5+vl599VVJ0rhx4zRmzBhFRUXJZrPJZrPpvPPO07Rp09SrVy9lZWXpiy++cLkfu92uAQMG6IUXXij6XSMoKEg333yzRowYIUkuF9B955135HA41Lx5c731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+uiCCy6ocrv169frgw8+0H//+1/l5uYqJCREycnJ6tOnj6699toq5+yvak7p7OxsXXHFFXI6nXrqqafUs2fPk2Z99tln9c9//lPNmjWrcvHibdu26c0339S3336rgwcPys/PT82aNdOf/vQn3XzzzYqKijphm4qpSc477zw9//zzWrdund555x1t375dubm5uuuuu3TPPfec8u/wTHnjflkf2Ww2XT7scb1476Vavny5rr/++pM+ti5/pna7XcnJyUpOTtbAgQM1depUffzxx1q1apWSkpJ0xx131FkWADjWRTcO01dvzNVDDz+sN15//ZSPtdJ74fbt2yt/f6rzkgrBwcFVfn3fvn1asmSJNmzYoMzMTJWXlyshIUEXXnihBg0apPj4+BO2+eCDDzRlyhQlJCTogw8+0M8//6xFixbpu+++0+HDhxUbG6uePXvqrrvuOuXUij/++KNefvllff/99youLlZcXJwuvfTSar8nFBQU6M0339Tnn3+u1NRUFRcXKyYmRuecc45uuukmderU6YRt9u/fr6uvvlqS9P7776u8vFyLFi3SN998owMHDqhRo0b64IMPqvX6VmGl/RIAYD0U+ABQA5fePUnff/iKnnjiiWovluYtjv3gUF5eftz3UlNTNXLkSKWmpko6WjCGhYXp0KFD+uyzz/TZZ59p4MCBevDBB094XqfTqZSUFL3xxhuVXwsJCZHT6dRvv/2m3377TZ9++qnWr19/3HZFRUWaOHFi5cg0SQoLC1NBQYG+++47fffdd/rwww81Z86cas3ZHxMTowsvvFD//ve/9eGHH570g7LL5dKqVUfnHr/yyitP+P5zzz2nF198sfLvKzg4WGVlZdqxY4d27Nih999/X3PmzDnhAsixUlJStHjxYtlsNkVERFSOtAMqJHf7c+Xi2VdffbXlFpcODg7WlClTtGfPHv3yyy96+eWXdf3111de2DzdIra7d+/W4sWLtXnzZmVmZkqSoqKi1LhxY3Xr1k39+vVTy5YtKx//++f76aef9PLLL+uHH35Qfn6+YmNjdckll+jOO++scoqb8vJybdmyRV988YU2b96srKwsZWdnKywsTElJSbr88survCA4ZMiQ4+alriiMKlRckDtWaWmpVq5cqXXr1umXX35Rfn6+GjRooCZNmuiiiy7SlVdeqaZNm57073bt2rV66623tHPnTpWUlKhFixa66qqr9Je//OWUx4qDBw/q9ddf13/+8x/t379fpaWlaty4sbp27apBgwapdevWJ2zz+7/Xbdu26bXXXtO3336rQ4cO6Zxzzjnhz1ddlFGoS7VZBNxqMjMz1apVqxpvt3z5cs2cOVNlZWWSpMDAQNlsNu3evVu7d+/W+++/r5kzZ550oIQkrVq1SpMnT1ZZWZnCw8PldDqVlpamJUuW6Ouvv9bLL7+s0NDQE7Z77733NH369MrzxvDwcKWnp+ull17Sp59+quuuu+6U2bdu3ar7779fhw4dknT0QnFwcLAyMzP18ccfa82aNfr73/9+yosBW7Zs0WOPPaaioiIFBwdb7v0SAAB34N0NAGrIV0uJr7/+WtLRcr5JkyaVX8/Pz9ewYcO0f/9+devWTUOGDFGHDh0UGBiogoICvffee3ruuef09ttvq0WLFrrpppuOe96nn366sry/+uqrddttt1XOjZ2dna0tW7ZUOa/rpEmT9MUXX6h58+a655579Kc//UlhYWEqKSnR119/rdmzZ2vLli2aOnWqnnzyyWr9Gfv166d///vf+uKLL5Sfn19l0ffDDz8oLS1N0okF/pIlS/TCCy8oLCxMd9xxh/r3769GjRrJ6XRq+/btmjdvnjZu3Kj7779fS5curfLD7rZt2/Ttt99q8ODBuvXWWxUdHS2Hw1H54RU41u/nlreSgIAA3XHHHXrwwQdVWFio9evX65prrjntdl9//bVGjx4th8Mh6egUESEhIcrMzFRmZqa2bt0qf3//k96Rsn79eo0fP16lpaUKCwuTy+XSvn379Oqrr2rt2rV67rnnjjuGSUfX+rjrrrsq/7+iJMrLy9O3336rb7/9VqtXr9b8+fOPG+EaGRmpqKgo5ebmSjp6kcFutx/3/WOlpaVp9OjR2rVrl6SjP7/w8HDl5ubq0KFD+vHHH3X48GHdf//9Vf7ZZs6cqaVLl8rPz6/yeLd9+3Y99dRT2rZtm6ZMmVLldl988YUmTpxYOf2Zv7+/AgIClJaWprS0NH344YeaOHGi+vfvX+X2krRu3TpNnDhRZWVlCgsLowSD17LycfP3zj77bNlsNrlcLs2ZM0czZ86s0foh69ev1/Tp0+Xv76/bb79dAwYMqBxtv2fPHi1cuFBr167VuHHj9Oabb1Y5Ej8nJ0dTp05V//79dddddyk+Pl7FxcV6//33NXv2bP3666965ZVXKi/2Vdi2bZsee+wxlZeXq0uXLho/frxatmypsrIyrVu3To8//rhefPHFk2bfv3+/hg8frvz8/MoR+8nJyfL391d2drbeeustvfTSS3r66afVqlUr9erVq8rneeyxx9S6dWuNHTtWZ599duWfHQAAX8KZOQDUwLoXpiosNERjx441HcVtKhax3bhxoyTpT3/603FTwPzrX/+qLO/nz59/XKkTHh6uQYMGqUmTJhozZoz++c9/6oYbbqh8zJ49e/Taa69JkgYPHqz77rvvuNeOiYlRr169TvhQ9u9//1vr169Xw4YN9dxzzyk2Nrbye0FBQerZs6fatWungQMHav369frll1/Utm3b0/5Ze/ToofDwcBUUFGjNmjVVTk2ycuVKSdK5556rZs2aVX49NzdXzzzzjGw2m2bNmlW5ZoB0tAxs37695s+frzvuuEM///yz3n33Xd18880nPH9RUZEGDRp03N9FYGCgEhISTpsf9cfOjZ9o+9cfa9myZZYuUi+66CLZ7XY5nU59++231SrwZ86cKYfDoQsuuEAjR45UcnKypKPTdO3du1effPJJlSVThcmTJ6tz58568MEH1apVK5WVlenTTz/VjBkzlJ6ergcffFAvvfTScUW73W5Xz5491bdvX5177rlq2LCh/Pz8VFRUpHXr1umZZ57Rd999p2eeeUajR4+u3G7WrFnHTdXwyiuvnHBxoEJBQYGGDx+u1NRUNWjQQMOHD9dll12m8PBwlZWVKS0tTV988cVJy8XPP/9cR44c0ahRo3TNNddUFv8LFizQu+++q5UrV6p///7q1q3bcdtt3bpVY8eOVWlpqa6//nrdfPPNat68uex2uzIyMrRo0SItXbpUjz76qFq3bl1ZcP3elClTdP7552vUqFGVdz9U3HVVG95UosL7lRYf0SfPT9Zf/vpX/eEPfzAdp9qaNGmia6+9VsuXL9fOnTs1cOBAnXXWWercubPatWunDh06KCkpqcp/T6WlpXriiSckSePHjz/h+NuyZUs9/vjjGj16tD7//HMtXry4youHxcXF6t+/vx566KHKrwUHB+vGG29UWlqaFi9erNWrV59Q4D/zzDNyOp1KTEzU3LlzKy9++vv7q0+fPpXHwZOZO3eu8vPzdeWVV2rq1KnHfS8mJkZDhw5VRESEUlJS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tm1b5Xbff//9SS8yHjs9w8lG58fHxysuLk6S9Pnnn580n7tVFHilpaWVFymsguMNPO3jZx9WfHyC/vGPf9RoOyudo6WlpWnPnj2nfVzFwApJlYtk/+lPf6q8ePjkk0+edvqv35+jnYmIiAhdcMEFkqTXXnutyuPnN998U+UCttLREfw9e/aUJL366qun/TtwZ3ar4pgJADgVCnwAOAlnWZnWPDNBl1/ep8bFlLeehN9+++2y2Wzav3+/3n33XUnSXXfdpWbNmsnpdGr48OF67bXXjptfuqCgQP/5z3/0yCOP6O677z7u+Zo3b65bb71VkvTKK6/o0UcfVWpqauX3c3Jy9PHHH+uBBx44brtevXpVLuC5YMECzZgx47gPd6Wlpdq6davmzZun/v37H7eYWnVdfvnlCggIUF5eniZPnixJatWq1XHF37EaNWqk4cOHS5L+/e9/6x//+Ie+//77yiLf5XJp9+7deu211/SXv/xFX3zxRY0zeZq37pf1QU0XYaxg6mdaXFysyZMn65dffpF09Njx+4Wfq3K6i21BQUGVvz/ZvMYnK4s2bdqkH374QZKOm3orLCys8gLsjh07TtiurKxMzzzzzEkzHTs3/akuzFVc7Hzvvfe0bdu2kz7Onc4+++zKixLPPPPMaRcbr8sSjOMNPGnfz5u1Zc2bmjplskJCQmq0rZX2zV9//VU33HCDRowYoRUrVhw35WBZWZm2bdumKVOmaPHixZKkDh06VC44HRQUpAcffFA2m03btm3TnXfeqa+++uq442xaWpqWLVumwYMHa+nSpW7NPnToUNntdu3evVsjR46snNqnrKxMa9as0fjx40/5vjBy5EhFRkaqsLBQd911l9577z0VFBRUfj83N1effPKJxowZo4kTJ7o1uxVZab8EAFiPdYYfAIDF1HQxyWNZaXRXTSQnJ6tHjx767LPP9NJLL+nqq69WZGSknn76aY0ZM0bbt2/XnDlzNGfOHEVERKi8vFyFhYWV21c1h+m9996rwsJCLV26VO+9957ee+89hYaGHrdYZHh4+AnbPfroo5o6dao+/vhjLVu2TMuWLVNISIgCAgJUUFBw3NQ6tZk3tEGDBvrjH/+oTz/9VD/99JOkk4++r/DXv/5VDodDTz/9tDZt2qS77rpLAQEBCg0NVWFh4XELTFpxLlNv3S993eGD6TVehLFCXX7gLy8v16+//qqvv/5aS5curZz7/sorr9Rtt91Wref44Ycf9OSTT+qqq67SRRddpBYtWsjPz08ul0tbtmzR448/LkmKi4s76Z0IBw8e1MiRIzVu3Di1bNlSZWVlWr9+vWbMmCHp6OjUiguA0tGpJ8455xx9//33SklJUVRUlLp06SI/Pz/t3LlTKSkp+vnnnxUSElLlNDQRERGKjY1VVlaWPvjgAyUnJ1f5b+mWW27R6tWrlZqaqr///e8aPny4LrvsMoWHh6usrExpaWn6+OOPFRwcXHlh80zZbDaNHz9eQ4YMUUZGhm6//XYNHz5cf/zjHxUcHCzp6PQamzZt0ooVK9SkSRM99NBDbnnt0+WijIInffz0BJ3Vtl21jz3HstK+6e/vr/Lycn355ZeVi7lWnFccPnz4uLt+2rVrpyeffPK4i5u9evXS1KlTNX36dG3fvl3Dhw+X3W5XeHi4jhw5IofDUfnYihHv7nL22Wdr3LhxmjFjhjZu3KiBAwcqPDxcDodDDodDLVu21HXXXaeUlJQqt2/WrJmefvppjR07Vvv379ejjz6qadOmKSIiQmVlZSoqKqp8bPfu3d2a3Yo4RwMAnArvEgBQhdosJnksK304rKk777xTn332mTIzM/XOO+/or3/9q5o2bapXXnlFq1ev1tq1a/Xzzz8rNzdXdrtdTZs21VlnnaU//elP6tGjxwnPZ7fbNW7cOPXp00fLli3Td999p+zsbAUFBalJkybq1KmT+vTpc8J2wcHBeuyxx3T99dfr/fff1w8//KCDBw+qqKhI0dHRat26tS688EJdcsklio2NrdWftV+/fpXTTvj5+emKK6447TaDBw/WJZdcoqVLl2rjxo3av3+/CgoKFBYWpmbNmqlr167q1auXOnXqVKtMnuTN+6UvW/fCVIWGBGvMmDG12t7f3/+4i0fucuy/S4fDocLCwuMunEVFRenee+/VgAEDavS8FaV5SkqK/P39FRYWpoKCgsq7WcLCwjRt2rST7q+TJ0/Wgw8+eEJZJB2dymbmzJknFCH333+/hgwZoqysLN17770KDAxUQECACgsLZbfbNWnSJC1cuPCk88hff/31Wrhwod58800tX75c0dHR8vPzU8eOHSsvHISFhWnevHkaPXq0fv31V02fPl0zZsxQeHi4CgsLK/98N910U43+vk6nY8eOmj17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Ho88Hs+B37tcLsN/KwAAgJYrLCxUjx49Wvy4V199VXPnzlVDQ4Mkye12y+FwaNu2bdq2bZv++te/au7cuTrjjDOOeIx33nlHU6dOVUNDg5KSkuT3+7Vr1y4tX75cn332mZ599lklJCT85HGvv/66Zs6cqcbGRklSUlKS8vPz9cwzz+iDDz7QZZdddtTsX331le666y7t27dP0o9r/3s8HhUWFuq9997TqlWr9F//9V9HvRiwYcMGzZo1S9XV1fJ4PIqJYY4iACDy8OoGAAiJ2tpalZWVqby8/EBp3xL7PxxKUkNDg6qrq3/yPTExMfJ4PEpKSlJycrISExPlcDjanB0AACDQTjjhBDkcDlmWpYULF2ru3Lnq1q1bsx//4YcfaubMmYqJidGNN96oK6644sBs++3bt+vxxx/X6tWrNWHCBL3wwgtNzsQvKSnR9OnTNXToUN1yyy3KysqSz+fTX//6V82fP19bt27Vc889p+HDhx/yuE2bNmnWrFlqbGzUaaedpokTJ6p79+5qaGjQmjVrNGfOHD399NNHzL57926NHDlSFRUVB2bs5+TkKCYmRsXFxXrxxRf1zDPP6NFHH1WPHj10zjnnNHmcWbNmqWfPnho/frxOOOGEA393AAAiidN0AABAZLIsS5WVldq1a5e+/vprffXVV9qxY4fKyspaXN43V0NDgyorK1VQUKDvvvtOGzZs0LZt21RSUiK/3x+UcwIAgPBhpwv7HTt21KWXXipJ2rJli6688koNGzZMc+fO1euvv64tW7YccW30+vp6Pfjgg5KkiRMnasSIEcrOzj5wt2L37t01Z84cnX322aqqqtKyZcuaPI7P59MFF1yge++990DB7/F4dPXVV+u3v/2tJDW5ge5///d/y+/3q2vXrlq0aJG6d+8u6cfJFIMHD9asWbNUUVFxxL/7okWLVFFRoYsuukhz585Vnz59DsyeT0tL0/Dhw3XnnXdKkp588skjHic5OVn//d//faC8l9SiiyB2YadxCQCwH2bgAwACxrIslZWVqbS0VGVlZQdu5zaloaFB+/bt0759++RwOOT1epWcnKy0tDRusQYAAMZNmDBB6enpWrZsmWpqavTdd9/pu+++O/D1tLQ0DRkyRDfccIPS09MP/Pknn3yioqIipaenH1gPvikXX3yxPv74Y3366adH/J7//M//bPLPBw4cqGXLlmnHjh3y+XwH1uCvqKjQZ599Jkm6/vrrm1yb/8wzz9TJJ5+sDRs2/ORrZWVl+uCDDyTpJxv3Hp59wYIF+v7777Vv375D/v77XX311U0u7xNuKPABAEdDewEAaDO/36+9e/eqqKhIdXV1puM0ybIslZeXq7y8XDt37lR6eroyMzOPuCEcAACIPPuXrLGLmJgYDR8+XNddd50+/vhjffnll/rmm2/0ww8/qL6+XsXFxVq+fLneeustLVy4UCeddJIk6V//+pckqby8XEOGDDni8evr6yVJ+fn5TX49OTlZXbp0afJrHTp0OPD78vLyA++ZNm3adGBpw379+h3x3P369WuywN+4ceOBx99+++1HfPzB8vPzmyzwf/7znzfr8QAAhDMKfABAq9XX16uoqEh79uwJqyVqLMvS3r17tXfvXqWkpCgzM1NJSUmmYwEAgCCz60znpKQkXXTRRbrooosk/bh30D//+U89//zz+tvf/qbS0lJNmDBBr7zyiuLi4rRnzx5JP74X278J7NEcafnCo81ed7lcB35/8F2VxcXFB36fkZFxxMcf6Wv7s0tqVnbpx6V+mpKWltasx9udXcclAMAeKPABAC1WU1OjwsJCFRcX22oWW2uUlpaqtLRUiYmJysrKUnJyMh+iAACIUOHyGh8XF6fTTz9dp59+uqZOnaqVK1eqsLBQn376qc4555wDEyfOOussLV682HDaltmfPS4uTp988kmbjuV0Rsa2fuEyLgEAZkTGqx0AICTq6uqUm5urb775Rvv27Qv78v5gVVVVB/5uZWVlpuMAAIAgOHhWebi47LLLDvx+27ZtkqT27dtL+nHz21A7eNZ7UVHREb/v4Jn2B9ufvba2Vjt27AhsuDDF3kwAgKOhwAcAHJNlWSosLNTXX3+t0tJS03GCyufzacuWLdq6deuBdWMBAEBkCMei9OBlbtxut6T/W/u9qKhI//znP0Oap0+fPgdmvq9fv/6I37du3bom//zkk08+MOP83XffDXzAMBSOF5YAAKFDgQ8AOKqqqip9++232rlz54ENx6JBSUmJvv76axUVFUXUnQYAAEQzOxWlu3bt0vbt24/5fStXrjzw+z59+kiS/t//+38HZrI/9NBDR1wjfr9A3l3o9Xp1xhlnSJL+/Oc/N7m+/ueff97kBrbSjzP4Bw4cKEn605/+dMz/BtFw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//Ef5/jjj09HR0eGh4dzzz335K/+6q9y//3356KLLspf/uVfTug1nnzyybnrrrty55135rnnnktXV9cBz/nBD36QJ598MsmBBf6XvvSlXHnlleno6Mgf/MEf5JRTTsm8efOyb9++PPzww/nsZz+bLVu25M/+7M9y3XXXpb29/YD9P/TQQ/ne976Xs88+O2eddVb6+vqyd+/e/Md//MeEXgMAwFQql8tpb28/6P/HAADTz5I/gEPwjSsvSkd7W84777yio0yan/3sZ/n4xz+eLVu2JEmOP/74/UbA/PVf//V4ef+5z30uRx99dFpbW5MknZ2dOfPMM/PRj340SXLVVVdlZGRkfNvHHnss1157bZLk7LPPzoUXXjhe3icvropfuXJlLrnkkv0y3XXXXfnWt76VuXPn5oorrshv//Zvp6OjI0lSqVTym7/5m/niF7+Ytra2fOtb38qPfvSjCb3WE044IZ2dndm7d29uu+22gz7na1/7WpLkzW9+cxYuXDj+9e3bt+fzn/98SqVSPv3pT+dd73pX5s2bl+TFX3SXL1+ez33uc1m+fHm2bdt20NE7yYvvLDjzzDPzp3/6p+nr60uStLa2ZnBwcEKvAQAAAJg9FPgAE/TM449kyw1X5oMXXPCK8++T1PXbhFevXj3+8ba3vS2nnHJKvvrVryZJlixZst8YnFqtlptuuilJcuaZZ77sjc5WrlyZjo6ObN++PQ899ND412+++eaMjo6mp6fnkOa7j5XfJ510Uvr7+w/6nPnz52fFihVJkn/8x3+c0H4rlUp+67d+K0lyyy23HPD43r17c/vtt48f+6W+/vWvZ8+ePVm+fPn4DX9/WXNzc1avXp3kv97R8MuampqyZs2aCeWdbPV8XgIAAAAHMkIHYII2feFDGRgYzLp164qO8pq83KiWk08+ORdccMF+NxV79NFHs2PHjiTJRz/60Vec1b579+4kyU9/+tP86q/+apLk/vvvT5K89a1vPaSblX3/+99Pknz1q189aNE+ZufOnUlefBfBRJ188sm54YYbxkflDA0NjT82NlqntbU1b3/72w+a6V//9V/HS/qD2bNnT5IXvw8Hs3DhwsyZM2fCeSeTAh8AAAAaiwIfYAKefOh7uf+2r+Sqq65KtVp91efXc1H63e9+N8mLq+vHbkJ72WWX5Wtf+1qWLl2as88+e/y5Tz/99PjnP//5zye0/7ECO/mviwWHMh5mZGQk27dvT/JiQT9W0k/0mK/mzW9+c4aGhvLkk0/m61//etauXTv+2NjFghNOOOGA+fhj34vh4eEMDw8fdqaiynsAAACg8SjwASbg1svOzxveeNR+5fYrqecCf0ypVMq8efPyzne+M4sXL8573vOe8Rnuxx57bJJkdHR0/Pm33npr5s6de9jHmqh9+/aNf/6JT3wiJ5544mEd85WyvOMd78iGDRtyyy23jBf427dvz3e+850kL67S/2Vj34t3vvOdOf/88w/7+K/0Loap1gjnJQAAAPBfzMAHeBU/3rI5D9+zKZde8omXnQH/yxqtKF2xYkVOOumk1Gq1fOpTnxov0V9a2P/4xz8+5P2O3eR169atE96mUqmks7PzsI85EWMF/eOPP55//ud/TpLcdtttGRkZSV9fX4477rgDthn7XkxVpunQaOclAAAAzHYKfIBXUKvVsuny83PsW96a3/3d353wduVyeepCTZFzzjkn5XI5//Zv/5abb745SbJ06dJ0dHQkSTZt2nTI+/y1X/u1JMk//dM/TWjszJhf//VfT5Lcfvvt+70LYLIsWrRoPNvY2JyxP1evXn3QCzVjmR544IGXnW9f7yZ6AQoAAACoDwp8gFfww29+NY8/8P/yqU9eekirlxuxKF24cOH4jVuvuuqqjIyMpLm5OaeeemqS5Oabbx6/kevLGbvh7Zjf+Z3fSblczo4dO3LFFVdMOMtpp52W5MUV8tdcc80rPnf37t154YUXJrzvMSeddFKSFy9MPProo+Mr8Q82Pmfs+ZVKJfv27csnP/nJ/Ub9/LLR0dE899xzh5xpqjXihSUAAACYzRT4AC9j38hIbvv8BTnxxNVZuXLlIW3bqEXpu971rpRKpWzdujU33HBDkmTt2rVZuHBh9u3bl/e+97259tpr97uh7c6dO3P33Xfnwx/+cM4555z99rdo0aKcddZZSZJrrrkmF198cR5//PHxx3/+859n06ZN+cAHPrDfditXrsyqVauSJJdddlkuueSSPPbYY+OPv/DCC3nggQfy2c9+NqecckqeffbZQ36tJ554YlpaWrJjx4585CMfSZK87nWvy/Llyw/6/Hnz5uW9731vkuSuu+7KunXr8v3vf3+8yK/VavnJT36Sa6+9NqeffnruvPPOQ8401Rr1vAQAAIDZ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2),\n", + " Circle((x+2.5, y+0.6), 0.5, fc=box_bg)\n", + " ]\n", + " \n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=0\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=1.8\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " \n", + " patches.extend( [ FancyArrow(3.1,2.4,0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " Circle((4.2, 2.4), 0.5, fc=box_bg),\n", + " FancyArrow(3.0,1.2,0.35, 0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(3.0,3.8,0.35, -0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,2.4,0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,1.8,0.35, -0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " FancyArrow(5.1,3.1,0.35, 0.4, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2 ),\n", + " Circle((6.2, 3.9), 0.5, fc=box_bg),\n", + " Circle((6.2, 2.4), 0.5, fc=box_bg),\n", + " Circle((6.2, 1.1), 0.5, fc=box_bg)\n", + " ])\n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(3.8,2.35, 'Dispatcher', fontsize=18)\n", + " plt.text(2.2,0.55, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(5.9,1.05, 'Sender', fontsize=18)\n", + " plt.text(5.9,2.35, 'Sender', fontsize=18)\n", + " plt.text(5.9,3.85, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(3.0, 5, 'PDS Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f1d19ebf", + "metadata": {}, + "source": [ + "Receiver\n", + "------------\n", + "\n", + "Scans an input directory, uses IPC to tell the dispatcher about it.\n", + "Log activities.\n", + "\n", + "Dispatcher\n", + "---------------\n", + "\n", + "Processes the files arriving as notified by the dispatcher using IPC.\n", + "\n", + "For each file:\n", + "\n", + "* Examine the accept/reject (imask/emask) rules for each sending process.\n", + " The decision to put files in a sender directory was based on evaluating\n", + " sequential regular expression patterns for each sender. Assume 10 regular\n", + " expressions per sender.\n", + "* As a single dispatcher is running, it can assign ensure file names are unique then they are placed in sender directories.\n", + "\n", + "place a file in the directory each sender uses. Log activities.\n", + "\n", + "\n", + "Sender\n", + "----------\n", + "\n", + "Scan the sending directory, send files, remove them.\n", + "Log activities.\n", + "\n", + "\n", + "Design Decisions/Assumptions\n", + "--------------------------------------------\n", + "\n", + "* Developers were asked to provide a single log for the entire system. \n", + "* on the order of 10 receivers.\n", + "* on the order of 100 senders.\n", + "* Assume 10 regex's per sender.\n", + "* it was the eighties... so 1 or two cpus. Servers were very expensive, clustering for HA was a thing, but scaling was accomplished by bigger, more expensive servers.\n" + ] + }, + { + "cell_type": "markdown", + "id": "09e41c5f", + "metadata": {}, + "source": [ + "Routing 1 Product\n", + "--------------------------\n", + "\n", + "* PDS dispatcher routing for all receivers at once.) \n", + "* represents a scale out vs. previous deployments.\n", + "* units of as the number of regular expressions evaluated. (not MIPS.)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "id": "c2a69a1e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PDS pre-routing cost 5000 regular exppression evaluations\n", + "Assuming an average regex costs about the same as 500 instructions\n", + "then that means the cost to route 1 product is, on average, about 500000 instructions.\n", + "Then add client-side second evaluation of RE's by sender: 50000.0\n" + ] + } + ], + "source": [ + "S=100 # number of senders to route to.\n", + "re=100 # number of regex's per sender to evaluate. \n", + "R=10 # number of receivers.\n", + "Rp=S*re/2\n", + "RE=100\n", + "s=10 # number of senders selected by pre-routing phase.\n", + "\n", + "print( f\"PDS pre-routing cost {int(Rp)} regular exppression evaluations\")\n", + "\n", + "print( f\"Assuming an average regex costs about the same as 500 instructions\")\n", + "print( f\"then that means the cost to route 1 product is, on average, about {int(Rp*RE)} instructions.\")\n", + "\n", + "Rpc= s* RE*re/2\n", + "\n", + "print( f\"Then add client-side second evaluation of RE's by sender: {Rpc}\")" + ] + }, + { + "cell_type": "markdown", + "id": "20ad3b93", + "metadata": {}, + "source": [ + "Observations:\n", + "--------------------\n", + "\n", + "\n", + "* modern systems have many more cores (16 is common on single servers... ) but also clustering is common. Clustering is scaling with multiple servers, rather than more powerful individual servers.\n", + "* In order to ensure log entries were not corrupted by different processes writing to the log at once, a locking mechanism was used to mediate access.\n", + "* The dispatcher is a single process doing all of the routing.\n", + "\n", + "2004, there was a project to use PDS to replace the existing Tandem Apps application. It didn't work, PDS was too slow, so that motivated some analysis of how PDS worked. The work can be used as motivation for a discussion of application design." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d9fba884", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/README.rst.txt b/_sources/Contribution/Philosophy/README.rst.txt new file mode 100644 index 000000000..07fb18e8b --- /dev/null +++ b/_sources/Contribution/Philosophy/README.rst.txt @@ -0,0 +1,9 @@ + +Some Development Background slideshows. + +To make a slide show, use normal notebook stuff: + +* To start up the Editor: jupyter notebook --ip=0.0.0.0 --port=8040 +* to make a slide show: jupyter nbconvert --to slides xx.ipynb + + diff --git a/_sources/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb.txt b/_sources/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb.txt new file mode 100644 index 000000000..7f1108470 --- /dev/null +++ b/_sources/Contribution/Philosophy/Sarracenia_Algoritmic_Designs.ipynb.txt @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "e9c590b8", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Sarracenia Algorithmic Design\n", + "=========================\n", + "\n", + "What were the issues with Sundew that led to Sarracenia?\n", + "\n", + "\n", + "* Problems/Weaknesses of Sundew\n", + "\n", + "* Key Decisions for Sarracenia\n", + "\n", + " * Use Folders/Directories\n", + " * Add a robust message queueing system.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "6d06c484", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Problems/Weaknesses of Sundew:\n", + "-------------------------------------------------\n", + "\n", + "* push only, required someone else to send to us for us to receive.\n", + " Senders always must configure who they are sending to.\n", + " \n", + "* limited parallelism: each receiver or sender is a single process, only a single sender for a single destination.\n", + "\n", + "* without dictionaries, the routing algorithm falls back on relatively expensive PDS routing. Message routing (the cheap kind) was disappearing.\n", + "\n", + "* internal design details, make the path names highly constrained (had to have colons in them, no folders, etc...)\n", + " Nobody without very specific directory structures could use it.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "1a17273d", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Use Folders/Directories\n", + "----------------------------------\n", + "\n", + "* Sundew used only the name of the file to make routing decisions. \n", + "* Sarracenia uses the entire path.\n", + " * It allows for any directory tree to be transferred, and for the \n", + " * all the directories in the tree to contribute to routing decisions. \n", + " * The entire path becomes file metadata. \n", + " * more compatible/adoptable for different file flows.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "51384c53", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Add RabbitMQ\n", + "--------------------\n", + "\n", + "Introduced rabbitmq/amqp broker. After years of study of performance (it was used as a pure dissemination tool, outside of Sundew for six years prior to start of work on Sarracenia, which made a message broker central to data routine.) We needed to ensured that, as a componentm it would scale reasonably well for our loads.\n", + "\n", + "Using RabbitMQ, we introduced topic/based routing, as an analog to dictionary routing, but much more applicable to non-message traffic (aka files.) should improve routing decisions for files, while not hurting routing for messages too much. Product mix is much more file oriented than message oriented in recent times.\n", + "\n", + "* topics provide a pre-filtering function that substantially reduces the amount of routing work done in the application. each sender now only has to check its own regexes.\n", + "\n", + "* the topic matching is done by the broker, and the broker has it's own parallelization methods.\n", + "\n", + "* by sharing of queues, can parallelize all transfers (except initial one), both reception and transmission.\n", + " \n", + "* can obtain new products by subscribing to other data pumps. One is not restricted to only receiving data sent by others.\n", + "\n", + "Introduced an architectural constraint or weakness in the form of a message broker. Introducing such components is a rare, exceptional event.\n", + "\n", + " * Use of message brokers do not introduce synchronization to an app. (messages are not locks.) they are queues so every process operates at it's own speed. \n", + " * It needs to be well done. RabbitMQ is well done. message brokers are hard.\n", + " * scaling of rabbitmq is, in principle, possible, we have done so in some deployments.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "867e5442", + "metadata": {}, + "source": [ + "Add RabbitMQ\n", + "---------------------\n", + "\n", + "* topic based pre-routing.\n", + "* robust, high performance implementation with good internal design for multi-core.\n", + "* able to scale beyond one node.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6db39240", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gU9f7+/3vTSQgJIYHQi1JERVGaiAiCYMHejw2PHEUR9ViOvaIiej7qUbAX9CAWFESQgwURaYoIiKL0JEBCeu/J7vz+4Ee+JLtJdje7mcnm+bguLpLZnZnXbjabmXvf83rbDMMwBAAAAAAAAAAALCXI7AIAAAAAAAAAAIAzAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCC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/PDDxsCBAxt8HiMiIowJEyYYCxcu9Go/7733ntM2k5KSam7fvn27cc011xgRERH11nDSSScZX331VYP7+fHHH42zzjrL5XN++LU7ceJE448//vD4MVx//fW1ttWzZ89aty9atMg45ZRT6q0/KCjIOO2004zPP//c4327Ulpaajz33HPGySefbNhstgbfL0477TTjnXfeMaqrqz3eT2N/NwoKCoxHH320wd/HqKgoY/Lkyca+fft88tgLCgqMp59+2hgyZEiDj91msxknnXSSMWfOHKOiosLj/fTs2bPW9q6//vpaty9cuNAYMWJEg/sfNmxYo6/bw1z9nnj6r+7faV9x9Tqo+3w0xXnnnee0/YSEBKOsrMyr7f3444/GFVdcYcTFxTX4fMXFxRnXXHONsW3bNq9rLysrM15++WVj6NChbv2MbDabMWDAAOOmm24yvvnmG6OqqqrRfTT0d9tdVVVVxgsvvNDgMUZwcLBx9tlnG2vWrGny/hs7Rty9e7cxefJkIzIyst56OnXqZDzxxBNGcXGxx48XAAB/IsAHAD87//zznU4Q3nzzzWat4YcffjC6d+/u9Ql6SEiI8fDDD3scSDYUxLz99tsNnkS5CgbqOzmrrKw0pk2b1mCwUt8JoFnPjavnavDgwV7XMX36dLf3VVFRYTzyyCNGVFSUx/s57rjjjN9++82jx1bfz2H//v0NBpCu/j344IMe7bspAf5VV13l1oc59f075phjvApum2LGjBlOdcTHxxvl5eXNWocrL7/8cr0f1jX075RTTjG2bt3q0b4aCvDnzJlT74d8rv7dcccdhsPhqLX9iooK45ZbbvHofcLTD1HqC/BLSkqMiy++2KPn8IILLjCysrI82v+RPv74Y6Nr164e/+yOPfZYjz9gbOjvxqpVqzyqo02bNsbixYu9ftwOh8N46aWXjA4dOnj82Hv27GmsWLHCo/3VF+Dn5eW5DJwb+nfdddc1GtS25gB/1apVLh+Pp6+XHTt2GGeffbbHz1tQUJBx4403evyBwcqVK43evXs36Wf2v//9r9H91F3H0wB/8+bNDX4wX/efzWYzbr/99poP/bzZf0MB/uzZsxv8wLbuv169ehk7d+706DEDAOBP9MAHAD9zNVnkp59+2qw1JCUlaf/+/V6vX11draeeekoXXHCBU2sObzz//POaMmWKSktLm7wtu92uSy+9VHPmzJFhGB6vb4Xn5qWXXtL48eO1efNmr+twt3dvbm6uJkyYoBkzZqikpMTj/fzxxx869dRTtWTJEo/XPdLevXs1YsQIrV+/3qP1nnnmGT388MNN2re71q1b16TX+19//aURI0bou+++82FVDfvyyy+dlk2ePNnrCbN9wW636+abb9btt9+uvLw8j9dfv369Ro0ape+//77JtTzzzDOaNm2aKioq3F7nP//5jx566KGa7ysrK3XhhRd61JKourpakydPdvnz8URFRYXOPvtsLVy40KP1Fi9erHHjxiknJ8fjfc6YMUNXXnmlUlNTPV5327ZtOvPMMzV//nyP161r6dKlGj9+vEd1lJWV6ZJLLtHy5cs93l95ebmuvPJK3XnnnV49bykpKZo4caLeeustj9c9Uk5Ojk477TSP33M/+OADTZ48uUn7DmSjR4/WiSee6LR86dKlbm9jxYoVGj58uMu5jhrjcDj0zjvv6PTTT1dGRoZb6yxbtkxnnXWWkpKSPN5fc/rll180duxY/fHHH26vYxiGXn75ZV1zzTVeHcs15MEHH9Rtt92m8vJyt9dJTk7WqFGjvHrfAwDAH+iBDwB+NnLkSL355pu1ln333Xd65ZVXNH36dFNqSkxM1Mknn6xjjjlGvXr1Urt27RQZGamSkhJlZmbq999/1/Lly5WZmVlrvaVLl+rBBx/Uc8895/W+v/nmGz377LM134eHh2vs2LEaM2aMEhMTFRISogMHDujnn39WcXFxo9t79NFHa4VicXFxOvvsszV06FB17NhRZWVlOnDggP73v/+51de0uZ+bBx98UDNnznR529FHH60JEyaof//+SkhIkGEYysvL086dO7Vx40Zt2LDBo4A5Pz9fp556qrZv3+5023HHHafTTz9dxx57bE2v3MzMTK1fv17Lli1TUVFRzX2Li4t12WWXae3atV7N51BUVKSzzz675sTYZrNp5MiRGj9+vHr06KG2bdsqKytLa9eu1aJFi5xOumfOnKnzzjtPw4cP93jf3goODtZJJ52kY489VgMGDFCHDh3Url07GYahwsJC7dq1Sz/99JPWrl0rh8NRs15xcbGuvPJKbd68Wd27d/drjUVFRfr111+dlp9xxhl+3W9jbrrpJr377rtOyyMiIjRx4kSNHj1anTt3VllZmZKTk7V48WJt3bq11n0LCwt19tln6/vvv9epp57qVR1ffPFFrSC+U6dOmjRpkk466STFx8erqKhIv/32mz799FOnQO3ZZ5/VhRdeqGHDhmnatGm1ArsBAwZo0qRJ6tu3r2JjY5Wbm6s1a9bos88+q/VBgcPh0NSpUzVmzBive8Tffffd+vHHH2u+j4+P18UXX6xBgwYpPj5e2dnZ+v3337Vw4UJlZWXVWnfr1q2aOHGi1q9fr9DQULf2N2PGDD366KNOy0NCQjR27FiNHz9eXbt2VXV1tfbv369ly5bpp59+qhXAVVZW6pprrlFISIguv/xyrx73li1b9MADD6iqqkqS1KZNG40bN06jR4+u+buxf/9+ffPNN1qxYkWtdaurqzVlyhRt27ZNMTExbu2vqqpKEyZM0OrVq51uO+qoozR27FgNGjRIcXFxCgkJUXZ2tn755RctW7as1vNeXV2tm2++WYmJiTrvvPM8ftzV1dW66KKLagWhgwcP1sSJE9WnT5+a19vPP/+shQsXqqCgoNb6H374oS688EJdeumlLrcfFxenE044QdKhn9Nff/1V6/bu3bs32p/86KOP9vhxWcW4ceO0ZcuWWsvWrVvn1rpLlizRJZdcUvOaPCwsLExnnHGGhg8fru7duysmJkbFxcVKTk7W999/7/Sa2rBhgy688EL9+OOPDf5e5uTk6Prrr3f68DEkJESjR4/WyJEj1atXL0VHR0s69J6ZmZmpbdu2adOmTS7/7vtDcnKyzjzzTKfXoiQde+yxOv/889WnTx9FR0crIyNDv/76q5YsWVLz4e7HH3/s03mi3njjjVrHWAkJCTXHiAkJCSovL9fu3bu1aNEibdu2rda6mZmZuvnmmz36UAcAAL8xc/g/ALQGmZmZRps2bVxeonveeecZq1ev9nsN7733ntGnTx/jqaeecrv9SXV1tTFv3jwjMTHR6TLnDRs2uLUNV5fAH9kr+pJLLmmwR3HdS8tdXR59eHvBwcHGI4880mDfUleXqpv13BiGYSxYsMDl6+LEE090a56E7Oxs46233jIGDRrkVmuBCy+80GlfI0eONH766acG18vLyzPuuusupxZFvXr1MgoLCxvdb919HnkZ+/Dhw41ff/213nWTkpKMk046yWkbEydObHS/h9evu667LXT69u1rXHzxxcbChQuN/Px8t9ZJTk42rrrqKqd9nnvuuW6t3xQ//vijy9dTdna23/ddn48//thlTRdccIGRmppa73pLlixx2SqlV69ebv0sXLUGOdw2Jzg42HjyySfrbV1RUFDgskXNhAkTjM8//7zm+4SEBOPTTz+tt4Zdu3YZ/fr1c9rOM8880/gTZzi30AkPD6/5HQwKCjLuu+++eh9DWVmZcf/99xtBQUFO+3/88cfd2v+aNWtc9vYfNWqUsWPHjnrXW7dunTFgwACn9WJiYozk5ORG9+vq78aR7xnXXnutkZaWVu/6P/zwg8s+5DNnznTrcRuGYdx+++1O6w8cOND4+uuvndopHam0tNSYOXOmERoaWmvd2NhYt+ZLqdtC58jH3a9fvwZb8mRmZhoTJ050qnvAgAFuPeamzhfia/5uoWMYhvHZZ5+5PKYoLS1tcL29e/casbGxtdYLCQkx7r33XiMzM7PBdTdv3mycfPLJTvu96667GlzPVXu0M8880+15eJKSkowXXnjB6Nu3r99a6DgcDmPcuHFO6yYmJjY4F0dRUZFxxx131Nzf1TGzty10Dv8OhYaGGjNnzqz3PdPhcBgvv/yyy/dMb+bfAQDA1wjwAaAZ3HvvvS5DrMP/unbtatxwww3Gm2++aWzZssWtCcY8UVhY2GDo0JCUlBSjR48eteq96qqr3FrX1Qn44X/Tp0/3uCZXJ2eHw6yGgrSGmPXcZGZmGtHR0U6P5bLLLvOqX3ljwdgbb7zhtK9bb73Vo8f+7rvvOm3j2WefbXS9+l4DkyZNcqv/b05OjtOElUFBQW4FF00JpdwN7V15/PHHa+3TZrMZf/31l9fbc8dbb73l9Fh79erl1302pKioyGXP+3/84x9uve727t3rMsS/7bbbGl23vt7eQUFBbk2MW1lZ6dS/2WazGfHx8YYko3Pnzm71R969e7dTv/2+ffs2up5hOAf4R/577bXX3NrGa6+95rRuaGiosXv37gbXczgcRv/+/Z3WPeecc9yanDUnJ8c4/vjjXf7ON6ahvxszZsxw63GvWbPG6QPHo48+2q11ly9f7rTfCy64wKNJab/55hunEH/q1KmNrlc3wD/8b+jQoUZOTk6j65eXl7vsO+7OQIHWGOCnpKS4fL4be6+uO3dLZGSkR/MdVFRUGGeeeabT72VDAxqGDBlS6/4DBgzw6ljB4XA0+gGFYXgX4Lv6wDYxMbHBD/yO9H//93/1/u57G+BLhz78/O6779yq4emnn3Za/8Ybb3RrXQAA/IkAHwCaQVlZmUeTdUZERBjDhg0zpk+fbixYsMBIT083tf6lS5fWqi8kJMStcLO+IGbYsGE1E5V5or6Ts7vvvtubh+UT3j43DzzwgNPjOO2007x6XhpTVVXlFAydddZZXm1rypQptbbTqVOnRkMEVz8zd0dSH/bqq686bePtt99udD2zQimHw2EMHTq01n7vvfdev+7z0UcfdXqsI0aM8Os+G/LKK6+4DCI9mfB53bp1TkFsVFSUkZub2+B69QX4nkyC/Omnn9b7Hu1uGGQYhjF16lSn9RsL0A2j/gB/8uTJbu/bMAxj8uTJHr9nLlmyxGmdHj16NHiFU1179+51Gklrs9mM7du3N7hefX83Lr74Yrf3bRiGcfnll3v1vI8cObLWOoMGDTIqKys92rdhGMZTTz3lFCI29rfcVYDv7uj9w5YtW+a0jYcffrjR9VpjgF9WVubytfbtt9/Wu84333zjdP+PP/7Y433n5+fXfCB4+N+dd95Z7/3rXlXy9NNPe7xPT9R9jO4E6KNGjXJab9myZR7tt74JupsS4L/88stu77+ystLpg+Nu3bp59BgAAPAHJrEFgGYQERGhZcuWud0Dt7y8XBs2bNArr7yiyy67TJ07d9aYMWP07rvvejQJl6+cffbZio+Pr/m+urpaP//8s9fbe/755xUcHOyL0hQdHa3HH3/cJ9vyhjfPTXFxsebMmVNrWVhYmP773//67Hk50scff6yUlJSa7202m1555RWvtvXoo4/WmksgIyPD44loJemxxx5zux+1JF155ZVOz42rfu9WYbPZdO2119ZatmbNGr/u09UEsYfnMzDD7NmznZbNmTNHQUHuH36ecsopuv7662stKykp0XvvvedxPbGxsXrwwQfdvv+kSZNcTv575plnaty4cW5vx1X/8U2bNrm9/pHatGmjWbNmebTOrFmz1KZNm1rL3nvvvQYn8nX1s/v3v/+tqKgot/fbu3dv3XfffbWWGYbh9N7njqCgII/nF7nmmmucljX2nrFmzRqnHugvvvii23MGHOmuu+6q6UcuHZqA2JvJTu+44w716NHD7ftPmDBBCQkJtZZZ+b3STBERES5/xxuaFL7u799pp52mK664wuN9x8TE6I477qi1bNGiRfXe/8h5aCSpQ4cOHu/Tn/766y+nv3ETJ07U2Wef7dF2XnzxRZ8eB/Xp00fTpk1z+/6hoaFOP88DBw44zXsEAEBzI8AHgGYSGxurxYsX64MPPvB40jfDMLRq1SrdeOON6t+/vz788EM/VelaUFCQjjrqqFrLfvrpJ6+21bdvX40ePdoXZUmSrrjiCrVt29Zn2/OUN8/NqlWrnAKCK664Qj179vR5fZL02Wef1fp+zJgxXk882L17dx1//PG1lq1atcqjbURFRelvf/ubR+u0b99effv2rbVsx44dHm2judWtd9OmTU4THvpSWVmZ0zJPPiTxpf379zv9fIYMGaKhQ4d6vK1bb73Vadm3337r8XauuOIKjwLoNm3aqH///k7Lb7zxRo/2O3jwYKdl3r52L7jgAnXs2NGjdTp27KgLL7yw1rLc3Fxt2LDB5f0rKyudfqcTExN10UUXebRfSbr55psVEhJSa5k3P7szzjjD6X22Ma4muW7sea/7Xtm7d2+vJ4Fu06aNxo4dW2uZp++VkvSPf/zDo/sHBwc7TQJq9fdKM7Vv395pmav3UunQ7833339fa9mUKVO83ve5555b6/uUlJRaH7YfqW5g7+8PhD1V93mRPH+vlKQePXpo/PjxvihJkvT3v//dow+NJWnYsGFOy/gdAgCYLaTxuwAAfOXwqNyrrrpKy5cv1/z58/XVV181ONqrrn379umaa67Rd999p9dff93l6DF3/PHHH/rll1+0detWpaSkqLCwUEVFRfWOyty9e7dTHd4YM2aMV+vVp25A4gv+fm5++OEHp2V1R2v7imEYWr16da1lI0eObNI2e/fura1bt9Z8v3nzZo/WHzFihMLCwjze71FHHaXt27fXfF9QUODxNpqiuLhYP/74o7Zu3ao///xTOTk5KiwsVElJiRwOh8v7H6miokIZGRnq1q2bX+ozDMNp2ZFXSzSntWvXOi1zNRLdHUOHDlXv3r2VlJRUs2z9+vUyDMOjx+fNB4c9e/as9VqXDo249URcXJyio6NrjaDNz8/3uBZJTkG8uy6++GJ99NFHtZb99NNPLh/Lpk2bnK70uvDCC52CeHckJiZq1KhRtd7zduzYoZycHI9GEJ9++uke77tjx46KiopSSUlJzbLG3jPqBuy+eK88kqfvlUcddZS6du3q8X7rftjR3O+VLYmr9+763ldWr17t9D7blNdI3deHdOg14urD/OHDh2vx4sU133/44Yc65ZRTdMstt5j2Pn+kugMXbDabzjrrLK+2NWnSJH399de+KMur9w5XHxbyOwQAMBsBPgCYICQkRJMmTdKkSZNkt9u1ZcsWrVmzRhs3btSmTZu0Y8cO2e32Brcxd+5clZSU6NNPP3V7vxUVFXrllVf03nvv6c8//2zSY/A2gDrppJOatF9/ba85n5u6LWeCgoI0YsSIJu2zPn/99Zdyc3NrLXv//fe1dOlSr7dZ9wOK7Oxsj9avOzLdXXVHkzfXCfWvv/6q559/Xl9++WW9IzPdlZ+f77cAv26blMP7M4OrFjFDhgzxentDhgypFeAXFBRoz549Hl1J4s1VJ0e2QJEOPcddunTxajtHBvjevnbrjqx2l6v3yfraqvj6Zzd06NBaAb5hGNq8ebNHo2yb8p7hboBfVFTk9GHNt99+qxNPPNGrfUtSenp6re8D/b2yJXL1HunqvVRy/cHkJZdc4tMAvb7XyA033FArwDcMQ9OmTdOrr76qG264Qeeff77Xrxdf+P3332t937dvX6f3T3e5umrJW948J66uXON3CABgNgJ8ADDZ4cvdjwxmSktL9fPPP2vlypVasGBBrVHHR1qwYIFeeeUVTZ8+vdH9rF27Vtdff7327Nnjk7q9PZnxtP1Dc2yvuZ+bjIyMWt/36tXL6xPdxhw4cMDlMlfLvZWTk+PR/ePi4rzaT90+1P5sR3N4+//85z/12muvuRyl6Q1/hgCunlezAnxXIdQxxxzj9fYGDhzoch+ehPKuWmU0pu5rzpttuNqON6/dkJAQj9vIHNanTx+FhYWpsrKyZll9PZ2b62fnieZ4zzh48KDT73lmZqZPe1+b9V5ZXV3t1XYCXWlpaa3ficPatWvn8v6u/m7W/dCnqep7jVxwwQW68MIL9cUXX9Ravm3bNt1zzz2655571L17d40aNUpDhw7VyJEjdfLJJ3t15Yw36tbdq1cvr7fl6soEb3nzO+Rqzgt/H28AANAYeuADgAVFRkZq7NixevLJJ/XXX39p+fLlOvbYY13e96mnnlJpaWmD21u5cqUmTJjgs4Ba8v5kpr4TY281dXtmPDd1R8R7Gwq6w9PAyBuejkr3ZkLI5lZVVaXLLrtMc+bM8Vl4f3i7/uJqZH/dD4uai68n1HX1O1L396gxvnjdmfnajY6ObtJI37rvlfV9uBOoP7vGNMd7paeT0LeE98qWrL73x+7du7tcbvbf0w8//LDB+WP279+vjz76SHfddZdGjBih9u3b6+KLL9ann37a4KTVvlD3faMp86/4cu4WfocAAIGCAB8AWoCJEyfql19+0dlnn+10W2Zmpr788st6183Pz9cVV1zhFPIHBQVpwoQJmjlzppYvX65t27YpOztbxcXFstvtMgyj1j9v+oi64uvRYE3ZnlnPTd05D/w5Ca+rMA6NmzVrVq12BYd17dpVt956q+bNm6f169dr//79ys/PV3l5udPrYuXKlc1as6sJV5OSkjwOS33hyHYx0qF+yJGRkV5vz9Xks3X3Eeg8mYDXnfXre/5cLW/KvlvKz473ytbH1UTOwcHB6tGjh8v7m/0aiYyM1IcffqhvvvlGY8aMafQDveLiYi1atEhXXHGFjjrqKL3xxhsu50rxhbofEHgzz81h3s7tBABAIKOFDgC0EG3atNHHH3+so446yqn9wIoVK3TllVe6XO/pp59WVlZWrWVDhgzR/PnzPeoN2tTe31Zk1nPTrl27WqFq3clOfclVL98vvvhCF1xwgd/22dJlZmZq5syZtZaFhITo+eef12233eb2h0bN/TszePBg2Ww2p4Bmw4YNXk8m6K26LaEMw1BpaanXIf6Rfczr20egc/UcNGX9+p4/V8ubsu+W8rNz9V750ksv6Y477jChGjSHuvPRSIdaPkVERLi8f93XSGxsrCmh/plnnqkzzzxTKSkpWrp0qVatWqW1a9cqLS2t3nVSU1M1depUffXVV/rss8+aFLC7EhMTU+sKhaZ8SFd3kAMAAGAEPgC0KO3atdPkyZOdlu/YsaPedT7++ONa33fv3l3fffedxxN7mTGK19/Mem46dOhQ63t/BgDx8fFOy46cDBTOvvzyS6erMmbNmqU777zToys+mvt3Jjo62uUkp99//32z1iG5bpvSlH78rtb1tj94S1VUVNSk0bN1Q7H62uK01p8d75Wtz4oVK5yWnXrqqfXev+5rJD8/37R5RiSpZ8+emjZtmj799FOlpqYqJSVF8+bN00033VTvZOlLlizRtGnTfF5L3feNprQbao5WRQAAtDQE+ADQwgwbNsxpWX0TAv71119Ok67dfvvtHvcXraqq8umkp1Zg5nOTmJhY6/vk5GS/tZTo1KmT0zJfT7oXaL799tta37dv31633Xabx9vZu3evr0py2/nnn++0bO7cuS4navSnhIQEp2V//fWX19v7888/nZa5ClwDWXV1tdevqaSkJKfXQH0TgLfWnx3vla3LihUr9McffzgtP++88+pdx+qvkR49eujqq6/WG2+8of3799fM8VPXO++8o23btvl033XnDfjjjz+8/sDRSs8pAABWQYAPAC2Mq4C5vlHB+/fvd1p22mmnebzPzZs3ezz5ntWZ+dyccsoptb53OBwuL+X3hUGDBjm1A1i+fLlf9hUo6r42hg8f7lW7AX/9TBty/fXXKyio9uFdVlaWFixY0Kx1nHTSSU7LNm7c6PX2fvnll1rfx8bG6qijjvJ6ey3Vr7/+6rP1XF2tIfn/Z2ez2Vzuw2wdO3ZUr169ai1bu3atJfv1o+n+7//+z2lZYmKixo8fX+86rgZQ/O9///NpXb40ZswYff3117rppptqLTcMQ4sWLfLpvuo+NwUFBdq+fbtX2/rpp598URIAAAGFAB8AWpiMjAynZa5GhUmuR+Z707rgk08+8XgdqzPzuRkzZozTsv/+978eb8cdERERGjVqVK1lBw8edNk6AIfUfW1487rIzs5u9klspUMjMC+++GKn5ffff3+ztnpw1Ybis88+82pbv/76q1MrkxEjRjQ6gWMg+uKLL7xab+HChU7LRowY4fK+J510ktOHfl988YXsdrvH+83IyNDq1atrLevfv78lW+hIcgpvKysr9emnn5pUTfNyNRDAm595S/Duu++6DN5vvfXWBj+sPfPMM52WffLJJ6qurvZpfb72zDPPKDg4uNYyX49yrzswQZLmz5/v8XYcDodTe0MAAECADwAtjqt+1vWNRI2KinJaVl+7nfrk5+fr3Xff9WidlsDM5+b000936hf76aefKiUlxeNtucPVhLWPP/64X/YVCOq+Njx9XUjSnDlzTLtq5amnnnIK4w4cOODzvsfl5eV69dVXXd7WrVs3HXPMMbWWbdy40asR5K724aotRGvwxRdfOE283ZisrCwtXry41rK4uDiXo4klKTQ0VGPHjq21LD093asPD958802ncNPKPztX75VPP/10s7egMoOriYX9OcG6WTZs2ODyvbBLly666667Gly3a9euTleuJCUlae7cub4s0ec6dOjg1BqroKDAp/uYMGGC03HNW2+95fF+Pvroo4Br2QgAgC8Q4AOAny1ZssRnE+Ht2bPH5WjASZMmubx/586dnZZ98803Hu3ztttuM3WSNn8x87mJjIzU9OnTay2rrKzUtddeK4fD4fH2GnPjjTc69d1fs2aNZs2a5fN9BYK6r41169appKTE7fW3bdummTNn+rost/Xv318PPvig0/L58+fr9ttv98lrbNeuXRo1alSDo5NdhWS33XabR32RN2zY4BSORUVF6YYbbnB7G4GkrKxM999/v0fr3H///U6TMt9www0KDw+vdx1XP7t77rnHaTsNSUlJ0bPPPltrmc1m88sEmr5y7rnn6sQTT6y1LCkpSXfccYc5BTWj6Ohopw/+zJjHw5/efvttjRkzxunDVZvNptdff93lB/t1PfTQQ07L7rnnHu3cudNndfpaeXm58vLyai1zNddFU7Rp00bXX399rWUZGRm6++673d5Genq6R/cHAKA1IcAHAD/76quv1K9fP91www1e9wOVpLS0NF100UVOAUpCQkK9PVsHDx6stm3b1lr2n//8x+3RTTNmzNCHH37oXcEWZ/Zzc8cddyg2NrbWstWrV+uqq65SRUWFx9traPR+mzZtXIYODz74oGbPnu3xvg5bvny5br31Vq/Xt6q6cyEUFxfriSeecGvd5ORknX/++V79DH3p4Ycf1umnn+60/JVXXtHll1+uzMxMr7ZbXl6uZ555RieeeGKjo+knT57s1Crlp59+cntC4JSUFF166aVOHzjceOONTr87rcm7776rt956y637vvXWW05XCYWGhuqWW25pcL1zzjlHAwYMqLUsOTlZf/vb39xqF5KXl6cLLrjA6e/Veeedp379+rlVuxlsNptmzJjhtPz111/XAw884PWHX+vXr9ff/va3ppbnV0FBQRo4cGCtZV9//bVfPlRubitXrtSkSZP0j3/8Q2VlZU63P/roow1OXnukiy66SEOGDKm1rKCgQGeffbbXE8MWFRXp+eef17x581zevnv3bs2YMcPjq28Oe+ONN5z+Jp1wwglebashd911l9Nx1TvvvKP77ruv0ddRamqqxo0b57JNJAAAIMAHgGZRXV2tuXPn6phjjtGIESM0e/ZsHTx40K11S0tL9frrr2vw4MH6/fffnW5//vnnnfoVHxYaGqoLL7yw1rK8vDyNGzeuwf6naWlp+tvf/qZHH320Zlm7du3cqrelMPu5iYuL0/vvv+/Ux/vTTz/VKaecou+++67RbeTl5endd9/VCSecoMcee6zB+06bNs2pPYTD4dD06dN10UUX6bfffnOr7qSkJM2aNUuDBg3S2WefrR9//NGt9VqSSy65xGki2Oeff16PPPJIg+HlRx99pFNOOaVm1KqZvzOhoaFatGiRjjvuOKfbPv/8cx111FF69NFH3f7AKiUlRU899ZR69+6thx56yK2R2FFRUXrttdeclr/66qu67LLLGgxqli1bplGjRjlNKNyrVy89+eSTbtUcaMLDw2veL6ZOnaqHHnqo3g+KKioq9NBDD2nq1KlOtz300EONTgBss9n0zjvvOPXNXrx4sSZMmKDdu3fXu+7PP/+sUaNGOb2nxMbG6pVXXmlwv1YwadIkpyukJOnZZ5/V2LFj3X7PO3jwoF555RWNHDlSI0eO1JdffunrUn1u5MiRtb7fsWOHpkyZ4rf2bv5y4MABLVy4UPfdd5+OO+44nXHGGfrqq69c3vfhhx/2uKXcRx995PTh5N69ezV8+HA9/fTTbrWNcTgcWrlypaZOnaoePXroX//6l9LT013et7i4WI8++qh69Oiha665RosWLXL5QURdlZWV+ve//61777231vLg4GBdeeWVja7vqe7du+u5555zWv7cc89p2LBhWrhwodPfjuTkZD3zzDM65phj9Oeff0py3U8fAIDWznm2IgCAX/3888/6+eefNX36dPXq1UvDhw/XwIEDFR8frw4dOshms6mwsFApKSnaunWrVqxYUW/7jssvv9zpkuW6HnnkEX3yySeqqqqqWbZz504NHjxYZ511ls444wx169ZN1dXVOnjwoH744Qd99913tYKhv//979qzZ49WrVrlmyfBIsx+bs4//3w99NBDeuqpp2ot37x5s84880z17dtXEyZMUP/+/ZWQkCDDMJSfn6+dO3dq06ZNWr9+fU3tgwcPbnBfNptN8+bNcxmsffHFF/riiy90wgknaMyYMerbt686dOgg6VCf/+zsbG3dulW//vprwLVUcKVfv3665ppr9MEHH9Ra/tRTT2nu3Lm69NJLNWjQILVt21a5ubnasWOHvvzyS+3Zs6fmvpGRkZo1a1ajI539qX379lq1apXOO+88rVu3rtZtxcXFmjFjhmbMmKGBAwdq7Nix6tatmxISEhQbG6vy8nJlZGRox44dWrdunbZt2+ZR65vDLr/8cn399ddOo8A/++wzffXVVzr77LN12mmnKTExUeXl5UpKStKXX36pLVu2OG0rNDRU8+bNU0xMjMd1BILExESde+65evXVV+VwOPTMM8/orbfe0iWXXKJBgwapQ4cOysnJ0datW7Vw4UKXV1mcfPLJLtsruTJy5Eg99thjtT6slA6NZh44cKDGjRunM844Q127dpXdbtf+/fu1bNkyrVu3zum1YrPZ9MYbb6hHjx7ePwHN6MUXX9TOnTv19ddf11r+448/6vTTT1f//v01ZswYHXvssYqLi1NoaKjy8/OVm5urbdu26ddff9WOHTta3Oj1v//973r99ddrLXvvvff03nvvKSEhQQkJCQoNDa11+5AhQ/T22283S31ffvmlU4sjSTIMQ8XFxcrPz1dBQYFbk+926NBBb7zxhi655BKP6zj66KP16aef6pxzzqk1P0JJSYkefvhhzZw5U6NGjdKpp56qzp07KzY2VqWlpcrPz9f+/fu1adMmbdq0yeMWfOXl5frwww/14Ycfqk2bNjrxxBM1ePBg9e3bV7GxsYqOjlZFRYXS09P122+/afny5S7fB+6//351797d48ftjqlTp2rdunVOVxP8+uuvuuSSSxQaGqpOnTopOjpamZmZysnJqXW/2NhYzZ07V/3796+1vO6HiQAAtDYE+ABgouTkZCUnJ3u17vXXX6933nmn0fv169dPs2fP1s0331xrucPh0LJly7Rs2bIG1z/jjDP06quvauLEiV7VaWVWeG5mzJihuLg43XvvvU6hw65du7Rr1y6vt11X27ZttXr1at1www36/PPPnW7/7bff3B6JH+hefvllbdiwwant1YEDB/TSSy81uG5oaKgWLFigyMhIP1bonri4OP3www966KGH9MILL7gMtv7888+akY+e6Nevn9PITlfefPNNBQcHO7V9KSsr08KFC7Vw4cJGt9GuXTstXLhQp556qsd1BpIXXnhBv//+u1avXi3p0AS1dQPX+gwaNEhff/21UwDbkEceeUSGYThd4VNVVaXly5dr+fLljW4jNDRU7733ni6//HK392u24OBgLVmyRHfddZfLNmM7duzQjh07TKjMv4YOHaobbrhB7733ntNtWVlZLlu4NGc7q7y8PKde7p6KiorSP/7xDz3yyCNOo+g9MW7cOK1evVqXXnqp05VCJSUl+vrrr50+APKlsrIyrV+/XuvXr/dovSuuuKLRK/aawmaz6f3331dwcLDef/99p9urqqrqvfIrNjZWS5YsUZ8+fZxuC7SrQAEA8BQtdADAz6699lpdffXVPjvJ7dOnjxYvXqy5c+e6PSLppptu0quvvqqwsDCP9vX3v/9dy5Yta3Cyw5bOCs/NP//5Ty1fvlzHH3+819vo2LGjW/eLjo7WZ599ptdee01du3b1en+S1KNHj4CdTDQmJkbfffedRowY4dF6Xbp00XfffadzzjnHT5V5LjQ0VM8995w2b96sSZMmObVt8lTfvn01Z84cbdu2Teeee26j9w8ODtabb76p//znP2rfvr3H+xsxYoTWrFmjcePGeVNuQAkPD9f//vc/p3ZYjTn//PO1YsWKmitrPPHoo4/qo48+UpcuXTxed+DAgfr222919dVXe7yu2UJDQ/XKK69owYIFTe7b37FjxxYzX8hrr72mO+64w6mNWEsWHBys0aNH6+WXX9b+/fv14osvNim8P2zYsGHatGmTbrjhBo8+GKvLZrNpzJgxTvOvHBYZGano6Givty8d+gB/5syZ+uijj5pUqzuCgoI0d+5czZ8/X4mJiW6tM3bsWG3YsEGjRo1y+SFNa73yCgCAwwLnyAwALOrUU0/VvHnzlJmZqRUrVujRRx/VGWec4TTRV0M6deqkq6++Wl999ZV27Nih888/3+M6brnlFm3atElXXHFFgydvYWFhOu+88/Tjjz/qnXfeCejw/jArPDfjx4/Xb7/9po8//lhnnXWWW6O3+/btq9tvv12bNm1y2Xe2IVOnTtXevXv11ltvafz48W7tLygoSIMHD9a9996rlStXKjk5WXfffbdH+21Junbtqh9//FGzZ892OSLwSD179tSMGTO0fft2jR49upkq9Mzxxx+vJUuWaMeOHXrsscc0ePBgtz4EPDy55a233qp169Zp586duvXWWxUS4tmFnLfffrv27Nmjhx9+2GmC1LoiIiJ05pln6vPPP9f69eub9OFWoImKitIXX3yhBQsWaOjQofXez2azadSoUfr888+1ePFixcfHe73PK6+8Urt379Zzzz2nk046qcEPgUJCQjRq1Ci9/fbb2rp1q8vJlFuSSy+9VH/99Zc++eQTTZo0ye0g8ZhjjtH06dP11VdfKTU11eP3aLOEh4frpZdeUnJysp577jldfPHF6t+/vzp06ODxB93NJTg4WJGRkerQoYP69++v0aNHa/LkyZo5c6a+/vpr5efna9WqVZo+fbpXHyI2JD4+Xu+++652796te+65R8cee6xbH5JGR0fr3HPP1YsvvqikpCStXLlSw4cPd3nffv36KTs7W998843uuusuDR8+3O2fxTHHHKMnnnhCu3bt0v3339/kD3A9cdVVV2nPnj365JNPdMUVV2jgwIFq3769QkJCFBcXpyFDhuiOO+7Q2rVr9f3336tv376S5HIuAF984AIAQEtmM7xpaAoAaDLDMJSamqpdu3Zp3759KiwsVFFRkWw2m9q1a6fo6Gh17txZxx9/vNsjmNxVUlKidevWae/evcrNzZXNZlNcXJz69u2roUOHevThQqCxynNTUVGhX375RQcOHFBWVpYKCgrUpk0bxcTEqE+fPho4cKBPXxeVlZX69ddfdeDAAWVnZysvL08hISGKjo5WfHy8+vXrp379+qlNmzY+22dLs3PnTm3YsEFZWVkqKSlRVFSUunXrpkGDBjn1620pSkpK9PvvvyspKUkZGRk1Ewy2b99e7du3V5cuXTR48OAmj/50JSUlRVu2bFFWVpays7MVFhamjh07qkuXLhoxYoQlWhCZafLkybVaUPTs2dNly7UDBw7ol19+UXJyskpKShQXF6fOnTtr2LBhTb7Kpj4ZGRn65ZdflJmZqaysLAUHByshIUGJiYkaMWJEQI+Wtdvt+u2335SUlKScnBzl5OTIZrMpOjq65m/FgAEDWvXf0dYuKytLv/76q7KyspSTk6Pi4mJFRUUpOjpa3bp104ABA9SzZ88mhekVFRXavXu39uzZo7S0NBUVFamiokKRkZGKiYlRr169dMIJJzTpgzuzvPXWW7rppptqLduzZ0+jH6QDABDICPABAAAAi3E3wAeAQHLhhRdq8eLFNd/Hx8e7nH8BAIDWhBY6AAAAAADAVNu3b9eSJUtqLatvbgAAAFoTAnwAAAAAAGCaiooKXX311XI4HLWW122nAwBAa0SADwAAAAAAmmz27NnasGGDR+ukpaVp3Lhx2rRpU63lffv21cSJE31ZHgAALRIBPgAAAAAAaLKlS5dq+PDhGjx4sJ544gmtW7dORUVFTvcrLy/X6tWrdfvtt6tfv35au3ZtrduDgoL09ttvN2myXwAAAkWI2QUAAAAAAIDAsWXLFm3Z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PUnavn27PvnkE23atEnZ2dkKCgpSt27ddNppp+lvf/uby/Y9b7zxht566y2ddNJJevPNN7VixQotXLhQO3fuVH5+vqZMmdJgvc3FZrPRTxQAgCay2+2qrq42uwwgIHXs2JFWjwAsgwAfAOoRKAH+kTIyMtS7d29T9p2WlqaHHnpIOTk5Cg8PV0hIiHJycrR8+XKtXLlSzz//vEaOHOlU74033qj09HRJUlBQkNq2bav8/Hzl5ORo9+7dSk5Odgrw7Xa7XnzxRX388cc1y9q0aSO73a6kpCQlJSVp5cqV+uGHH1zWumLFCj300EOqrq5WVFSUQkI8/3NZWFio6667TikpKQoNDVVYWJgKCgq0atUqrV69Wg899JAuuOACl+u+8cYbevvtt2taOEVERKi6ulq7du3Srl279OWXX+qll17SgAED6t3/iy++qA8//FA2m03R0dGWGu3eoUMHhYaGml0GAAAtWiAeqwJWEBISQqtHAJZCgA8A9QiUk6KBAwfKZrPJMAy99NJLmjVrlnr27Nnsdbzwwgtq27atZs+ereHDh8tms+mPP/7QU089pd27d+uBBx7Qp59+WqvX5Jtvvqn09HR16dJFDz/8sE4++WQFBwfLbrcrMzNTa9eu1cGDB532NWfOnJrw/vzzz9f1119f85hzc3O1devWBvv9P/HEExo+fLj++c9/1lwZsG/fPo8e71tvvaWgoCA9++yzGjNmjEJCQpSUlKSZM2dq06ZNeuaZZ9S/f3+nEH7+/Pl66623FBUVpRtuuEGTJk1SfHy87Ha7du7cqZdfflm//PKL7r77bi1YsMDlxFrbt2/Xpk2bdN111+naa69V+/btVVlZqZycHI8egz/YbDZ17tzZ7DIAAGjxAuVYFbCaLl26WGrwCwDwjgQA9QiUk6IuXbrowgsvlCTt3r1bl156qa6++mrNmjVLixcv1u7du5tlst6Kigq98sorGjFiRM3lqMcdd5xeffVVxcTEqKSkRHPnzq21ztatWyVJt956q4YNG6bg4GBJhyZA7dy5sy699FJNnz691jopKSmaN2+eJOm6667To48+WusDi7i4OI0ZM0YzZ86st9bevXvrhRdeqNXWp0ePHh493uLiYj377LMaP358zQj+3r176+WXX1aPHj1kt9v12muv1VonPz9fr776qmw2m55//nlNnjxZ8fHxNY/5mGOO0SuvvKJjjjlGGRkZ+uKLL1zuu7S0VFdffbVuv/12tW/fXpIUFhZmieC8U6dOCgsLM7sMAABavEA5VgWsJCIioub4GwCsggAfAOoRSJOC3XfffZoyZYratGkjwzC0Y8cOLViwQDNmzNCVV16piRMn6oUXXvDrCO3x48e7bN8TFxeniy++WJL0zTff1LotOjpa0qHe+e5aunSpHA6HYmJivO73fu2119Z8WOCtE044wWVv/oiICF177bWSpPXr16u4uLjmtv/9738qLy/XMcccU29f/5CQEE2cOFGS9NNPP7m8T1BQkK6//vom1e8PISEhSkxMNLsMAAACQiAdqwJW0bVrV3rfA7AcWugAgAuBNilYSEiIpk6dqmuuuUY//vijNm3apD///FNJSUmqqqpSbm6u5s+fr2XLlumll15qdIJVb9Q3Aa4kDR06VO+9954KCgqUmpqqrl27SpJGjRqlrVu3avbs2UpJSdHYsWM1aNAgtW3btt5tHR61P3z4cIWHh3tV64knnujVekcaOnRoo7c5HA5t37695rnZsmWLJGnPnj01Ib0r5eXlkuSyfZAkdevWTXFxcd6U7VedO3du8gcjAADgEEbgA77Vtm1bxcbGml0GADghwAcAFwL1hKht27Y655xzdM4550g69Di3bNmijz/+WKtXr1Z+fr7uu+8+LVy40Ovwuz4dO3Z067a8vLyaAP+6667Trl279O2332rRokVatGiRbDab+vTpo5EjR+rCCy906ud/+CqCprSLOdx2pikSEhLcui03N7fm66ysLEmHfi7uvAYPB/l1WTG8Dw8Pb/A5AQAAngnU41XALIfPQQDAagjwAcCF1nJCFB4eruHDh2v48OF6/PHHtXTpUmVkZGj9+vUaM2aMT/flzaWoISEhmjlzpm644QatXLlSW7Zs0R9//KE9e/Zoz549mj9/vqZPn65rrrnGJ/s7zBejxBvaf323ORwOSdIll1yiBx54wOt9W3HSrS5dunA5MgAAPmIYBi10AB+KjY1t8CpfADCT9c7wAcACWuMJ0UUXXVTzdXJycs3Xh8Pshp6TI/u41ycjI6Pe2zIzM2u+djX6vV+/frr55pv12muvaeXKlXr11Vd10kknyW636z//+Y927txZc9/Dk06lpaU1WpM/HfmYGrrtyNHyHTp0kHRosuFAEhkZ6ZOrGgAAwCFVVVUyDMPsMtCCLFmyREOGDNF5551ndimWY7PZGH0PwNIYgQ8ALgRS/3t3RUZG1nwdFhZW83V0dLTy8/PrDeBLSkpqBf712bhxoy644IJ6b5OkmJiYRg+eQ0JCNGzYMB133HEaP368KisrtWHDBvXr10+SNGjQIG3cuFE///yzKioqfN4KyF2HH1NDtwUFBal///41y0844QStWrVKf/zxhw4ePNikNkBW0q1bN0bfAwDgQ63xWLW5GIahFStWaPny5dq+fbvy8vIUFBSkuLg4xcfH69hjj9XgwYM1dOhQRmwHiPj4eEVERJhdBgDUixH4AOCC3W43uwSfSU1NVUpKSqP3W7p0ac3XAwYMqPn6cDD+/fffu1xv3rx5bl2xsGLFCpdBf35+vhYuXChJOvPMM2vd1tB2w8LCalrFHNky5rzzzlNwcLAKCgr0xhtvNFqXv2zZssVliF9RUaF58+ZJkkaMGKHo6Oia28455xyFh4fLbrdr1qxZDb4OHQ6HioqKfF+4j8XExNR6jAAAoOkC6VjVSoqKinTzzTfr/vvv1w8//KD09HRVV1crLCxM6enp+u233zR//nzde++9WrlypdnlwgeCgoICZtAMgMBFgA8ALgTSqKa9e/fqsssu0x133KGlS5fWai1TXV2t7du364knntCHH34oSTr22GN14okn1txnwoQJkqT169frjTfeqGmXk5+frzlz5uidd95xK6ANCwvT7bffrp9//rnmku9t27bp1ltvVX5+vqKiojR58uRa65x33nmaPXu2fv/991ph/v79+/Xwww+rvLxcQUFBOuWUU2pu6969u6699lpJ0gcffKAZM2Zo3759Nbfn5eXpm2++0T333OPO0+e1tm3b6r777tN3331X83pKTk7WnXfeqeTkZAUHB2vq1Km11omPj9f06dMlSWvWrNG0adO0ZcuWmpN0wzCUnJysefPm6YorrtDq1av9+hiaymazqXv37maXAQBAwAmkY1UrefTRR7Vp0yYFBwfrmmuu0cKFC7V+/XqtWLFCa9eu1UcffaTp06fXDHBBy9e1a1eFhoaaXQYANIgWOgDgQiCNagoJCZHD4dDatWu1du1aSVJoaKgiIyNVWFhYq3/qgAED9O9//9tpRPvy5cu1ceNGvfXWW3r77bcVHR1dM/r79ttv1+rVq7Vp06YG6/jnP/+pV199VdOmTVNERISCgoJUWloq6VC4//TTTysxMbHWOjk5OZo7d67mzp2roKAgtW3bVhUVFTWTDNtsNt15553q3bt3rfVuueUWlZSUaMGCBVq8eLEWL16syMhIORwOlZeXS5LfL3n+xz/+oYULF+r+++9XWFiYwsLCaj78sNlsuv/++zVw4ECn9a688kpVVlZqzpw52rhxo6ZMmVLz8yopKal1wm71tjRdunQxrYURAACBLJCOVa1i3759NYMjbrnlFqeBJSEhIerbt6/69u2r66+/vuaYEi1XVFSUEhISzC4DABpFgA8ALgTSSdEpp5yiRYsWae3atdqyZYv27NmjzMxMFRUVKSIiQgkJCerfv7/Gjh2r8ePH1wrvpUOT2L700kv673//q6+//lppaWmy2WwaMWKErr32Wg0bNsytkeBdu3bVhx9+qHfeeUdr1qxRdna24uLiNHToUE2ZMsUphJek2bNn69dff9WWLVuUnp6u3NxcSYdG2Z944om6/PLLdcwxxzitFxwcrPvuu08TJ07U559/rs2bNys3N1fh4eHq0qWLjj/+eE2cONHLZ9Q97dq10/vvv6+5c+fq+++/V0ZGhmJiYjRo0CDdcMMNGjRoUL3rXnfddRo7dqwWLFigX375RWlpaSouLlZUVJS6deumIUOGaMyYMTr++OP9+hiaIjIyUp06dTK7DAAAAlIgHataxc6dO2u+Pv300xu9f3090w8cOKD58+drw4YNysjIkMPhUOfOnXXKKafo6quvdhqwIh2aYPaJJ55Q586dtWTJEv311196//33tXnzZhUWFqpjx446/fTTNWXKFLVr167emn7//XfNnTtXW7ZsUXl5uTp16qRx48bphhtucOMZkIqLi/XJJ5/oxx9/1L59+1ReXq64uDidcMIJuuqqq1wee6alpen888+XJH355ZdyOBx6//339fPPPysrK0vx8fFasmSJW/tvTjabTT179rT8gBgAkCSbwdT1AODkjz/+qBnlDcAzNptNAwYMqDUxMgAA8J3U1FSlp6ebXUZA+e6773T//fdLOjSIZMSIER5vY9GiRZo1a1bNFZNhYWGy2Ww15xVRUVGaNWuW07aPDPCnTZumxx9/XNXV1Wrbtq1KS0vlcDgkSX369NHcuXNdHmMtXrxYTz/9dM19D1+5WlVVpV69eumiiy7Siy++WPMhQV1//PGH7r77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A0ADOZ1OsyMAYSEhIUEWi8XsGEFtxowZ6tL1LH311VdmR2mSQK67MTExuvrqq+XxeLRixYo6P2/ZsmWSpBtuuMGoaECDhXvd3bhxozp07KS5c+cGdSO/qKgoqPMDwcJisSghIcHsGADCAKcTAUADHT9+3OwIQFhgR1PTrVu3Tnv37Fb/AQP00oIFuu2228yO1CiBXneHDRumFStWaMWKFfrNb35zSgP0m2++0YEDB5SWlqZevXqd8et99dVXevvtt7VlyxYdO3ZMUVFR6tChg66++mrdeuutiomJqfV1X3zxhd5++21t375dBQUFstlsSkxMVNu2bXXppZdq+PDhcjgcJ73m22+/1cKFC7V161bl5+fLarUqMTFRqampuuSSSzRs2DClpKTUfL7H49HWrVu1du1aff3118rNzVVBQYHi4uLUpUsXDRo0SDfeeONpD0EtLCzUv//9b61Zs0Z5eXlKSEjQhRdeqHHjxqlbt27q3bu3JOm5556r+fXPffrpp1q+fLm2b9+uwsJCxcTEqGvXrrruuuvOuD7qFu5194cfftDBA/v1hz/8QVu3btM///msbDab2bEaLNBrJhAq4uPjZbWyLxaA/3FlCwANUFpaKrfbbXYMICyEeyPJV87qc63ik1I0atQobdv2rR5/fGpQ3WwGQ93t1auX2rZtq0OHDmnz5s2nNOmrd98PGzbstF/H7XbriSee0JIlS2p+LzY2Vi6XSzt27NCOHTu0bNkyPfPMM2rTps1Jr33hhRf0/PPP1/yz3W6X1+tVVlaWsrKytGHDBnXv3v2khviKFSs0derUmp260dHRioiIUHZ2trKzs7Vp0yalpKSclDs7O1t33XVXzT9HRETIbrfL6XRq06ZN2rRpk95//309/fTTstvtp3yP+/fv17333qu8vLyaNV0ulz766COtWbNGM2bMOO2fUWlpqR555BGtXbu25vfi4uJUXFyszZs3a/PmzXrvvfc0d+5cakgj8Gf2k5sf+ZdemXm/vv/hB73z9mIlJyebHalBaOADxqBmAjAKDXwAaABuiABj2Gy2oNz1GIgio+269fGX1brreZo+/SFt375dr7zysuLj482OVi/BUHctFotuuOEGPffcc1q2bNlJDfyysjJ9+OGHslqtuuGGG3To0KE6v05GRoaWLFmiFi1a6O6779a1114rh8Mht9utb775Rk899ZS+//57Pfjgg3r55Zdr3og5cuSIXnjhBUnS6NGjNWbMGLVq1UqSVFxcrF27dun9998/aba5y+XSrFmz5PV6NWTIEN1zzz1q27ZtTeZ9+/Zp9erVat68+UkZIyIidPnll2vw4MG68MIL1aJFC1mtVpWWluqjjz7Ss88+q82bN+vZZ5/VH//4x5Ne63a7NWXKFOXl5SkxMVGPPPKIBg4cqIiICO3bt08zZszQX//619P+WT/66KNau3at2rVrp3vuuUcDBgxQXFycysvLtX79ej355JPaunWrHn/8cc2ePfsMf3M4EXX3v3pdf4dadz1PmZNv1EW9L9aK5ct0wQUXmB2rXsrLy1VeXm52DCAs/PypNgDwl+DZfgUAASAYGklAKGBHk29ZLBZdfuefdMeTy7Rq9Qfq2+8y7du3z+xY9RIsdfeGG26Q1WrVRx99pNLS0prf/+CDD1RaWqqLL75YrVu3rvP1u3bt0sKFC2W32/WPf/xDI0aMqGkMREZGqnfv3vrXv/6llJQU7dy5U2vWrKl57bfffiuPx6P27dvrD3/4Q03zXvrp8f4LL7xQU6ZMUffu3U9ar6SkRDExMXrsscdqmvfST3P9u3fvrgkTJqh///4n5UxJSdGcOXN07bXXqlWrVjVvIsTGxmrYsGGaM2eOJOmdd945pYm4evVq7dq1SxaLRbNmzdKVV16piIgISVLHjh01d+5cJSUl1fln9Nlnn+nTTz9VixYt9Pzzz2vw4ME1b0rYbDZdfvnl+te//qWYmBh9+umn+v777+v8WjgVdfdk7c+7VPe99KUU31J9+/XTO++8Y3akegmWmgkEu6ioqDpH2gGAr9HAB4B68ng8Ki4uNjsGEBbY0eQf3QfcoN/NX6/sY8XqffHFJ40hCUTBVHdbt26tSy65RGVlZfrggw9qfn/58uWSpOHDh5/29UuXLpXX61X//v3VtWvXWj8nLi5Ol19+uaSf5t1Xqz5Ar7S0VGVlZfXKW/2ayspKnx4S3KNHDyUlJamsrEw//PDDSR/78MMPJUk9e/ZUz549T3mtzWbT7bffXufXrh4tNHTo0DpHmqSkpNSMCTrxzwhnRt09VWLrdvrtC2t11mU36Oabb9a0adMC/nBYGviAMXjTE4CRGKEDAPVUVFQU8DdtQCiwWCxBM94lGKV0SdfvFmzQaw+N1NVXX61//vOf+s1vfmN2rFoFW90dNmyY1q9fr2XLlukXv/iFDh48qM2bNyshIUFXXHHFaV+7ZcsWSdLnn3+u6667rs7Pq97df+TIkZrfS09PV2JiovLz83XnnXfqlltuUZ8+fdShQ4dTDtSt1rZtW3Xs2FH79u3T2LFjdcstt6hv377q2rVrza74ulRWVmrp0qX65JNPtHv3bh0/flwVFRWnfF5OTo7OO++8mn+u3hF/0UUX1fm1T/exb775RtJPu/vfe++9Oj+v+k2f7Ozs034f+C/qbt2i7bH65fSFSulyrh599FFt+/ZbLZg/X7GxsWZHO4XX61VRUZHZMYCwQAMfgJFo4ANAPbGjCTBGYmLiGRuIaJq4xJb69TOrtWzWeN11113aunWb5syZrcjIwLo0DLa6e+WVV6pZs2basmWL9u/frxUrVkiSBg8efMbZ4tWHupaWlp40gqcuLper5tcJCQn629/+pr/85S/as2ePZs2aJemn8Tk9e/bUtddeq0GDBp309xsREaHp06frwQcfVFZWlp555hk988wzstvtOv/883XVVVfphhtuOOUg2oKCAt13333atWtXze/ZbLaT/r09duyYPB7PSRmrf1+SWrZsWef3VdfOerfbrcLCQkk/Nejr82TGz9dH3ai7p2exWHT1Xf9PyZ16aNFf79Bl/Qdo+bKlJ42eCgQlJSWqqqoyOwYQ8qxWqxITE82OASCMBNZdGgAEsGBrJAHB6nQzsOE7EZFRuunhf6p11/P0zOzx2rFjh958841TDi01U7DV3ejoaF133XV66623tHTpUr3//vuSftqZfyYej0eSdP/992vs2LENXrtPnz5atmyZPv74Y3355ZfaunWrDhw4oLVr12rt2rVasGCBnnnmmZMa5GeffbYWLVqktWvX6osvvtDWrVu1Z88ebdy4URs3btT8+fOVkZFx0kifJ598Urt27ZLD4dCECRPUr1+/Uxry119/vXJycup8eqKupwJO58Sm5PTp0zVo0KAGfw3UjbpbP+ddfYtatO2izMm/0EW9L9bSJe/o0ksvNTtWjWCrmUCwSkxMrDkDBgCMQMUBgHqoqKhgJx9ggIiICOYwG6zvyPv062dW64uNX+mSPpcGzMGfwVp3q5v1r7/+unJyctSlSxf16NHjjK9r0aKFJGn37t2NXjsmJkbXX3+9/vrXv+rtt9/We++9pwce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SXN1819e0L//829de+2g057t4AvUTMD/YmNjad4DMA0NfAA4Ax5JBvzLYrGoRYsWZsfAaUTbY/XL6Qt17b2P69FHH9Vto0b5tTZSd0PXV199pTlz5mjHjh015xx4vV599913+sMf/qCNGzfKYrHojjvuMDkp0DSX3HiXfvPsR/p667fqffEl2r59u9/WomYC/teyZUuzIwAIY2xrAYDTqKqqCsuDFAEjNW/e3O/jBdB0FotFV9/1/5TSOV1vPXa7Lus/QMuXLVXbtm19ug51N7QVFxfr9ddf1+uvvy7pp0NUy8vLVV5eLumnn7MJEybooosuMjMm4BOdeg7QfS99qVcmDdelffvq9dde0w033ODzddiBD/hXREQEm00AmIod+ABwGuxoAvwvJSXF7AhogHOvuln3vLhOB7LzdVHvi7V+/Xqffn3qbmg799xzde+99+qiiy5S69ataxr3aWlpuuGGG/TSSy9pzBjOv0DoSErtqHv//bk69LpKw4cP14wZM3w+hoy6CfhXq1atZLXSPgNgHnbgA8BpcEME+FdCQoJiY2PNjoEGSj3nQt330pd69U836/IrrtCLL7yg22+/3Sdfm7ob2lq2bKm77rpLd911l9lRAMPYYuM1etbb+uC5R/XQQw9p27ff6sUXXvDJPO2Kigq53W4fpARQG4vFouTkZLNjAAhzvIUIAKfBI8lN17t3b/Xu3VtfffWV2VF85re//a169+6t559//pSPDRs2TL1799by5ctNSBZ82H0fvOKTkvWbZz/S+deN1h133KEHH/yTqqqqmvx1qbsAQpHVatV19/2Pfvm31/XmW4s0YODlOnLkSJO/LjUT9bV8+XL17t1bw4YNMztKUElKSmLUIwDTsQMfAE6jrKzM7Ag+4/V69dFHH2nVqlXauXOnjh07JqvVqqSkJLVs2VLp6enq2bOnLr74YsXHx5sdF2HAZrOpWbNmZsdAE0RG23TL/3tRrbuepyefnKQdO3bo9ddfa9LfayjVXQD4uQuuG6UW7boqc/IvdFHvi7Vs6RL17t270V+Pmul7XDPjROy+BxAI2IEPAHXwer01s3mDXVFRke655x499NBD+vTTT5WdnS23263o6GhlZ2dry5Yteu211/Tggw/qk08+MTtuwGvdurU6dOigxMREs6MEtZSUFFksFrNjoIksFov6/2qixma8p0/XfqY+l/bV7t27G/W1QqnuAkBd2vborfte+lJRSWnqP2CA3njjjUZ/LQ799i2umXEiRj0CCBTswAeAOlRWVvr8kDGzPProo9q0aZMiIiL0y1/+UjfffLPatm0rq9Uqt9utvXv36vPPP9f7779vdtSg8Pjjj5sdIehFRESoRYsWZseAD53d9zr9bv4GvfzHYbr4kku06K23dNVVVzXoa4RS3QWA02nWKlV3P/ep3v7b3Ro1apS2bt2madMeb/BBmbzp6VtcM+NEjHoEECho4ANAHULlhujAgQNau3atJOl3v/udxo4de9LHIyMjddZZZ+mss87SnXfeyU4uGKJVq1YNblIg8LXqeI5+t2CDFj58mwYNGqR58+bpvvvuq/frQ6XuAkB9RNljdOvjryily3n6+98f1vbt25WZ+UqDxrJUVFT4MWF44ZoZJ7Lb7Yx6BBAwaOADQB1CpZH0ww8/1Pz68ssvP+Pn2+32k/65ei7rc889V+eM1t/+9rfatGmT7r77bt1zzz11fu38/Hz9+9//1rp165Sfn6+EhARdfPHFuuuuu9SxY8daX3P8+HG99tprWrt2rQ4dOqTy8nI5HA41b95c559/vq655hpdcskltb7222+/1eLFi7V582bl5+crIiJCycnJOvfcc3Xdddfp0ksvrfncr776Svfee2/Nr3fu3KnMzExt2rRJR48e1QUXXKB//etfDfp+S0pKNH/+fH388cfKycmR3W7XhRdeqHHjxuncc8+t83WStHPnTr3xxhvatGmT8vPzZbVa1bZtWw0YMEC/+tWvah3f8/zzz+uFF15Qr1699K9//UsfffSR3n77bf3www8qLCzUXXfdddq8RrFYLMwTDWGxzZrrzoz39F7GZP3+97/Xtm3fat68jHodABcqdRcA6stiseiKsVOU3LmH3vzLr9S332VavmxpnddFJ6qqqpLb7fZ/yDDR1GvmaocOHdJrr72mjRs3KicnRx6PR23atFHfvn01evRotW7d+pTXLF++XFOnTlWbNm20fPlyfffdd3rppZe0efNmHT9+XMnJybr88st11113nbapvG3bNi1YsEDffPONXC6XUlJSdPXVV2vcuHH1+BOQiouL9cYbb2jNmjU6cOCAXC6XkpKSdMEFF+iXv/ylzjvvvFNec/jwYQ0fPlyStGzZMnk8Hr300kvasGGD8vLy1LJlSy1fvrxe6weS5ORkRj0CCBg08AGgDqHYSMrJyVGnTp1MWfvw4cN65JFHdPToUdlsNkVGRuro0aNatWqVPvnkE82aNUv9+vU7Je9vfvMbZWdnS5KsVqvi4+NVWFioo0ePateuXdq3b98pDfyqqio99dRTWrhwYc3vxcTEqKqqSnv37tXevXv1ySef6NNPP60160cffaRHHnlEbrdbcXFxioxs+H8ujx8/rjvuuEP79+9XVFSUoqOj5XQ69b//+79au3atHnnkEf3iF7+o9bXPP/+8XnzxxZpRIna7XW63Wz/++KN+/PFHLVu2THPnzlW3bt3qXP+pp57Sq6++KovFooSEhIDa7d6iRYt6NXMRvCIiIzVs0lyldDlXL864T4VOp15/7dUzvi4U6y4A1EePgcP0u/nr9cqk4bro4ou164cf1Lx589O+hprpP429Zn7nnXc0Y8aMmjdWoqOjZbFYtG/fPu3bt0/Lli3TjBkzTtpE8nOrVq3SX//6V7ndbsXHx6uqqkpZWVl67bXXtH79ei1YsKDWuexLly7V3/72N3k8HklSfHy8jhw5ovnz5+uTTz7RTTfddNrs3377rSZNmqSjR49K+mncod1uV05OjlavXq0PPvhA991332nfDNi6daumT5+u0tJS2e32Rl1DB4LIyEhGPQIIKMFZTQHAAKFyU9SjRw9ZLBZ5vV7NnTtXM2bMUIcOHQzP8eSTTyo+Pl7PPPOM+vTpI4vFom+//Vb/8z//o127dunhhx/Wm2++edKsyX/961/Kzs5Wamqq/vKXv+iiiy5SRESEqqqqlJubq3Xr1unIkSOnrPWPf/yjpnk/fPhw3XnnnTXfc0FBgbZu3Xra2aVTp05Vnz599Ic//KFmB9yBAwca9P2+8MILslqteuKJJ3TFFVcoMjJSe/fu1d///ndt2rRJ06dP1znnnHNKE/61117TCy+8oLi4OI0bN0433HCDWrZsqaqqKv3www+aN2+evvzyS02aNElvvfVWrTdwO3fu1KZNm3THHXfo9ttvV/PmzVVRUVFzQ2Ymi8WiNm3amB0DASpU6i4ANIbX65XX65VF9dv1S830raZeM3/66af629/+psjISI0dO1a33HJLzW77/fv367nnntOHH36oKVOm6I033qh1J/6xY8f0+OOP64YbbtBdd92l1q1by+VyadmyZXryySe1Z88evfzyyzVPjVbbuXOnpk+fLo/Ho4suukgPP/ywOnbsKLfbrY8++khPPPGEXnzxxTqzHz58WA888ICKiopqdux37dpVkZGRKigo0Jtvvqn58+frH//4hzp16qQrrrii1q8zffp0de7cWX/605/Uo0ePmu892KSmpgbU5hcAoCIBQB1C5aYoNTVVN954oyRp165dGjFihEaPHq0ZM2Zo6dKl2rVrlyGHRpaXl+vpp5/WpZdeWvM46rnnnqtnn31WDodDJSUlWrBgwUmv2bp1qyTpvvvu0yWXXKKIiAhJP+0IatOmjUaMGKEHHnjgpNfs379fmZmZkqQ77rhDjz766Ek3X0lJSbriiiv097//vc6snTp10pNPPnnS4+vt27dv0PdbXFysJ554Qtdcc03N7qNOnTpp3rx5at++vaqqqvTPf/7zpNcUFhbq2WeflcVi0axZszR27Fi1bNmy5nvu3r27nn76aXXv3l05OTlasmRJrWuXlpZq9OjRGj9+fM3uvejo6IBonKekpCg6OtrsGPCzKrdby2ZP0Nv/c7fuvutuvfzSgnq9LlTqLgA01I7/Xabnft1XbZIS9PVXX55x971EzfS1plwzV1ZWaubMmZKkhx9+WPfff7/atGkji8Uii8Wijh076oknntDAgQNVUlKiV1+t/ak0l8ulQYMG6S9/+UtNg99ut+vWW2/VbbfdJkm1bkJ59tlnVVVVpfbt2ysjI6PmGjYyMlLXXXedpk+frqKiojq/94yMDBUVFWno0KGaMWOGunXrVnP9mpSUpHvvvVfjx4+XpJqRkrVxOBx69tlna5r3kkzZONQUdru95vobAAIFDXwAqEMoHQo2ZcoU3XXXXYqJiZHX69X333+vt956S9OmTdOoUaN03XXX6cknn/TrDu1rrrmm1keRk5KSdPPNN0uSVq9efdLHEhISJP00O7++VqxYIY/HI4fD0eh577fffnvNmwWNdcEFF9Q6m99ut+v222+XJH3xxRcqLi6u+djKlSvlcrnUvXv3Ouf6V9+ISdL69etr/Ryr1ao777yzSfn9ITIystbdZggtpceP6aUJQ7XhrX/o2Wef1bPP/qPeI5NCqe4CQH14vV59uuAJvTL5Rl137TX64vN19W54UjN9r7HXzOvWrVNubq5atGhRMw++Ntdff72kn64B6/Kb3/ym1t+vnst/8ODBkw7QLSoqqrkmvOOOO2qdzd+3b1+df/75tX5dp9OpTz75RJJOObi3tuw//PBDnfcMt956a61PhwaTtLQ0Zt8DCDiM0AGAWoTaoWCRkZG69957NWbMGK1Zs0abNm3Sjh07tHfvXlVWVqqgoECvvfaa3nvvPc2dO/eMB6w2Rl0H4ErSxRdfrPnz58vpdCorK0tpaWmSpP79+2vr1q165plntH//fl155ZU6//zzFR8fX+fXqt6136dPH9lstkZlvfDCCxv1uhNdfPHFZ/yYx+PRzp07a/5svvnmG0nS7t27a5r0tam+aattfJAktW3bVklJSY2J7Vdt2rRp8hsjCGy5+3bqlT8Ol7v4qD744ANdeeWV9X5tqNVdADiTSleZ3v7b3dq88lU98shf9PjjUxs0toMd+L7X2GvmLVu2SPrpDKTBgwfX+fUrKysl1X0N53A41K5du1o/1qpVq5pfHz9+vKZRv3Pnzpq596e73u7du3fNdfKJtm3bVvP63/3ud3W+/kRHjhypdUb8BRdcUK/XB6r4+HglJiaaHQMATkEDHwBqEao3RPHx8Ro6dKiGDh0q6afv85tvvtHChQu1du1aFRYWasqUKXr77bcb3fyuS3Jycr0+duzYsZoG/h133KEff/xRH3zwgd555x298847slgs6ty5s/r166cbb7zxlF1q1TuCmjIupj6PrZ/JiTdZp/tYQUFBza/z8vIk/fT3Up+fwRN3X50oEJv3NpvttH8mCH7ff75KbzwySu3bpmnFxxvVpUuXBr0+VOsuANTmeN5hZU6+UTm7t2nhwoU141EagrrpPw29Zq6+hqusrKzXE611/d2dbvf6iZsgTnzD+8Rryfpeb5+oOrukej+NG0zXoA1RfQ8CAIGGBj4A1CJcbohsNpv69OmjPn366K9//atWrFihnJwcffHFF3UeTtVYjXkUNTIyUn//+981btw4ffLJJ/rmm2/07bffavfu3dq9e7dee+01PfDAAxozZoxP1qvmi13ip1u/ro9V73665ZZb9PDDDzd67UA8dCs1NZXHkUOU1+vVZ6/N1cqMyRoyZKhee+1VNWvWrMFfJ1zqLgAc2vGVMif/QjGRFq377DNddNFFDf4aXq+XEToGOtM1c1VVlSSpX79+mjdvnslpG6Y6u81m07p1/GmHpgAArQVJREFU65r0tQLxGrS+EhMTT/uULwCYKXirKwD4UTjeEN100001v963b1/Nr6ub2af7MzlxjntdcnJy6vxYbm5uza9r2/1+9tln65577tE///lPffLJJ3r22WfVq1cvVVVVKSMjQz/88EPN51YfOnX48OEzZvKnE7+n033sxJ1K1Y8i79q1y3/BTBAbG+uTpxoQeNwV5Vr0+G/07lN/1KRJk7V06ZJGNe+l8Ky7AMLPN6te1/N3D9BZHdvp66++bFTzXvppp3ddB6rCv2q7Zq6+/jTjGu7Ea8nTXX+euNP+RNXZy8vLdfDgQd+GCxIWi4Xd9wACGg18AKhFOM5hPvGR3ejo6JpfVx8kW1cDvqSk5KSGf12++uqrM37M4XCc8eI5MjJSl1xyiebOnavo6Gh5vV5t3Lix5uPVB3Rt2LDB1B299fl+rVarzjnnnJrfr54b+u2339Y5GzUYtW3blt33IajoaI5e/N1V2rb6Nb388suaOXNGk55eCce6CyB8eDwevf+PR7TwL7/SbbeO1Jr//bRJ4/6omeap7Zq5+houNze35kwjo3Tr1q1m5/vprj+//PLLWn///PPPr7lOe//9930fMAi0bNmy1sN/ASBQ0MAHgFpUP0oaCrKysrR///4zft6KFStqft2tW7eaX5999tmSpI8//rjW12VmZtZr5+xHH31Ua6O/sLBQb7/9tiTp2muvPeljp/u60dHRNTcrJz6uO2zYMEVERMjpdOr5558/Yy5/+eabb2q9iSovL1dmZqYk6dJLL615g0SShg4dKpvNpqqqKs2YMeO0P4cej0dFRUW+D+5jDofjpO8RoeHw99/o2TsvVmn2Hq353//V7bff3uSvGUp1FwBOVF5SpFcfvFmfLvi7ZsyYoZdfeqnJzUJqpu815Zp5wIABNTvZZ8+eXeeM+GpOp7MJSU+WkJCgSy+9VNJP1+W1bWDZsGFDrQfYSj/t4L/88sslSa+88soZ/wx8mT0QWK3WJr2ZBgBGoIEPALUIpV1Ne/bs0ciRIzVhwgStWLHipNEybrdbO3fu1NSpU/Xqq69KktLT03XhhRfWfM6gQYMkSV988YWef/75mnE5hYWF+sc//qF///vf9WrQRkdHa/z48dqwYUPNI9/bt2/Xfffdp8LCQsXFxWns2LEnvWbYsGF65plntG3btpOa+QcPHtRf/vIXuVwuWa1W9e3bt+Zj7dq1q2kmvvzyy5o2bZoOHDhQ8/Fjx45p9erVmjx5cn3++BotPj5eU6ZM0Ycffljz87Rv3z5NnDhR+/btU0REhO69996TXtOyZUs98MADkqTPPvtMv//97/XNN9/U3KR7vV7t27dPmZmZuu2227R27Vq/fg9NZbFY1K5dO7NjwMe+/fhtPX/XZerQppW+/upL9enTxydfN5TqLgBUKzi8T8/fdZn2b/pYy5Yt05/+9CefPJVGzfS9plwz22w2PfTQQ7JYLNq5c6d+/etf64svvlBlZWXN18jKytLixYt1xx136K233vJp9nvvvVcREREnXWtW5/7ggw/08MMPn/Z6feLEiXI4HCopKdFdd92lpUuXnjQis7CwUB9//LEefPBBPfLIIz7Nbra0tP/P3p2HR1We/x//TNbJRkJ2wg6CiCBYwGoRRGRTCyoFopUCKqiotVYLriiIoILbtyoVEBXFhQAKuPxYZJFFVChFUInsEAaykz2Tbeb3hyUVBSTJzDmzvF/X1asImbk/YLw5557nPE9TBQcHmx0DAM6KQ2wB4DR8aVVTUFCQHA6HNm/eXHswVXBwsMLDw1VUVHTK/qkdOnTQc88996sV7StWrNC2bds0d+5cvf7664qKiqpd/X3vvfdq48aN2r59+1lz/P3vf9esWbN09913y2q1KiAgQGVlZZJ+Gu5PmzZNycnJp7wmLy9Pb731lt566y0FBAQoMjJSFRUVtSuLLBaL7rvvPrVu3fqU140fP16lpaVatGiRli1bpmXLlik8PFwOh6N2RZS7D6kaN26cPvzwQz300EMKCQlRSEhI7Y2QxWLRQw89pI4dO/7qdTfeeKMqKyv16quvatu2bRo7dmztv6/S0tJTbtg9fVualJQUhYaGmh0DLuJ0OrXm9an6fPYTGpGaqjffeOOUbQQaypf6LgBI0oHtG/Teg39SfEwjfbVliy688EKXvTc90/Uaes3cp08fPfnkk5o2bZr27Nmjv/71rwoMDFRkZKTKy8tPWYxycsW7q3Ts2FEPPvignn76aW3dulXDhg1TZGSkKisrVVlZqVatWumGG27Qiy++eNrXN2vWTK+++qomTpyoY8eOaerUqXrqqacUFRWl6urq2mt2Sbrkkktcmt1MERERSkhIMDsGAPwmBvgAcBq+dFN02WWX6aOPPtLmzZu1Y8cO7d+/X9nZ2SouLpbValVCQoLOP/98XXnllerX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6667Xu+887YiIyPNjuXzkpKSFB0drcLCQpWXl5/yayf30P/d736nOXPmaM2aNfrwww+1Z88eFRQUaOzYsadsu5Oenq733ntP27dvV35+vkJDQ9W6dWv169dPw4YNU0hISJ3zHTt2TPfcc4+OHDmi888/X//85z9P2Q4oNzdX77//vr788ksdO3ZMVVVVSkhIUPfu3XXzzTerTZs2v3rPbdu26c4776z9cXp6uhYsWKDt27crLy9PXbp00Zw5c+qc1Z/Rd81xeOcWvTvhBkVHWPXl5s08QQN4kebNmysqKsrsGADgMgzwAQDAObFYLGrTpo2sVqvZUVAH1ZV2pT3+F/3n/72rxx6bpClTJisggIcwjZCdna3CwkJJPx08eiYvvvii3n33XVksFkVFRf3q3897772nF198sXbFbWRkpMrLy7Vz507t3LlTH3/8sV5++WXFx8efc7Y9e/bo3nvvVW5uri655BLNnDlTERERtb++ceNGPfrooyorK5P001YEwcHBstlsstls+uyzz/Too4/qj3/84xlrrFmzRo8++qiqq6sVERFh2iHJ3oy+a45/fzJfH8/8qy655BJ99OESJSYmmh0JwDlKSEjgv1kAPoeraAAA8JtODpEaNWpkdhTU0d6vVyvUatUHH3yg1NRUs+P4hZqaGn3//fe1W+jExsbq2muvPe3Xpqena/v27Ro1apT+8pe/qHHjxqqsrFReXp6knwbpL7zwgiTpiiuu0P3336+mTZuqqqpKq1at0owZM7R3715NnDhRc+fOPadV2tu2bdMDDzyg0tJSDRgwQFOmTFFwcHDtr3/33XeaOHGiqqqqNHToUP35z39W8+bNFRgYqMzMTM2fP1+LFv20H3ibNm3UsWPH09aZMmWKfv/73+vvf/+7WrVqJUk6cuTIOf85+jv6rnk+mn6Hbr31Nv3rX7Pq9XQLAHPEx8erefPmZscAAJdjgA8Av+Ape5sCnuLkECkmJsbsKKijnj176rvvf9CitIXq1q2b2XHOyNv77sCBA2t/7HA4VFRUpJqaGkVEROjqq6/WXXfddcZH+cvKynTzzTfr3nvvrf25kJAQNWnSRJL08ssvS5K6du2qGTNm1A7og4ODde211yoqKkr333+/du7cqXXr1qlfv35nzbp69Wo9/vjjqqqq0k033aT777//V3/+M2bMUFVVlcaOHVu7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dlx4KG//7wCAZ2GADwAejuE9AAAAPJmvDO+Li4v1wAMPaPv27bU/FxgYqMjISGVmZspms+nbb7/Ve++9pyeeeILV7AAAQzDABwAPxvAe+DVvHw4AAOBLfGV4L0mPP/64tm/frsDAQN10000aOnSomjVrpoCAAFVXV+vgwYP68ssvtXLlSrOjwsP5wn8PADwHA3wA8FAM74HT44YIAADP4EvD+yNHjmjjxo2SpPHjx2vMmDGn/HpQUJDatWundu3aafTo0bLb7SakBAD4Iwb4AOChMjIyGN4DAADAIxUWFvrM8F6S9uzZU/vjK6644je/3mq1nvbnjx49qvfee0/ffPONsrKy5HA41KRJE1122WW6+eablZyc/KvXfPzxx5oyZYqaNGmijz/+WLt379b8+fP1n//8R0VFRUpMTNQVV1yhsWPHqlGjRmfMtGvXLr311lvasWOH7Ha7kpKSdNVVV+mWW245hz8BqaSkRAsXLtSGDRt05MgR2e12xcbGqkuXLrrpppvUuXPnX73m2LFjGjJkiCRp+fLlcjgcmj9/vr7++mvl5OQoPj5eH3/88TnV9yW+8t8FAM/AAB8APFB2drZycnLMjgEAAAD8it1u18GDB312SJmVlaXWrVvX+XUfffSRnn32WVVXV0uSQkJCZLFYdOjQIR06dEjLly/Xs88+q0svvfSM77FixQpNnjxZ1dXVioyMVE1NjWw2m9577z199dVXeuuttxQeHv6r1y1btkzTpk2Tw+GQJEVGRur48eN68803tW7dOt1www1nzf7dd9/pgQceUF5enqSf9v63Wq3KysrSqlWrtHr1at11111n/TBg586dmj59usrKymS1WhUUxMgJAFwhwOwAAIBTFRcXKyMjw+wYAAAAwK/U1NRo3759qqmpMTuKS3Xs2FEWi0WS9NJLL+nw4cN1ev369es1bdo0SdKYMWP08ccfa/Pmzdq0aZMWL16sfv36qbS0VA8++KAyMzNP+x4nTpzQk08+qT/+8Y/65JNPtH79em3YsEETJ05UUFCQDhw4oLfffvtXr0tPT9f06dPlcDjUrVs3LV68WOvXr9fGjRs1bdo05eXl6fXXXz9j9mPHjumvf/2r8vLydNVVV2nBggXavHmzvvjiC61atUpjx45VQECAXn31Va1fv/6M7zN9+nS1adNGb7/9tjZt2qSNGzfqlVde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not77703Nm3aFLt27Ypzzz037rzzzjN6vQcOHIivfvWr8dhjj8XExERUKpU477zz4qqrroqzzz571udFRDz33HPx13/917Fp06bYuXNn5HK5GBsbi5/92Z+N3/zN35zx+J477rgj7rrrrvjJn/zJuPPOO+PRRx+NBx98ML73ve/Fnj174uqrrz5l3uWSJInzRAFggSYnJ+PIkSNpx4C21Gg0HPUIZIYCH2AW7VLgv93ExESMj4+n8rlfffXVuOGGG2LXrl1RLpejUCjErl274hvf+EY8/vjj8YUvfCHe+973npT3t37rt2L79u0REZHL5aKnpyf27NkTu3btihdeeCG2bt16UoE/OTkZX/ziF+NrX/va9K91dXXF5ORkvPjii/Hiiy/G448/Hv/yL/8yY9ZHH300brjhhjhy5Eh0d3dHoXDm3y5ff/31uPLKK2Pbtm1RLBajVCrF3r1741//9V/jiSeeiBtuuCF+6Zd+acbn3nHHHXH33XdPH+FUqVTiyJEj8fzzz8fzzz8ff/d3fxe33HJLnHXWWbN+/i9+8Ytx3333RZIkUavVMrXavb+/P4rFYtoxAKClteNYFbKgUCg46hHIFAU+wCzaZVL0rne9K5IkiampqbjlllvipptuijVr1ix7jptvvjl6enri1ltvjQsvvDCSJIlnn302/uiP/iheeOGF+PSnPx1/8zd/c9xZk3feeWds3749ms1mfOYzn4mf+qmfinw+H5OTk7Fjx4548skn44c//OFJn+u2226bLu9/8Rd/MT7ykY9Mv+bdu3fHM888c8rz/j/72c/GhRdeGJ/85Cendwa89NJLZ/R677rrrsjlcvH5z38+Lr744igUCvHiiy/GjTfeGJs2bYo/+ZM/iQ0bNpxUwt9///1x1113RXd3d1x11VXxC7/wCzEwMBCTk5Pxve99L770pS/Fd77znfi93/u9eOCBB2a8WOu5556LTZs2xZVXXhlXXHFFrFy5Mt58883YtWvXGb2GpZAkSYyMjKQdAwBaXruMVSFrms1mpha/APiKBDCLdpkUNZvNeP/73x8RES+88EJ84AMfiA996ENx0003xcMPPxwvvPDCslzWe+jQofjyl78cF1100fR21LPPPjtuv/32qNfrceDAgbjnnnuOe84zzzwTEREf+9jH4oILLoh8Ph8Rb12AOjIyEh/4wAfi2muvPe4527Zti3vvvTciIq688sr4gz/4g+N+YNHX1xcXX3xx3HjjjbNmHR8fj5tvvvm4Y31Wr159Rq93//798fnPfz4uvfTS6RX84+Pj8aUvfSlWr14dk5OT8ed//ufHPWfPnj1x++23R5Ik8YUvfCE++tGPxsDAwPRr3rhxY3z5y1+OjRs3xsTERPzt3/7tjJ/7jTfeiA996ENx3XXXxcqVKyMiolQqZaI4HxoailKplHYMAGh57TJWhSypVCrT42+ArFDgA8yinS4Fu/766+Pqq6+Orq6umJqaiv/93/+NBx54ID73uc/FBz/4wbjsssvi5ptvXtIV2pdeeumMx/f09fXFr/zKr0RExDe/+c3jHqvVahHx1tn5c/XII4/E0aNHo16vz/u89yuuuGL6hwXzde655854Nn+lUokrrrgiIiKeeuqp2L9///Rj//AP/xAHDx6MjRs3znquf6FQiMsuuywiIv793/99xt+Ty+XiIx/5yILyL4VCoRDDw8NpxwCAttBOY1XIitHRUWffA5njCB2AGbTbpWCFQiGuueaa+PCHPxz/9m//Fps2bYr/+Z//iRdffDEOHz4cu3fvjvvvvz/+/u//Pm655ZbTXrA6H7NdgBsRcf7558dXv/rV2Lt3b7zyyisxOjoaERE/8zM/E88880zceuutsW3btrjkkkvinHPOiZ6enln/XcdW7V944YVRLpfnlfW8886b1/Pe7vzzzz/tY0ePHo3nnntu+s9m8+bNERGxZcuW6ZJ+JgcPHoyImPH4oIiIsbGx6Ovrm0/sJTUyMrLgH4wAAG+xAh8WV09PT/T29qYdA+AkCnyAGbTrhKinpycuv/zyuPzyyyPirde5efPm+NrXvhZPPPFE7NmzJ66//vp48MEH511+z6bRaMzpsddee226wL/yyivj+eefj3/6p3+Khx56KB566KFIkiTWrVsX733ve+P973//Sef5H9tFsJDjYo4dO7MQg4ODc3ps9+7d0x//6Ec/ioi3/l7m8h48VuSfKIvlfblcPuWfCQBwZtp1vAppOTYHAcgaBT7ADDplQlQul+PCCy+MCy+8MP7wD/8wHnnkkZiYmIinnnoqLr744kX9XPPZilooFOLGG2+Mq666Kh5//PHYvHlzPPvss7Fly5bYsmVL3H///XHttdfGhz/84UX5fMcsxirxU33+2R47evRoRET86q/+anz605+e9+fO4qVbzWbTdmQAWCRTU1OO0IFF1Nvbe8pdvgBpUuADzKATJ0S//Mu/HI888khERGzdunX61/P5fExOTp7yz+Tt57jPZmJiYtbHduzYMf3xTKvf3/nOd8Y73/nOiIg4cuRIbNq0Ke6+++7YtGlT/Nmf/VlccMEF048PDAzE1q1b49VXXz1tpqX09td0qsfevlq+v78/It66bLidVKvVRdnVAAC85fDhwzE1NZV2DGgLSZJYfQ9kWvaW6AFkQDudfz9X1Wp1+uNSqTT98bGLZGcr4A8cOHBc4T+bp59++rSP1ev10w6eC4VCXHDBBXHLLbdEqVSKqamp+I//+I/px88555yIiPj2t7+d6k6KubzeXC4XGzZsmP71c889NyIinn322VnPt29FY2NjVt8DwCLqxLEqLJWBgYGoVCppxwCYlQIfYAaTk5NpR1g0r7zySmzbtu20v+/Y6vuIiLPOOmv642Mr2x977LEZn3fvvffOacfCo48+OmPRv2fPnnjwwQcjIuLnfu7njnvsVP/eUqk0fVTM24+Med/73hf5fD727t0bd9xxx2lzLZXNmzfPWOIfOnQo7r333oiIuOiii6Z/QBIRcfnll0e5XI7Jycm46aabTvk+PHr0aOzbt2/xgy+yer1+3GsEABauncaqkKZcLregu7MAloMCH2AG7bSq6fvf/3782q/9Wvzu7/5uPPLII8cdLXPkyJF47rnn4rOf/Wzcd999ERHxEz/xE3HeeedN/56f//mfj4iIp556Ku64447p43L27NkTt912W3zlK1+ZU0FbKpXiuuuui29/+9vTW77/+7//Oz72sY/Fnj17oru7Oz760Y8e95z3ve99ceutt8Z//dd/HVfm/+AHP4jPfOYzcfDgwcjlcvGe97xn+rFVq1bFFVdcERERf/mXfxmf+9zn4qWXXpp+/LXXXotvfvOb8fu///tz+eObt56enrj++uvjn//5n6ffT1u3bo1PfOITsXXr1sjn83HNNdcc95yBgYG49tprIyLiW9/6Vnz84x+PzZs3T0/Sp6amYuvWrXHvvffGb/zGb8QTTzyxpK9hoZIkiVWrVqUdAwDaTjuNVSFNo6OjUSwW044BcErOwAeYQTutaioUCnH06NF48skn48knn4yIiGKxGNVqNV5//fXjzk8966yz4k//9E9PWtH+jW98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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(2.2,0.75, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,2.35, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,4.15, 'Subscriber', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'Sarracenia Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "0f8a727c", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " the cost on the broker to route a single product: 120366 in integer ops\n", + "\n", + "regex's are O(m,l) = 1500 hmm... sounds like too much.\n", + "overriding source, lowering estimate to 100 more investigation needed.\n", + "Assuming 10 senders selected by broker, each one needs to look at the 10's in it's configuration\n", + "Client-side routing cost is 5000\n", + "\n", + "t total cost=125366\n" + ] + } + ], + "source": [ + "\"\"\"\n", + "\n", + "Estimating the cost to route 1 product, using topic based routing along with the traditional regular expressions.\n", + "\n", + "\n", + "\"\"\"\n", + "from math import log\n", + "\n", + "K=10000 # number of topics/folders in the hierarchy\n", + "B=8 # branching level at each level in the hierarchy\n", + "n=10 # number of characters in a topic name\n", + "b=2 # number of bindings per sender\n", + "S=100 # number of senders\n", + "\n", + "\n", + "# routing cost on the broker, in integer ops.\n", + "# log(K,2)*n - lookup each topic in a dictionary to map it to an integer\n", + "# log(K,B) - the depth of the higherarchy is a function of the total, and the number of choices at each level.\n", + "# log(B,2) - the time to search each level in the hierarchy, comparing integers.\n", + "# \n", + "Rpb = ( log(K,B)*(log(K,2)*n + log(B,2))*b*S )\n", + "\n", + "# AMQP exchanges function as an additional topic in the higherarchy at the top of the tree.\n", + "# so they can be ignored.\n", + "\n", + "print( f\" the cost on the broker to route a single product: {int(Rpb)} in integer ops\" )\n", + "\n", + "# this is pretty iffy, used this conversion throughout, but if RE's are much cheaper, then\n", + "# ... the numbers change alot.\n", + "# convert integer ops to regular expressions. This is difficult to estimate\n", + "# https://swtch.com/~rsc/regexp/regexp1.html \"Regular Expression Matching Can Be Simple And Fast (but is slow in Java, Perl, PHP, Python, Ruby, ...)\" -- Russ Cox, rsc@swtch.com\n", + "# it lists O(m,l)\n", + "# \n", + "m=30 # the length of the regex being matched to.\n", + "l=50 # the length of the string being matched.\n", + "RE=l*m\n", + "\n", + "print( f\"\\nregex's are O(m,l) = {RE} hmm... sounds like too much.\")\n", + "# Seems overly pessimistic, overriding.\n", + "\n", + "RE=100\n", + "\n", + "print( f\"overriding source, lowering estimate to {RE} more investigation needed.\")\n", + " \n", + "# on the client side, in # of regex evals. \n", + "\n", + "s=10 #number of senders pre-routed to by the broker.\n", + "re=10 #number of regular expressions per sender to evaluate.\n", + "\n", + "Rpc = s*RE*re/2\n", + "\n", + "print( f\"Assuming {s} senders selected by broker, each one needs to look at the {re}'s in it's configuration\")\n", + "\n", + "print( f\"Client-side routing cost is {round(Rpc)}\")\n", + "\n", + "Rp=Rpb+Rpc\n", + "\n", + "print( f\"\\nt total cost={round(Rp)}\")\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "2cf1861e", + "metadata": {}, + "source": [ + "The Algorithmic Cost to Route 1 File\n", + "----------------------------------------------------\n", + "\n", + "\n", + "| Application| Pr | Client | Dominant Term/ compute cost scales as a function of | Pre-Routing Done By |\n", + "|------------|-------|--------|----------------------------------------------------------|-----------------------|\n", + "| PDS | 500 | 50 | product of the number of receivers, senders, products | One Dispatcher process|\n", + "| Sundew File| 500 | 50 | product of the number of senders, products | All (10ish) receivers |\n", + "| Sundew Msg | <1 | 5 | log of the number of messages | All (10ish) receivers |\n", + "| Sarracenia | 120 | 5 | log of the number of topics/folders | Message broker |\n", + "\n", + "* Units: Kilo Instructions per second. 500 = 0.5 MIPS.\n", + "* broker is a service, but is multiprocessing/scaled itself. not a single process.\n", + "* sender processes do the client-side routing in all cases.\n", + "* For Sundew, this number is optimistic (bulletin) case, file case is more like PDS, but multiple routers.\n", + "* for Sarracenia, this number applies to all routing.\n", + "* give scaling factors the large the configuration, the bigger the perf. advantage for topc routing.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fa0fc94b", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "s=10 # number of senders / product shipped.\n", + "ServerPeak=1000\n", + "\n", + "#for PDS\n", + "clientSide=1000/0.050 = 20000\n", + "Dispatcher=1000/0.5= 2000\n", + "PDSPeak=2000\n", + "# limited to slowest element, which is the dispatcher, so 2000 is upper bound.\n", + "\n", + "#For Sundew\n", + "SundewClientSide=1000/0.005= 200000\n", + "SundewReceivers=1000/0.182 = 5494\n", + "SundewPeak=SundewReceivers*s=54940\n", + "# limited minimum of client and receivers, so 54.9 is it.\n", + "\n", + "#For Sarra\n", + "SarraClientSide=1000/0.005 = 200000\n", + "SarraBroker= 1000/0.125 = 8000.0\n", + "# how many cpus can a broker use to route? um... it isn't limited.. or is at least unknown...\n", + "# hmm... defined by the broker implementation.lets just use s.\n", + "SarraPeak= SarraBroker*s= 80000\n" + ] + }, + { + "cell_type": "markdown", + "id": "53af0101", + "metadata": {}, + "source": [ + "\n", + "Overall Server Algorithmic Ceiling\n", + "-------------------------------------------------\n", + "\n", + "\n", + "Assume a 1 GIPS machine within infinite cpus, and take IP cost per route, so the number of products that can be routed by a server is going to be inverely related to cost to route each one: route/second = 1000 MIPS / cost to route from previous cell. The other contributor is the number of processes doing the routing.\n", + "\n", + "| Application | Route/s | Routers | Total |\n", + "|-------------|----------|---------|--------|\n", + "| PDS | 2000 | 1+10 | 2000 |\n", + "| Sundew | 5494 | 10+10 | 54940 |\n", + "| Sarracenia | 8000 | 10+10 | 80000 |\n", + "\n", + "\n", + "* These numbers are algorithmic maxima, never to be seen in real life.\n", + "* The Sarracenia one requires a guess at parallelization of Rabbitmq, is conservative.\n" + ] + }, + { + "cell_type": "markdown", + "id": "64ff1fe0", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Duplicate Suppression\n", + "===================\n", + "\n", + "* All message passing networks implement \"Duplicate Suppression\"\n", + "\n", + "* typical:\n", + " * checksum the message, store the checksums.\n", + " * when you get a new message compare it's checksum to what is in the store.\n", + " * if there is a hit, then it's a duplicate, so discard the message.\n", + " \n", + "* Does not need to be perfect...\n", + " * a few duplicates is ok, but need to prevent \"storms\"\n", + " \n", + "* Simple if one process is handling an entire flow.\n", + "\n", + "* Not simple if > 1 process share the flow.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "bfc76ce5", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3hb5f3//5dsJ3Y84pnYsZPYGWRvNgUSAoQ9WtYHKFDKar8t3YNSPi0dtKW0pYO2pIzyYZQRKJSmbJJAmAGyyI7jvadseUiWpfP7g59VFHnJlnSO5OfjunyBj3Xu85ajnEivc5/3bTMMwxAAAAAAAAAAALCUOLMLAAAAAAAAAAAAgQjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfADAsRUVFstlsvq8vfOELEa/hoYce8qvBZrOprKws4nXAOqLxNXF4vbfffrvZJUVENP5ZYWRWrVrl9+e8atUqs0sCAAAAolaC2QUAAIDY0tTUpD179qiiokJNTU3q6uqSzWZTenq6MjIylJWVpQULFmj69Olmlwr46enp0a5du1RaWqqamhp1dnbK4/EoIyNDmZmZmjx5spYtW6acnByzSwUAAAAwRhDgA0CYvPfeezr++OP9tp144onavHnziMb7+OOPtWTJkoDt119/ve67774Rjblu3TpdeumlftsuueQSPfXUUyMaD2OTYRh69dVX9c9//lOvvPKKSktLh7VfVlaWli1bpjVr1ujiiy/WrFmzwlwpEKi6ulqPP/64nnvuOX3wwQfq6ekZcp8ZM2boM5/5jC6//HKtWbNGCQm8pQYAAAAQHrTQAYAwOeqoo5SWlua3bcuWLerq6hrReJs2bep3+8aNG0c03kBjnnLKKSMerz9jtV3IWOD1evXAAw9ozpw5OuOMM7R27dphh/eS1NLSog0bNuiWW27R7NmztWLFCj344IPq7e0NY9XAJ8rLy3X11VdrxowZ+u53v6u33357WOG9JJWWlurRRx/VOeeco4KCAt12221qb28Pc8UAAAAAxiICfAAIk4SEBJ144ol+23p6evT222+PaLyBAvxDhw6psrIyZGOGOsBHbNq1a5eOPPJIXX/99SouLg7JmNu2bdN1112nuXPn6oknngjJmMDhDMPQ7373O82fP1+PPPKI3G73qMZraGjQHXfcoVmzZumvf/1riKoEAAAAgE9wvy8AhNEpp5yiF1980W/bpk2bdPrppwc1jmEYevPNNwf8+aZNm3TVVVcFNWZDQ4P27Nnjty0vL0/z5s0LahyMPU8++aS+8IUvyOl09vvzcePG6eijj9bxxx+vyZMnKzs7W+np6XK5XGppaVFpaam2bt2qDz74oN87UkpKSvSb3/xG//M//xPup4Ixxul06oorrtCzzz474GOKiop06qmnqqioSJMmTVJ2drZcLpeampp08OBBvfvuu9q+fbu8Xq/ffk1NTbrzzjv15S9/OdxPAwAAAMAYQoAPAGHU32z2kbS8+fjjj9XU1OT7Pj4+Xh6Px2/MYAP8YGffl5WVBTU+YtODDz6oG264ISC8lKQlS5botttu09lnn62UlJQhx3K5XHr11Vf10EMP6V//+hetcxBWLpdL55xzjjZs2BDws3HjxumGG27Q17/+dc2ZM2fIserr6/XEE0/oj3/8o0pKSsJRLgAAAABIooUOAITVihUrlJ6e7rftww8/VGdnZ1DjHB62X3HFFbLZbAP+fCRjSrTPweBeeukl3XjjjQHhfVpamv7xj39o+/btuuSSS4YV3ktSYmKizj33XD399NMqLi7WF77wBcXF8dYE4XHdddf1G95/5jOf0d69e/XnP/95WOG9JOXm5urrX/+6Dhw4oHvvvVc5OTmhLhcAAAAAJBHgA0BYxcXF6eSTT/bb5na79dZbbwU1zuFh++c+9zktWrTI931paanKy8tHNaZEgI+B1dfX6+qrr/a780OSpk6dqrfeekuXX36530WlYBUWFurvf/+73n77bc2fP3+05QJ+HnroIT322GMB2y+44AK9+uqrmjVr1ojGjY+P10033aSPP/5Ya9asGW2ZAAAAABCAAB8Awqy/UDyYGfOH97+32Ww66aSTtHLlyhGPWV9fr7179/ptmzp1qmbPnj3sMTC2fOtb31JjY6PftgkTJujll1/WkiVLQnac4447Th9++KGuvPLKkI2Jsa2lpUXf/OY3A7afcMIJeuaZZzRhwoRRHyMvL08vvviivvGNb4x6LAAAAAD4NHrgA0CYjbYP/s6dO9Xc3Oz7fvHixcrOztaqVat0zz33+I15zTXXDGvMWJ9973a7tWXLFu3Zs0dNTU0aN26cJk2apDlz5uiYY45RfHy82SX6dHd368CBA9q3b5+amprU3t6uhIQEZWZmKicnR8uWLVNRUZGpNe7YsUOPP/54wPa77rpLCxYsCPnxkpOT+w1cI6Wrq0vvv/++ampq1NjYKKfTqUmTJmny5Mlavny5pk6dGvYaDMPQ9u3btXPnTjU0NMjj8WjKlCmaPn26TjjhBCUmJob0eA0NDdq3b58OHToku92uzs5OpaWlKSsrSwUFBTrmmGOUmpoa0mNGyq9+9SvZ7Xa/bampqXr44YdDei6Ii4sbUYBvhXOAx+PR9u3bVVZWpqamJrW0tCghIUETJ05UUVGR5s+fr+nTp4f8uI2NjXrvvfdUUlKijo4Opaena/LkyTr22GNVWFgY8uNJktfr1bZt21RWVqbGxka1tLRo4sSJmjRpkmbPnq3ly5fTygsAAACWQoAPAGG2dOlSZWVlqaWlxbfto48+ksPhUFpa2pD7Hx629828P7w1TzAz8EcS4BcVFfm16bnmmmv00EMP9Tv2YGP95Cc/0U9+8pNBj1VYWDiiRXPr6ur0q1/9Sg899JDa2tr6fUxGRoa+8IUv6LbbblN2dnbQxxgtwzD0zjvv6Pnnn9emTZu0devWIRdvnTp1qj73uc/p29/+dlhCtKH84Q9/kGEYftsWLFigr3zlKxGvJZzWrVun++67T2+++aZcLteAj1u4cKEuvvhifetb39LEiRNDWoPD4dBvf/tbrV27VnV1df0+Jj09XRdeeKFuv/32EQe7DodDzz//vF555RVt2rRJFRUVgz4+Pj5ey5cv10033aSrr75a48ePH9FxI62rq0t/+9vfArZ/73vfG3HbnNGy0jng+eef14MPPqhNmzYNeM7sM336dK1Zs0ZXXXVVwL8/wdq0aZN+8Ytf6PXXX+93QWzpk3PMrbfeGrDmy0ht3rxZf/7zn/Xqq6/6/Xt8uKysLJ199tn6wQ9+EJYLlAAAAEDQDABA2H32s581JPl9vfDCC8Pa98ILL/Tb75lnnvH9bMGCBX4/KykpGdaY8+bNC6intLR00H0KCwv9Hn/NNdf0+7iNGzcGjB3sV2FhYb9j//3vfx+w7qefftrIyMgY9jGys7ONd999d1i/r1DZtGmTMW3atBH/XhISEozbbrvN8Hg8Eau5q6vLSE5ODqjlnnvuiVgNgxnsNTFcH330kXH00UcH/eeRk5Nj3HvvvUHXfPg4P/7xjw3DMIwtW7YE9fpITk42fv/73wd9/O9+97tGUlLSiF+HU6dONd58882gjxuKP6tgPfLIIwHHHDdunFFbWxvW4w7EKueAjRs3GsuXLx9xHTfffPOg469cudLv8StXrjQMwzCcTqdx/fXXB3WsNWvWGB0dHSN+rvv37zfOOuusoJ9jXFyccd111xnd3d0jPjYAAAAQCtwfCgARMNI++EY//e8/PfNxJH3w6+vrtW/fPr9thYWFprdpGY17771Xl1xySUCbjME0NzfrtNNO0/bt28NW1+FKS0tVWVk54v17e3v185//XBdccMGQM3ZD5fXXX1dXV5fftqSkJF199dUROX64vfjiizr55JP1wQcfBL1vU1OTvvSlL+nrX//6gLOIh+vDDz/UKaecEtTro6urS9/4xjd02223BXWsLVu2yOl0BluiT1VVlU499VQ98sgjIx4jUp5//vmAbRdccIHy8vJMqMYa54Df//73Ou2007Rt27YR19He3h70Pk6nU2eddZbuv//+oPZ75ZVXdPbZZwcsoD0cr7/+uo499li9+OKLQe/r9Xr1wAMPaOXKlaqvrw96fwAAACBUaKEDABEw0j74O3bs8LvVf+HChcrJyfF9v3LlSv31r3/1G/Paa68ddMz+jrt69eohaxmu1NRULV261Pf9jh07/H6em5s7ZHiWn58/7OO9+OKL+upXv+pr8ZKenq41a9bohBNO0OTJk+X1elVWVqb169fr/fff99u3s7NT1157rT744AMlJET+n8S8vDwdeeSRmj9/voqKijRx4kQlJyers7NTDQ0N+vjjj/XSSy+poaHBb7/169fr1ltv1a9//euw19jf62XFihXDav9kdRs3btT555/fbxC6dOlSnX/++SoqKtKECRNUW1urN954Qy+//HJAe50//vGP8ng8fmtSBMNut+vCCy9UZ2enb9vy5ct1/vnnq7CwUImJiaqurtbrr7+u119/PaDeO+64Q9nZ2SNaN8Bms2nx4sVavHix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5betra1NZ5111ohbZTkcDt1111169NFHQ1EiAAAAEBQCfAAwSX+z4A9vLzOShWYPD+cPH3PFihUB7STC6fB+4G1tbbrsssuisvXKSI0bN04XXnih37bW1ladeuqpg876rqmp0RVXXKEf/ehHvm0TJ04MV5lDOuecc/SXv/wloB++w+HQ5ZdfruXLl2vdunVBXaDZs2ePvv/97+vYY48NdbmD+uUvf+m36LMkFRcX6zOf+Yxee+21Aferrq7WBRdcoP/7v/8L+Nnf/va3gJn9w9G3T1VVlU466SS9/PLLAz72wIEDOvXUUwN65iclJQ25JsGZZ54Z0NLqqaee0vXXXz/on9krr7yi4447zrfYr5mvwdF48MEHtXr16oDtb775pubNm6ebb75ZBw8eHNZY9fX1+sMf/qA5c+boS1/6kpqbmwd9vNnngKysLP3f//1fwN/dp556Sscff/ygr/lP1/vggw9q6dKl+vGPfxx0DZH0+OOPB6wxUFJSomOPPVZ33HGHX6u6gXi9Xm3cuFFf+tKXNH36dH3ve99TXV1duEoGAAAABpRgdgEAMFYN1cZm5syZmjZtWtDjrly5ctCFDiPZPkeSrr76at12221+iwj+5z//0X/+8x9lZmYqNzdXiYmJfvvk5+frhRdeiGid4fa///u/evLJJ+V2u33bDhw4oOXLl+vMM8/U6tWrNXXqVPX29qq2tlabNm3Sa6+95jc79otf/KIOHTqkN954w4ynIEm68cYblZqaqi9+8YsBM3d37NihSy+9VOPGjdMxxxyj4447Trm5ucrOzlZ6erqcTqc6OztVU1Ojffv26YMPPhi0pdL06dPD9jyysrL08MMPa82aNX6vzdLSUp1++ulasWKFzjvvPBUVFSkpKUm1tbV688039dJLL8npdAaM95WvfEUXXHDBiGq56aabtG7dOtXU1KiqqkpnnnmmjjzySN/xx48fr+rqam3YsEGvvfaa32uozy9+8QvNmzdv0ONkZmbqm9/8pn7605/6bX/ggQf03HPP6ZJLLtGKFSuUmZkpu92ukpISrV+/Xh9//LHvsfHx8frDH/6ga6+9dkTP1UyJiYn6z3/+o8svvzxgId6enh7dc889uueeezRjxgyddtppKioqUk5OjrKystTT06OmpiYdPHhQ7733nrZt2xb0gtxmnwPOP/98/fCHP9TPf/5zv+3btm3T6aefriOOOEJr1qzR3LlzNWnSJBmGIbvdrgMHDmjr1q169913fbUvX7486ONH0uzZs/XUU0/p7LPPVk9Pj297Z2enbrvtNv3yl7/UiSeeqM985jOaMmWKMjIy1NXVJbvdrsrKSm3dulVbt241tW0ZAAAA4GMAAEwzb948Q1K/X9dee+2Ixty9e/eAY0oyXnjhhRGNW1hY6DfONddcM+x9b7/99kFrOvyrsLCw33H+/ve/Bzy2tLR0RM/n8HF+/OMfj2icYKxduzao38Onv1avXm04nU5j5cqVfttXrlwZ9rr7s2PHDmPx4sUjfj6Dfc2YMcN48sknh1XHaF8TL7zwgpGSkjKqem+++WbD4/EM+5j9vfa2bNky4jpuvfXWYR+7p6cn4DU03C+bzWasXbvWKC0tDfjZ3//+9yGPHcq/v6Ph9XqNu+66y0hKSgrp6zY/P9946KGHBj22Fc4Bv/vd74z4+PhRPdehzv+hOk+N9jXz/vvvG9OmTQvZn/Fdd901oucBAAAAjAYtdADARIPNhg+2/32fBQsWaNKkSf3+LCEhQSeeeOKIxh2N//3f/9Udd9yh8ePHR/zYVnLjjTfqL3/5S9C/hy9+8Yt64YUXAu5UMNOSJUu0bds23XvvvZo5c2ZIxjz++OO1du1a7du3T5deemlIxhzKWWedpTfffDOgZ/ZwZGdn669//av++Mc/Ki5udG+pjj76aL3++usqKCgY9j7Jycm6++67dccddwx7n3Hjxulf//qXzj333KDqy8jI0FNPPaUbb7wxqP2syGaz6Tvf+Y727t2rK6+8UgkJo7shtaCgQHfccYcOHjyoa665ZtDHWuEc8M1vflMvvfTSoOslDGXy5MmjriMSjjnmGG3dulXXXnutxo0bN+JxbDabVq1a1e86EgAAAEC4EeADgIkGC/BH0v++z0CL1B555JEBPbAjIS4uTrfeequqq6t1zz336LLLLtOiRYuUk5Mzop7h0ezLX/6ytm7dqssuu2zQQGn8+PE677zz9Oabb+qBBx6wVHjfJz4+XjfddJMOHjyoF198Uddff31QbW+ysrK0Zs0a/eQnP9HBgwf1zjvv6MYbb4z4hZ4VK1Zoy5YtevLJJ3XaaacN+btesGCBfvSjH+nQoUP60pe+FLI6jj32WO3evVu33nrrgBfhpE96oF9zzTXatWuXvvGNbwR9nPT0dD3//PN67LHHtGTJkkEfO3nyZH33u9/V/v37dfHFFwd9LCsrKirSo48+qrKyMt1555064YQThv3amzVrlq699lq9+uqrqqio0K233qrk5ORh7WuFc8Bpp52mHTt26IknntCZZ545rNqPOOIIfe1rX9PWrVv161//OmS1hFtOTo4efPBBFRcX6zvf+Y4WLlwYsBZAf9LS0nTOOefo7rvvVmlpqTZu3BjxtToAAAAASbIZhmGYXQQAAGNRZ2en3nnnHZWUlKilpUU2m01ZWVk64ogjdPTRRys1NdXsEkekoaFBe/bsUUVFhZqamtTd3a24uDhlZGQoIyNDmZmZmjNnTshm7odaZ2en3nvvPdXW1qqhoUE9PT3KycnR5MmTtXz58hGtTREsr9erbdu26eOPP1Z9fb0Mw1Bubq6mT5+uE088MaRhbkVFhd59913V19ervb1dSUlJys/P18KFC7VkyZJhhZ2xwuVyadeuXSopKVFtba06Ozvl9Xp9r928vDzfOgGhYJVzgMvl0gcffKCqqio1Njaqra1NEyZMUHp6umbOnKkFCxYoLy8vIrVEQmNjoz766CM1NjaqublZHR0dSklJUVpamqZOnap58+apsLBwTL32AQAAYF0E+AAAAAAAAAAAWBAtdAAAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIISzC4AAIBoZhiGXC6XnE6n779Op1M9PT3yeDwyDMP3JUlxcXGy2Wyy2WwaP368EhMTlZSUpKSkJN//x8fHm/ysAAAAECt6enr6fb/q8Xjk9Xr93qv2vU+Ni4tTfHx8wPvUxMREjR8/3uRnBABji83oO0sDAIAhuVwutbW1qb293fchKNQSEhKUlJSk1NRUpaenKyUlRTabLeTHAQAAQGzxeDxqb29XW1uburu75XQ65fV6Q3qMuLg4JSUlacKECUpPT9fEiROZgAIAYUSADwDAIAzDUGdnp9ra2mS32+V0OiNeQ0JCgtLT0/mABAAAgAB9E0za2trkcDgU6ZjHZrMpLS3N9341MTExoscHgFhHgA8AwGEMw/AF9m1tbert7TW7JJ9Pf0DKyspSQgLd8AAAAMaarq4utba2mjbBZDBJSUnKyMhQZmamkpOTzS4HAKIeAT4AAP8/j8ejpqYmNTQ0qKenx+xyhmSz2ZSdna3c3FwlJSWZXQ4AAADCrK2tTfX19XI4HGaXMixpaWnKzc1Venq62aUAQNQiwAcAjHlut1sNDQ1qbGyUx+Mxu5wRycjIUG5urlJTU80uBQAAACFkGIZaWlpUV1dnudn2w5WUlKS8vDxlZWWxthMABIkAHwAwZnV3d6u+vl4tLS0R7xUaLikpKcrLy1N6ejofjgAAAKKYx+NRY2OjGhoa5Ha7zS4nJMaNG6fJkydr0qRJrOsEAMNEgA8AGHN6enpUWVkpu91udilhk5SUpKlTp3K7MgAAQJTxer2qr69XXV2dvF6v2eWERVxcnPLy8pSbm6u4uDizywEASyPABwCMGYZhqKGhQTU1NTH7YehwmZmZmjZtmsaNG2d2KQAAABiCw+FQRUVF1LbKCVZSUpKmT5+utLQ0s0sBAMsiwAcAjAmdnZ0qLy9Xd3e32aVEXHx8vPLz8zVp0iTa6gAAAFhQb2+vqqqq1NzcbHYppsjOztbUqVOVkJBgdikAYDkE+ACAmObxeFRdXa3GxkazSzFdSkqKpk+fruTkZLNLAQAAwP+vqalJVVVV8ng8ZpdiqoSEBBUUFCgnJ8fsUgDAUgjwAQAxq6WlRZWVlert7TW7FEvJzc3VlClTWDgMAADARE6nU+Xl5ero6DC7FEtJTU1VYWGhkpKSzC4FACyBAB8AEHO8Xq8qKirG7C3Iw5GUlKRZs2bxwQgAAMAEzc3NKi8vF5FM/2w2m4qKipSVlWV2KQBgOgJ8AEBM6enp0aFDh9TV1WV2KZYXHx+vGTNmKD093exSAAAAxgTDMFRVVaWGhgazS4kKkydP1tSpU1nHCcCYRoAPAIgZDodDJSUltMwJUn5+vvLy8vhgBAAAEEa9vb0qKSmRw+Ewu5SokpaWppkzZ7LALYAxiwAfABATGhoaVFVVxW3II5SRkaGioiL64gMAAIRBV1eXDh06pJ6eHrNLiUrjx4/XrFmzlJycbHYpABBxBPgAgKhGv/vQoS8+AABA6LW0tKisrIyJJqNEX3wAYxUBPgAgarndbhUXF9PvPoToiw8AABAahmGourpa9fX1ZpcSU3Jzc1VQUED7RwBjBgE+ACAqud1u7d+/Xy6Xy+xSYo7NZtPMmTOVkZFhdikAAABRyTAMVVRUqKmpyexSYlJOTo6mT59OiA9gTIgzuwAAAIJFeB9ehmGopKREdrvd7FIAAACiDuF9+DU1NamiooK2RADGBAJ8AEBUIbyPDEJ8AACA4BHeRw4hPoCxggAfABA1CO8jixAfAABg+AjvI48QH8BYQIAPAIgKhPfmIMQHAAAYGuG9eQjxAcQ6AnwAgOUR3puLEB8AAGBghPfmI8QHEMsI8AEAltbb20t4bwF9IX57e7vZpQAAAFhKZWUl4b0FNDU1qbKy0uwyACDkbAaXJwEAFmUYhg4ePCiHw2F2Kfj/xcfHa968eUpKSjK7FABjlGEYcrlccjqdvv86nU719PTI4/HIMAzflyTFxcXJZrPJZrNp/PjxSkxMVFJSkpKSknz/Hx8fb/KzAhCtGhoaCI0tZtq0aZo8ebLZZQBAyBDgAwAsq6KiQo2NjWaXgcMkJSVp3rx5BF4AIsLlcqmtrU3t7e2+0D7UEhISlJSUpNTUVKWnpyslJUU2my3kxwEQWxwOhw4cOGB2GejHnDlzlJaWZnYZABASBPgAAEtqampSeXm52WVgAOnp6Zo1axYBF4CQMwxDnZ2damtrk91ul9PpjHgNCQkJSk9PV3p6uiZOnMgFSwABXC6X9u7dK4/HY3Yp6EdCQoLmzZunxMREs0sBgFEjwAcAWE5HR4cOHDjAIlQWl5eXp4KCArPLABADDMPwBfZtbW3q7e01uyQfm82mtLQ0paenKysrSwkJCWaXBMBkHo9H+/fvV3d3t9mlYBATJkzQ3LlzuQgLIOoR4AMALKWnp0d79+61VHiDgc2YMUNZWVlmlwEgSnk8HjU1NamhoUE9PT1mlzMkm82m7Oxs5ebmshYIMEYZhqGSkhLZ7XazS8EwZGRkaObMmdw1CiCqEeADACzD6/Vq//796urqMrsUDFNcXJzmzp2r5ORks0sBEEXcbrcaGhrU2NgYte0nMjIylJubq9TUVLNLARBBNTU1qq2tNbsMBCE/P19TpkwxuwwAGDECfACAZZSWlqqlpcXsMhCk8ePHa968eRo3bpzZpQCwuO7ubtXX16ulpSVm2qSlpKQoLy9P6enpzPAEYpzdbtehQ4fMLgMjMGvWLGVkZJhdBgCMCAE+AMASGhoaVFlZaXYZGKG0tDQdccQRhFcA+tXT06PKysqYbjmRlJSkqVOnKj093exSAISBy+XSnj175PV6zS4FIxAXF6cFCxawqC2AqESADwAwndPp1J49e2JmNuZYNXXqVOXm5ppdBgALMQxDDQ0NqqmpGTOhV2ZmpqZNm8ZdSUAMMQxD+/fvV2dnp9mlYBRSUlI0d+5cJpwAiDpxZhcAABjbDMNQaWkp4X0MqK6uVnd3t9llALCIzs5O7d27V1VVVWMmvJek1tZW7d69Ww0NDfzbBsSIuro6wvsY0NnZqfr6erPLAICgEeADAExVW1vLorUxwjAMlZWVEVgBY5zH41FFRYX27ds3Zi/qeTweVVZWsjA7EAO6urpYtDaG1NTUcF4GEHUI8AEApunq6lJdXZ3ZZSCE+JALjG0tLS3atWuXGhsbzS7FEj59F4LH4zG7HABB8nq9TE6IMX0TTsbSnWEAoh8BPgDAFMzWjl11dXXMbALGmL6Qq7S0VL29vWaXYzn19fXat2+fnE6n2aUACEJdXd2YvZMolnV3dzOJCEBUIcAHAJiCD0SxyzAMlZeXc3EGGCN6enq0f/9+NTc3m12KpTmdTu3bt09tbW1mlwJgGAh5YxufRQBEEwJ8AEDEOZ1O2qzEuK6uLhYJA8YAh8OhvXv3ctfNMHk8HhUXF6u2tpaLnICFMRkh9vFnDCCaEOADACKKN8tjR01NjVwul9llAAiThoYGHTx4kJY5I1BTU6OSkhL64gMW1dDQoM7OTrPLQJh1dnayZguAqECADwCIqJaWFnV0dJhdBiLAMAxVVlaaXQaAEOvrd19ZWcnF2FGw2+30xQcsyO12q6amxuwyECHV1dVyu91mlwEAgyLABwBEjNfrVXV1tdllIILa2trkcDjMLgNAiLjdbvrdhxB98QHrqa2tldfrNbsMRIjX66W1JwDLI8AHAERMQ0MDM1zGoKqqKmbpAjGgL7yn331oeTweHTp0SHa73exSgDHP6XTSUmUMampq4m4oAJZGgA8AiIje3l7V1dWZXQZM0NXVpdbWVrPLADAKfeE961qEh2EYKikpIcQHTMadomOTYRj82QOwNAJ8AEBE1NbWsljfGFZdXc3t6ECUIryPDEJ8wFwdHR38/RvD7HY763QBsCwCfABA2LlcLm5HHuN6enrU1NRkdhkAgkR4H1mE+IB5qqqqzC4BJmMWPgCrIsAHAIRddXU1PdDBXRhAlCG8NwchPhB5drtdnZ2dZpcBk3EXBgCrIsAHAIRVZ2cn/c8hiXUQgGhCeG8uQnwgcgzDYPY9fJh4BMCKCPABAGHFraj4tPr6evX09JhdBoBB9Pb2Et5bQF+I397ebnYpQExramrifAcfp9NJ20cAlkOADwAIm7a2NjkcDrPLgIUYhqHa2lqzywAwgL7QmDDLGvr+PJxOp9mlADHJ6/WqpqbG7DJgMTU1NfJ6vWaXAQA+BPgAgLCpr683uwRYUHNzs9xut9llAOhHZWUlF14txuPx6NChQ6whAoRBc3Ozent7zS4DFtPb26vm5mazywAAHwJ8AEBYdHV1EQKhX4ZhqKGhwewyABymqalJjY2NZpeBfjidTpWWltKXGQghwzCYbIIBNTQ0cM4FYBkE+ACAsOADEQbT2NjIrcmAhXR0dKiiosLsMjCItrY2Wn0AIdTW1ka7MAzI6XSyBgkAyyDABwCEXE9Pj1pbW80uAxbm8Xi4NRmwiJ6eHh06dIiZhlGgrq5OLS0tZpcBxATuBsRQmJAEwCoI8AEAIdfY2EgQhCHV19fzOgFM5vV6dejQIXpAR5Hy8nJ1dXWZXQYQ1Wj1iOFwOBycbwFYAgE+ACCkvF4vPZQxLC6Xi1uTAZMRBkefvosuLAYOjBwzqzFc3KkBwAoI8AEAIdXc3CyPx2N2GYgSfIAGzNPQ0EA7lijV09PDorbACLndblo9YthaWlq4YArAdAT4AICQMQyDQBZB4dZkwBxOp1NVVVVml4FRcDgczAwFRqChoYGLXxg2wzA41wIwHQE+ACBk2tra5HK5zC4DUYaLPkBkGYbB7O0YUV1dre7ubrPLAKIGrR4xEo2NjfJ6vWaXAWAMI8AHAIQMs1MwEq2trdyaDERQbW0td77ECMMwVFZWxsUYYJho9YiR8Hg8am5uNrsMAGMYAT4AICRcLpccDofZZSAKGYbBhyIgQrq6ulRXV2d2GQihrq4u1dbWml0GEBWamprMLgFRitcOADMR4AMAQoKFEDEavH6A8GO2duyqq6vjrgpgCE6nk78nGLGuri45nU6zywAwRhHgAwBCggAWo9Hd3U0fZyDM6urq+HsWowzDUHl5ORdngEHwXhWjxWsIgFkI8AEAo9bZ2cmMFIwaH4qA8HE6nbRZiXFdXV0sCg4MghYoGC3eqwIwCwE+AGBUPB6PSktLzS4DMaClpYXZo0AYMDt77KipqZHL5TK7DMByqqqq5Ha7zS4DUc7lcqmzs9PsMgCMQQT4AIARczqd2rdvH2EBQqKnp4cPRUAYtLS0qKOjw+wyEAGGYaiystLsMgDL8Hq9Kisr4+4UhAyz8AGYgQAfADAibW1t2rdvH61zEFJ8KAJCy+v1qrq62uwyEEFtbW1yOBxmlwGYzu12a//+/Wpubja7FMQQ7hgFYAYCfABA0Ox2uw4dOiSPx2N2KYgxfCgCQquhoYG2EWNQVVUV51KMaX3hfVdXl9mlIMb09vZykRRAxBHgAwCCYrfbVVJSQjCAsPB4PGprazO7DCAm9Pb2qq6uzuwyYIKuri61traaXQZgir7wnhaPCBfu6gAQaQT4AIBhI7xHJNBGBwiN2tpa7pQaw6qrq+X1es0uA4gowntEgt1u5/wKIKII8AEAw0J4j0ix2+2EjsAouVwuNTY2ml0GTNTT06OmpiazywAihvAekeL1emW3280uA8AYQoAPABgS4T0iyTAMdXR0mF0GENWqq6s5Z4O7MDBmEN4j0trb280uAcAYQoAPABgU4T3MQB98YOQ6Ozvpfw5JrIOAsYHwHmYgwAcQSQT4AIABtbW1Ed7DFHwoAkauurra7BJgIfX19erp6TG7DCAsent7Ce9hCrfbre7ubrPLADBGEOADAPrldDpVWlpKeA9TuFwuPowDI9DW1iaHw2F2GbAQwzBUW1trdhlAyBmGoZKSEt4vwDTcMQogUgjwAQABPB6PiouL6ZsLUzELHwhefX292SXAgpqbm+V2u80uAwipyspKLljCVLxXBRApBPgAAD/MZoJV8KEICE5XVxdhFvplGIYaGhrMLgMImaamJjU2NppdBsa4jo4Oeb1es8sAMAYQ4AMA/FRXVxOcwhIcDgctnIAgMPseg2lsbCRoQkzo6OhQRUWF2WUAMgyDC+cAIoIAHwDg09LSQgAEy/B4POrs7DS7DCAq9PT0qLW11ewyYGEej0fNzc1mlwGMSk9Pjw4dOsQFflgGE58ARAIBPgBA0ietF8rKyswuA/DDhyJgeBobGwm0MKT6+npeJ4haXq9Xhw4dUm9vr9mlAD68VwUQCQT4AAC53W4VFxfzoR6Ww4ciYGher5de0BgWl8vFeRVRq7y8XF1dXWaXAfhxOp3q6ekxuwwAMY4AHwDGOMMwVFpaKrfbbXYpQIDOzk5m2gFDaG5ulsfjMbsMRAla5SEaNTQ0qKWlxewygH5xYRRAuBHgA8AY19DQwOJLsDT64AMDMwyDQBZBcTgczGJGVHE6naqqqjK7DGBAHR0dZpcAIMYR4APAGNbd3a3q6mqzywAGRdAEDKytrU0ul8vsMhBluOiDaNF3pyhtHmFlvFcFEG4E+AAwRvGBCNGCGfjAwBoaGswuAVGotbWV1nmICrW1tYSjsLzu7m55vV6zywAQwwjwAWCMqqmpUXd3t9llAEPigzvQP5fLRQs0jIhhGGpubja7DGBQXV1dqqurM7sMYFh4vwognAjwAWAM4gMRoonb7WamKNAPFnTEaPD6gZUZhqGysjLuFEXUIMAHEE4E+AAwxhiGofLycrPLAIJCGx0gEAEsRqO7u5s78WBZdXV1vD4RVXivCiCcCPABYIypr69nhgiiDq9ZwF9nZ6ecTqfZZSDKcREIVuR0OlVbW2t2GUBQeK8KIJwI8AFgDHE6naqpqTG7DCBofCgC/svj8ai0tNTsMhADWlpaaFECS+m7U5TXJaKN0+mUx+MxuwwAMYoAHwDGkKqqKj4QISpxWzLwCafTqX379snlcpldCmJAT08P51dYSktLizo6OswuAxgRJpwACBcCfAAYIxwOh9ra2swuAxiR3t5e9fT0mF0GYKq2tjbt27eP1jkIKdrowCq8Xq+qq6vNLgMYMQJ8AOFCgA8AY4BhGKqqqjK7DGBU+FCEscxut+vQoUPcno+Qo40OrKKhoUFut9vsMoAR470qgHAhwAeAMaC1tZU3lIh6vIYxVtntdpWUlBCyIiw8Hg936MF0vb29qqurM7sMYFR4rwogXAjwASDGcTsyYgVtQzAWEd4jEmijA7PV1tZyhxGinsvl4t9rAGFBgA8AMa6xsZHe4YgJvI4x1hDeI1LsdjvhKUzjcrnU2NhodhnAqBmGQRsoAGFBgA8AMczj8XA7MmKGy+UyuwQgYgjvEUmGYaijo8PsMjBGVVdXc65DzOD9KoBwIMAHgBhWV1en3t5es8sAQqK3t5cZohgTCO9hBvrgwwydnZ1qbW01uwwgZAjwAYQDAT4AxKienh7V19ebXQYQUnwoQqxra2sjvIcp2tvbzS4BYxDrNCHW8F4VQDgQ4ANAjKqpqSEAQszhQxFimdPpVGlpKedumMLlcnGORUS1tbXJ4XCYXQYQUpxHAYQDAT4AxCC3262WlhazywBCjoVsEas8Ho+Ki4tpEwVTMQsfkcSdoohFvFcFEA4E+AAQgxoaGpjBiZjErCbEIsMwVFJSwusbpiPAR6R0dXUx+x4xiX/LAYQDAT4AxBiv16vGxkazywDCgg9FiEXV1dUEp7AEh8PBBABEBLPvEat6e3u5mw5AyBHgA0CMaW5u5k0jYhYBPmJNS0sLQRYsw+PxqLOz0+wyEON6enrU2tpqdhlA2PB+FUCoEeADQAwxDIMgCDGtp6eH2aGIGV1dXSorKzO7DMAPd4Mg3BobG/m3HDGNPvgAQo0AHwBiSFtbGzM+ENMMw5Db7Ta7DGDU3G63iouLCbFgOQT4CCdaPWIs4PMYgFAjwAeAGNLQ0GB2CUDY9fb2ml0CMCqGYai0tJSLUbCkzs5OzrMIG1o9YizgHAog1AjwASBGdHV1yeFwmF0GEHZ88Ee0a2ho4HwNS6MPPsKBVo8YK3ivCiDUCPABIEbwgQhjBR+KEM26u7tVXV1tdhnAoLq6uswuATGIVo8YK3ivCiDUCPABIAa43W61traaXQYQEdyWjGjV1zqHvvewOmbgIxxo9YixgveqAEKNAB8AYkBzczOBEMYMZjUhWtXU1Ki7u9vsMoAhMQMfoeZyuWgdhjGD96oAQo0AHwBiQEtLi9klABHDrCZEo66uLtXV1ZldBjAsbrebRZYRUrxXxVjCe1UAoUaADwBRrru7mxmdGFOY1YRoYxiGysvLzS4DCAptdBBKBPgYS3ivCiDUCPABIMo1NTWZXQIQUXwoQrSpr6+nJQmiDq9ZhEpnZ6ecTqfZZQARw3tVAKFGgA8AUayzs5MFwTDmcFsyoonT6VRNTY3ZZQBBI8BHKHg8HpWWlppdBhBRhmEQ4gMIqQSzCwAAjExLS4vKysrMLgOIOD4QIZpUVVWxyDiiEi10MFpOp1OHDh2Sy+UyuxQg4jwej+Lj480uA0CMIMAHgChjGIaqq6tVX19vdimAKQjwES0cDofa2trMLgMYkd7eXvX09Gj8+PFml4Io1NbWptLSUv7NxpjFax9AKNFCBwCiiGEYqqioILzHmEYLHUQDwzBUVVVldhnAqNBGByNht9t16NAhAkyMabxfBRBKBPgAECX6wnsWrcVYRzsSRIPW1lbCT0Q9XsMIlt1uV0lJCf9WY8zj7wCAUCLAB4AoQHgP/BcfiGB1Xq9X1dXVZpcBjJrT6TS7BEQRwnvgv/h7ACCUCPABwOII7wEgujQ2Nqqnp8fsMoBR43WM4SK8BwAgfAjwAcDCCO+BQIQDsDKPx6O6ujqzywBCwuVymV0CogDhPRCIvw8AQokAHwAsivAe6B8fiGBldXV1LFyHmNHb28tCpBgU4T0AAOGXYHYBAID+VVZWEt4DYeZyuTR//nyVlpYqOztbBQUFKigoUH5+vt9XZmam0tLS/L6SkpJks9nMfgqwkJ6eHtXX15tdBhBSLpdLycnJZpeBEDMMQ06nUw6Hw++rtbVVNTU1fl/V1dWqrq5Wc3OzZsyYob179yoxMVFtbW2E98AA+HsBIJQI8AHAghoaGtTY2Gh2GUDMczgcKi0tlSQ1NzerublZO3fuHPW4iYmJA14MmDx5siZOnKi0tDRNnDhRGRkZSk5O5mJADKipqeEDO2IOAb51GIahrq4u2e12tbe3y+FwqL29XQ0NDQOG7qFug1RaWiqHwyHDMFRaWso5DwCACCDABwCLcTgcqqysNLsMYEzIyclRZ2enOjs7FR8f7wtE+kKRuro6XwhyeDgyWGjhcrlUUlKikpKSUdU3ZcoUzZgxw+9r6tSpyszMVGZmpjIyMpSenq5x48aN6jgYPbfbrZaWFrPLAEKOhWxDy+12q62tTa2trbLb7WptbVVlZaVKS0v9viKxlobNZvO7wNx30bmgoEB5eXm+i819F5w9Ho9SUlKUmJiovXv30l4JGAQTMwCEEgE+AFiIy+XSoUOHzC4DsLRQfyBKTk72zS7Nysoa9Xher1ddXV1+FwMcDoeam5v9ZkVWV1ersrJSZWVlA4YgtbW1qq2t1TvvvBN0HYmJiQHh/4wZMzRp0iRf+J+ZmamUlBQ+ZIZAQ0MDM1ERk1jI1p9hGOrs7PQL4BsbGwMC+NLS0rD87uLj41VUVKRp06b57vTqC96zs7P9Wr1NnDhRycnJiosL3dJ3hmGouLiY1wUAABFEgA8AFuHxeHTo0CFmMwFDsHrYHBcXp9TUVKWmpo5qHJfL5TdLs6WlRRUVFQEB0UBrZbhcLu3bt0/79u0b0fFzc3MDwv9p06b5zf7PyMhg9r8+uWhD2zPEqlgMat1ut+x2uy+AH2gWfLjWtMjJyQk4v06fPl1ZWVm+i6vp6elKTEwMy/FHo7q6Wu3t7WaXAVie1d+vAoguBPgAYAGGYaisrEzd3d1mlwLAIhITEzV58mRNnjw56H0Nw5DD4fCFU3a7XfX19SorKwsIqNxud79j1NfXq76+Xu+9915Qx168eLGWLl2qpUuXas6cOcrLy1Nubq5yc3OVlJQU9HOJBs3NzVx8RcyKlgDf6XSqvr5edXV1qq+v14EDB7Rjxw7t2LFDH3/8cUiPNW7cuIAAvqioSLm5ub4APiMjQ2lpaTEV4rW0tLBQNwAAJrAZ3OsLAKarqalRbW2t2WUAUSE+Pl7Lli0zu4yY1tPTM+Ts1EOHDg04+3+4lixZoqVLl2rJkiVRG/YbhqHdu3dHTcgJBMtms2n58uWmBNF9oXxfMN8Xyu/cuXPUC47n5ORo1qxZQ95lNH78+BA9m+jW1dWlffv20SoMGKYjjjhCEydONLsMADGCGfgAYDK73U54DwQhlmYzWtX48eODnv1vGIba2tp8s19ramq0a9cuX9BWUVERsM9wQ7icnBytXLlSJ598smbPnu2b7TphwoSgnlc4tLW1Ed4jphmGIbfbHfIgu7u723dX0MGDB7V582a98cYbo7owOH36dN+FwYULFyo/P1+5ubnKy8tTeno6/36MkNvtVnFxMeE9EATONwBCiRn4AGAil8ulPXv2yOv1ml0KEDWSkpK0cOFCs8vAKAwW9u/YsUOVlZVBjTdz5kytXr1axx57rGbOnOnrJx0fHx+mZ/BfBw4ckMPhCPtxADPNnz/ft9j3cHk8Ht+6HSUlJXr//fe1YcMGlZSUBDVOXyi/ZMkSLVq0SPn5+b67dQjlw88wDB08eJDzHBCkBQsWWGKiAYDYQIAPACYxDEP79+9XZ2en2aUAUSUlJUXz5s0zuwxEiMvlUnl5ua9tz9tvv60NGzaorq5uyH3T0tJ05ZVX6uijj9aKFSu0YMGCkM4i7urq0t69e0M2HmBVc+bMUVpa2oA/7+np0Z49e7R161Z98MEHeuyxx4YV+Obl5Wn16tX6zGc+42tnU1hYaMnFW8eq+vp6VVVVmV0GEHUWL15MCy4AIUOADwAmqa2tVU1NjdllAFFn4sSJOuKII8wuAxbR3t7ua8Oxc+dOPffcc9q6deuAj58xc5ZWn7JKp512ms477zylpKSM+NilpaVqaWkZ8f5AtJg1a5YyMjJ833d2durf//63XnvtNW3YuEmlJYcG3HfFihW68MILtWTJEl+v+cEuBsA6uru7tXfvXlrnACOwbNmyiNwJCGBsIMAHABOwEBgwcllZWZoxY4bZZcDi2tvbtWPHDm3dulWbN2/WM888E/CYpAnJuuD883XddV/U6aefHtT4brdbH3/8MedxjAmFhYXKycnRq6++qgceeFD/ev55Obu7Ah530UUX6aSTTtKKFSu0dOlSFnCMYoZhaO/everu7ja7FCDq2Gw2rVixwuwyAMQQAnwAiDCv16t9+/bxgQgYoUmTJmn69Olml4EoVFlZqaeeekqPPPYP7dj2ySz9Cbkz1F1fqttuu00/+9nPhj1WXV2dqqurw1UqYClTp07VPffco5///Oe+vzOStHT5Cl115RW69NJLNW3aNJOrRChVV1cPq1UZgEAJCQlaunSp2WUAiCEE+AAQYTU1NaqtrTW7DCBq5eXlqaCgwOwyEOXOOussvfTSS5r2rafV+M87lGgvk711+O1w9uzZw4VYjBl5eXlauGixXBlFmvS5H6rydxfrzDPP1Isvvmh2aQgD1vcARicxMVGLFi0yuwwAMSTO7AIAYCzp7u5mNhMwSgkJCWaXgChVXl6uu+66S8tWHKmXXnpJktT02PfkLNumm7/6lWGP093dTXiPMcXj8eirX/l/cpZtU9Nj35MkvfTSS1q24kjdddddKi8vN7lChIphGPx5AqPEe1UAocYMfACIEMMwtH//fnV2dppdChDV+noxA4Npb2/X9u3btW3btiF64J+n6667Lqge+JWVlWpoaAhluYCl9a098uqrr+r+++/X8/9eP2gP/OXLl2vZsmX0wI9CtAcDRm/ixIk64ogjzC4DQAwhwAeACKmvr1dVVZXZZQBRb9asWcrIyDC7DFiEw+FQaWmpSktLtX37dj333HPavn37gI+fOWu2Tlm1UmeccYbOOeccJScnB3W8zs5O7du3b5RVA9Hl8DCqs7NTL7zwgl5++WVt3PSGSg4VD7jvsmXLdOGFF2rZsmWaMWOGZsyYobS0tEiUjSA5nU7t2bOHxbmBUeq76AkAoUKADwAR4Ha7tWvXLnm9XrNLAaLenDlzCH/GEKfTqbKyMpWWlurQoUN66623tGHDBjU2Ng65b3pGhq64/HIdffTRWr58uRYsWKDx48ePuJaWlhaVlZURbmHMSUlJ0bx58wb8eU9Pj/bs2aNt27bpgw8+0D8ef1xtdvuQ406ePFmnnHKKTjzxRM2ePVtFRUUqKipSUlJSCKvHcBUXF6utrc3sMoCoN2nSJE2fPt3sMgDEEAJ8AIiAioqKYYVNAIY2f/78oGdNw1oMw5Ddbld9fb3q6+tVXV2tXbt2aefOndq5c6cqKyuDGm/GjBlavXq1jjvuON8M36KiIsXFhWa5J8MwVF1drfr6+pCMB0SbpKQkLVy4MKh9vF6v7+JbaWmp3nvvPW3YsEGlpaVBjTNt2jQtWbJES5Ys0aJFi1RQUKDc3Fzl5uYqIyNDNpstqPHQP4fDoQMHDphdBhAT8vLyVFBQYHYZAGIIAT4AhJnT6dTu3bvNLgOIGYsXLx7VLGqEh2EYam1t9YXyVVVVfqH8aHoqT5o0yW+W7owZM1RYWKgJEyaE8Bn0zzAMVVRUqKmpKezHAqwqISFBS5cuDemY3d3dKi8vV2lpqYqLi/XWW29p48aNo5rwUFBQ4Av7Fy9e7Bf2Z2ZmEvYPwDAM7du3T11dgesaAAje1KlTlZuba3YZAGIIAT4AhNmhQ4dkH8Zt5ACGZrPZtHz5ckKYMHO73bLb7WptbZXdbldLS4sqKyt9s2lLSkp08OBBtbS0jOo4S5cu9YVtc+bMUW5urvLy8jR58uSIhPNDIbwHPhEfH69ly5ZF/Ljd3d1qaGhQXV2d6uvrdeDAAd9FwR07doxq7KysLB1xxBGaOXOm766d6dOnKzMzUxkZGb7/jhs3LkTPxrpaWlqCvjMCwMBYrwlAqBHgA0AYdXR0aP/+/WaXAcSMxMRELVq0yOwyLM/r9crhcPiF8PX19b5WFn1fZWVl6u3tDdlx4+Pj/UL52bNnKy8vT7m5uZo8eXJU9bUmvAf+Ky4uTsuXLze7jEE5nU41NDSovr5edXV1Ki4u9gX9O3fulMfjCdmxEhISVFRU5GvZ1ffVN9M/IyNDGRkZSktLC1krr3Dxer3avXu3enp6zC4FiBm0ewQQaglmFwAAsayqqsrsEoCYkpiYaHYJEeN0Ov0C+ObmZl+7ib7wvbS0VK2trWE5fl5enm9Wal84NXXqVGVlZfnCqYyMjJhsZ0R4D0SfpKQkTZ8+fVgLR/b09Mhut/u+WlpaVFVVFXCRc6B1L3p7e1VcXKzi4uIR1ZqZmekX/BcVFamwsFDZ2dl+s/8jcdGzsbGR8B4IsbH0fhVAZBDgA0CY2O12dXZ2ml0GEFOi4QOR1+tVZ2en2tvb5XA4fF9NTU2qqalRdXW176usrEwVFRVhqSM5OTkgICoqKtKkSZP8AqKUlBRaEn0K4T0QKNZu2h4/frwmT56syZMnB72vYRjq7Oz0u8Da2Njot2Bv3/8P1FO+tbVVra2t2rp164jqnz59uoqKilRQUOD7ys/PV05OjtLS0nxfEydOVEpKyoB3AXg8HtXV1Y2oBgD9S0hIUHx8vNllAIgxBPgAEAaGYTD7HgiDcAX4PT09AYF7W1ub6urqfGF7TU2N76u2tjYsdRxu2rRpAS0a8vPz/Xo0T5w4UQkJvKULBcJ7oH+xFuCPhs1mU2pqqlJTUzV16tSg9+/t7VV7e7sv/G9tbVVNTU3A7P/KysoBx6ioqAjJxd+vfOUruvbaa0c9DoD/iobJJgCiD5/2ACAMmpqa5HK5zC4DiDmhbtdy4MABzZ07N6RjStKECRP8ZkV++mvSpEmaOHGi0tLSlJ6erszMzKjqDR/LKisrCe8BhFVCQoKysrKUlZU1ov2dTqdaW1vV1tYmh8Oh9vZ2NTY2+l1k/vTdXt3d3f2OM3nyZF1++eWjeSoA+hGLrQUBmI8AHwBCzOv1qqamxuwygJgU6llN2dnZOuaYY7RlyxZNnjy538A9Pz/ftxjhp78SExNpPRNDGhoa1NjYaHYZADCopKQkTZkyRVOmTBnVOGVlZWpubg5RVQD6MAMfQDgQ4ANAiDU3N6u3t9fsMoCYFI4A//333w/pmIg+Dodj0HYVABBL3G63WlpazC4DiEkE+ADCof/VbAAAI2IYhurr680uA4hJLAqGcHC5XDp06JDZZQCWxt1GsaWhoYF1DYAwIcAHEA4E+AAQQm1tbfS+B8KED0QINY/Ho0OHDsnj8ZhdCmBpBPixw+v10i4MCCPerwIIBwJ8AAihhoYGs0sAYhaLgiGUDMNQWVnZgAs8AkAsam5u5qIlECY2m03jxo0zuwwAMYgAHwBCpKurSw6Hw+wygJiVlJRkdgmIIbW1tbLb7WaXAUQFZuDHBlo9AuGVmJjI+RJAWBDgA0CI8IEICK/k5GSzS0CMsNvtqq2tNbsMU61du1ZHHXWUbrzxxqB+hrGJQCo20OoRCC/eqwIIlwSzCwCAWOB2u9Xa2mp2GUBM40MRQsHlcqm0tHTE+69du1b33Xef3zabzabk5GSlpKQoLy9Pc+fO1ZFHHqmVK1dyK30IbNq0Sfv379fcuXO1atUqs8sZkxIS+NgYC2j1CIQX71UBhAvvxAAgBBoaGmQYhtllADErISGBHvgYNcMwVFpaKq/XG5LxsrOzff/vdDrV2NiohoYG7dy5U+vWrVN6erq+/OUv66KLLoqqGcwZGRkqLCxUXl6e2aVI+iTAX79+vc4991wCfJPEx8ebXQJGiVaPQPgR4AMIFwJ8ABglr9erxsZGs8sAYlpKSorZJSAG1NXVqbOzM2Tjvfzyy37fezwelZaW6r333tO6detUXV2tX/3qV9q+fbt+9rOfRU2If9lll+myyy4zuwxYCAF+9KPVIxB+BPgAwoUe+AAwSs3NzfJ4PGaXAcQ0PhBhtLq6usLe9z4+Pl6zZ8/W5z//eT355JNas2aNJOmll17SQw89FNZjA+FEC53oRqtHIPySkpK42AkgbHgnBgCj1NTUZHYJQMwjwMdoeL1elZWVRbTVWVJSkn7yk5+ovLxc+/fv10MPPaTPfe5zSk9Pl/TfXvorVqzQ3/72t37H+PDDD/WlL33J9/+fdvj+r776qtatW6fi4mL19PSoqKhI5513ni6++OKgA4Xh1Ga327Vu3Tq99dZbqqyslNPpVHZ2tgoLC7Vq1SqdeeaZSk1N9T2+paVFmzdv1ltvvaXS0lI1Njaqt7dXkyZN0pFHHqkrrrhCs2bNGvD5S9L69eu1fv16v8fce++9Ouqoo/y2VVVV6R//+Ie2bNmi+vp6eb1eTZkyRccff7yuvPJKy7QGiiaEUtGtubmZVo9AmPFeFUA4EeADwCg4nU51dXWZXQYQ82ihg9Goq6tTd3d3xI87btw4XXvttbrlllvU2dmpTZs26YILLgj5cf74xz/q4Ycfls1mU1pamnp6erRv3z7t27dPb731ln7729+GdA2J9957T7feeqva29slfRLuJicnq7a2VrW1tXrvvfeUk5Pj16/+j3/8o1/4npKSot7eXlVVVamqqkovvviifvazn+nUU0/1PWbcuHHKzs5WR0eHXC6XEhMT/S4K9D3m05599lndeeed6u3tlSSNHz9eNptNZWVlKisr0/PPP68777xTxx13XMh+H2MBAX50a2lpMbsEIObxXhVAOBHgA8Ao8IEICL9x48YFhHTAcHV3d6uurs60459wwgmKj4+Xx+PR1q1bQx7gHzhwQFu3btWll16qG264QZmZmero6NATTzyhtWvX6t1339U999yjb33rWyE53r59+/Ttb39bLpdLM2fO1Ne//nUde+yxSkhIkNPpVElJiV555ZWAmYj5+fm67rrrtHr1ak2fPl0TJkyQ1+tVaWmpHnroIb344ou6/fbbtWTJEk2aNEmStHTpUr388su6/fbbtX79ep1++um6/fbbB6xt06ZNuuOOO5SQkKAvfOELuuiii3yz7cvLy3Xvvffqtdde0/e//309+eSTzMQPAi10old3d7cpFzCBsYYZ+ADCiR74ADAKBPhA+PGBCCNlGIbKy8tNbR2RnJysgoICSZ+0dgm1jo4OnX322fre976nzMxMSVJqaqquv/56ffGLX5QkPfnkkyFbbP03v/mNXC6Xpk+frgcffFCf+cxnfOFuUlKSFixYoG984xs65phj/Pa78cYb9eUvf1lz587VhAkTJElxcXGaNWuWfvazn+nEE09Ud3e3/vWvf42oLrfbrV//+teSpB/84Af66le/qilTpshms8lms6moqEi/+tWvdPLJJ6uzs1OPPfbYKH4LYw8z8KMX71WByOD9KoBwIsAHgBHq7OyUy+Uyuwwg5nFLMkaqoaFBnZ2dZpehiRMnSpKv5Uyo3XDDDf1uv+qqq5SYmCiPx6PXX3991MepqKjQ9u3bJUn/7//9v4B2NqNx4oknSpJ27Ngxov3ffvttNTQ0KDs7W+eff/6AjzvnnHMkSe++++6IjjNWMQM/OhmGQYAPRMCECRMUF0e8BiB8eCcGACPEByIgMpjRhJFwu92qqakxuwxJCusdALm5uZo2bVq/P0tNTdX8+fO1fft27d27d9TH2rlzp6RPZmN/5jOfCXr/AwcO6J///Ke2b9+u2tpadXV1Bfxu6uvrR1RbX/Df3t6uM888c8DHud1uSVJtbe2IjjNWMQM/OnV2dqqnp8fsMoCYx3tVAOFGgA8AI8CMJiBymIGPkaitrZXX6zW7DEmSw+GQJKWnp4d87MmTJw/6875+8q2traM+VlNTkyQpIyPD1wZnuJ588kn99re/9f2Z2Gw2paam+hbXdTqd6uzslNPpHFFtfS2C3G63mpubh3w8d9AFhwA/Og3n7wKA0QvlHWkA0B8CfAAYgfb2dvX29ppdBhDzUlJSaN2AoDmdzpD1fB+trq4uVVdXS5KmTp0a8vFtNlvIxwy10tJS/e53v5PX69Vpp52mq666SnPmzPFbnPq5557Tz3/+8xHfreDxeCR9smjwH//4x5DUjf/iPBx9DMMIyYU7AEPra5UHAOHCOzEAGAFm3wORwQcijERfYG4F77zzji9cPvLII33b+2Y0D9beoqOjY8jxh2o503cho2+B29HIycmRJNntdnV3dw97Fv7rr78uj8ejGTNm6Be/+EW/fYJHO1O4r7bi4uJRjYNANpvN72ILokNbW5vv3AMgfJKSknx3kwFAuLDKBgAEyev1ym63m10GMCYQ4CNYHR0dljlHu91u/f3vf5f0ye31q1at8v2s77U9WAC/e/fuIY9RX1+vqqqqfn/W2dnp630/f/784ZY9oKVLl0r6ZLb722+/Pez9+p7jEUccMeAif1u2bBlw/767DAabnd9XW0NDg2+hXYTG+PHjo+JOD/hjsgkQGbxXBRAJBPgAECS73W6ZvspALIuPj6f/PYI2UJgdaU6nU7fffrv2798vSfrCF76gtLQ038/nzJkj6ZMZ8h9//HHA/i0tLXr22WeHdaz777+/3+2PPvqoXC6X4uPjtXr16mCfQoBp06ZpxYoVkqS//OUvw7pDQPpvb+Di4uJ+Q/i3335bH3300ZD7960l0J+TTjrJNwv/N7/5zZC99Nva2oasG59ITEw0uwQEyePxWOZCJhDrCPABRAIBPgAEiQ/9QGSkpaUx6xNBsdvt6uzsNO34Xq9XxcXFevTRR3XZZZfp5ZdfliSdffbZuuaaa/weu2TJEk2ZMkWSdPvtt2vPnj0yDENer1cffvihbrrppmH1g09NTdX69ev1m9/8xhfYdXZ26sEHH9QDDzwgSbr00kuHXOx2uL797W8rMTFRFRUVuu666/TOO+/41oRxOp3atWuXfvGLX+j999/37XP88cdLkkpKSnTnnXf6/h3t7u7WM888o+9///uDLvA7a9YsSdL27dtVVlbW72MSExN1yy23yGazad++ffriF7+od999V2632/eY6upqPfPMM7r66qu1bt26Uf0exhIC/OjjcDhGvJ4EgOGz2Wx+F+cBIFzogQ8AQWpvbze7BGBMYEYTgmEYRsRn359xxhm+/+/p6VFnZ6ffHVoZGRn68pe/rIsuuihg37i4ON1666365je/qfLycl199dVKSkqSYRhyuVyaPn26vve97+mHP/zhoDXMmTNHixYt0sMPP6ynnnpKaWlp6ujo8PW+PuaYY/TVr341RM9Ymjt3rn7729/qlltu0aFDh/S1r31NCQkJSk5O9vv38YQTTvD9/zHHHKM1a9bolVde0dNPP62nn35aaWlp6urqksfj0fz583Xuuefqrrvu6veYq1ev1p///Ge1trbq4osvVkZGhq///i9+8QstXrxYkrRq1Sr99Kc/1R133KEDBw7o5ptvVnx8vFJTU9Xd3e233sDKlStD9juJdQT40Yf3qkBkpKamDtgaDgBCiQAfAILQ1dXlm2kIILyGE+Dffffd+ta3viXpk/7a+fn5KigoUEFBgfLz85Wfn6+cnBylpaVp4sSJSktLU1pamlJSUvjAFWOamprkcrkiesy+hVdtNpsmTJig7Oxs5eXlae7cuTr66KN18sknD7r45/HHH6/7779fDzzwgHbs2CGn06m8vDytXr1a1157ra9//VC+9rWvad68eVq3bp2Ki4uVkJCgWbNm6fzzz9cll1ziWzA3VI477jg9++yzevzxx/X222+rqqpKLpdL+fn5Kiws1CmnnKKjjjrKb5+f//znWrx4sZ5//nmVl5fL6/Vq1qxZWrNmja644grf3Qr9mThxou677z79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jB07VuXl5fJ4PFF7zr4VfRUVFdpll12G/Ljf/Gaurr/+Op15ywLtftjw9vIdqeqKD3XPuQdo56mTtXLlSuO3bH91dWA4HO6/kwEAEB/d3d06cub39eEny3X+vW+qZKc943r9z955WY/87w904oknav7f/77NCuSf/ezn+vvjT+qK59cpLT1TlmXpD7N30g9nHqb7779vWNdqamrq33M/VtP+vfbaS8uXL9fbb7+tAw44YEiPOemkk/T000/r8ssv1y233BLVPHV1dSopKZGUuIfZtre3q6qqKmG3yhkun8+n8ePHx+QuEQBIFhT4AICU0NnZqfXr16u7u9t0lLhzuVwqLS1VQUHBqAvs7u5uZWRkSBrexPiJJ57QqaeeqiMv/K0OO++Xo8owVJubanXH6d9QZ2uTamuqVVRUFJfrbs/rr7+uww8/PCn35wWARLF582YdMuNQra2q1k8eeFv55fE98HTFq0/rH9ecovPP/4nuuefu/tfmuro6TZg4UTPO/fVWr5Uv/ukK/ef//qb6utphbznS99z19fUqLCyM3jfxX++99572228/7bLLLqqoqBjSY9544w0ddthhkmJ7mG0s9tmPpVAopI0bN6q5udl0FCPy8vJUVlYmt9ttOgoA2A7LvgAASS0cDquqqkoVFRUpWd5LW/4ONmzYoE8//VRdXV2jeq709HTdfffdWr58+ZAf8/777+vsc87R3t8/XYeee82orj9UwUC3/n7ZD9XV1qyc7DG2KO8l6eyzz5Yk3XXXXYaTAEDqGjNmjBa//JKK8nL0158fobaG6rhef48jTtTx19yne+/9i6655sui/g9/+KOc7jTtf/LPt/r63Q+breamRi1btmzY1/rzn/8sSbr55ptHF3oQ3/72tyVJn3766ZC3xDn00EP1+OOPa8mSJTHJtGDBAknS+eefH5Pnj4WmpiatXLkyZct7SWpubtZ//vMfNTU1mY4CALbDCnwAQNJqaWnRhg0bor7HaqIrKipSSUlJXA4Oq66u1rf22Vfe/PE6/y9vyOMb/NC+aLEsS0/86gwtf/VJRcJhLVu2TN/97ndjft0d6e3tldfrlZS4t/UDQDLZsGGDvnvAgQp5MnX+fW8pMyc/rtd/a/7v9eLtv9Att9yiOXPmqHz8eO1zws911MW/2+rrIpGIbjm2XGefdpJuv/32YV2js7NTWVlZ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2XXGcQqFQlq0aJGR6+/IvffeK0k699xzDSf50llnnSVJeuyxxxSJRKLynO3t7dqwYUNUngvRwxwCQLJhCx0AgC01NTVp/fr1pmNgENnZ2ZoyZcqIyunPP/9cu+66q478+U06+Ee/2O7Xbm6q1d1n7asp40u19K03lZ6evtXne3t75fV6JW15c8HptMfahJKSEtXV1WnFihXafffdTccBAETZZ599pgMOPEjpBeN13t2vyZc18u3lRuqeH++vvacU69mFIztbJpYsy+p/Te7t7bXNWTAHHnigli1bpueff14/+MEPRvVcPT09Wr169YgXNSC23G63pk2b1j9OBIBEZo9ZLgAAX9HR0aGqqirTMbAdbW1tqqmpGdFjd9ppJ5133hy9+dA8dbe3Dvp1wUC35v/iOKW7peefe3ab8l6S/va3v0nasqrOLuV9b2+v6urqJInyHgCS1M4776xXFr+sturP9fdf/HDId5VF066HztZLL72kzs7OuF97RxwOh4477jhJ0vPPP282zFf0jRtmzZo1qucZ7R2JiL1QKMS/IwBJwx4zXQAA/qu3t1dr1qwRN4jZX11dnVpaWkb02Guv/bXCwYDeeuTWAT9vWZYW3HCeGtas0AvPP6fS0tIBv+6nP/2pJOmWW24ZUY5YeOqppyRJP/rRjwwnAQDE0t57760X//lPVf/nPf3jmlMUDgXjev3dDj1ePYGAXnrppbhed6huvXXLa/ypp55qOMmXpk798uDhke6TblmW1q1bp+7u+L9pg+Hp7u7WunXrmFcASHgU+AAA24hEIlqzZo1CoZDpKBii9evXq6ura9iPKy0t1aX/c6mW/eOP2txUu83nX3/wRn388j/0yMMP61vf+taAz7F27dr+PxcVFQ07Q6yceeaZkqTf/va3hpMAAGLtgAMO0MJnntHn77ykp687N2p7qw9FfvlUle68pxY880zcrjkcfWV5KBSy1V0Cd911lyTpqquuGtHja2tr1draGsVEiKXW1tb+OyMBIFFR4AMAbGOkZTDM6XvTJRgc/qrDK6+8QhnpPr3+wA1bfXzFawv0yl9+rblzr9NJJ5006OPnzJkjSXr22WeHfe1Y+eqBaePHjzeYBAAQL9///vc1f/58ffzSo3rh1kviutp31xmztWjRIvX09MTtmsNxySWXSPpy6xo7OO+88yRJDz300LD/XbW2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PfbY5jE1NTWaNWuWJOn5559XJBLRww8/rPfee0+NjY3Kz8/XCy+8MKTrJ5Nk+e8CgD1Q4AOADTU0NKixsdF0DAAAAGAbgUBAlZWVSVtS1tfXa9KkScN+3MKFC3XzzTcrFApJktLS0uRwOLRu3TqtW7dOzz//vG6++Wbtt99+gz7HSy+9pLlz5yoUCikrK0vhcFjV1dV67LHH9O677+pvf/ubMjIytnncc889pxtvvFGRSESSlJWVpdraWj300EN64403dPzxx283+8qVK3XZZZepublZ0pa9/30+n+rr67V48WK98sor+tnPfrbdNwOWL1+uefPmqaurSz6fT243lRMARIPTdAAAwNba29u1YcMG0zEAAACAbYTDYX3xxRcKh8Omo0TVrrvuKofDIUm6/fbbtX79+mE9fsmSJbrxxhslSeecc45eeOEFLVu2TG+//baefvppHXHEEers7NSVV16purq6AZ9j06ZNuv7663Xsscdq0aJFWrJkid566y1dccUVcrvdWrt2rR555JFtHldRUaF58+YpEonoW9/6lp5++mktWbJES5cu1Y033qjm5mY98MADg2avqanRxRdfrObmZh1++OGaP3++li1bpjfffFOLFy/WnDlz5HQ6ddddd2nJkiWDPs+8efM0efJkPfLII3r77be1dOlS3XnnncP6e0wWfT9LABANFPgAYCM9PT1as2aN6RiArTEhAgDADMuytHbtWvX09JiOEnWlpaU67rjjJElffPGFTjzxRJ1xxhm6+eab9dxzz+mLL74Y9I6DYDCoW265RZJ09dVX66KLLlJJSYkcDoccDocmTpyo3/3udzr44IPV2dmpRx99dMDnCQQCOvLII/WrX/2qf6sdn8+nk08+WaeccookDXiA7t13361wOKzx48frT3/6kyZOnChpy779M2fO1Lx589Te3j7o9/6nP/1J7e3tOvroo3XzzTdr2rRp/avnc3NzdcEFF+iSSy6RJN13332DPk92drbuvvtu7brrrv0fmzBhwqBfDwAYGgp8ALCJcDisNWvWJN1qJiDaKPABADCjurpamzdvNh0jZq688krNmTNH6enpsixLn376qZ566indcMMNOvXUUzVz5kz94Q9/6N9mps+yZcvU0NCgvLy8/v3gB3LMMcdIkt55551Bv+a8884b8ON9+/Jv2LBhqwN029vb9e6770qSzjrrrAH35t9///215557Dvi8bW1teuONNyRpm4N7B8r+2WefbfP99zn55JMH3N4nFTFeBRBNbEgGADZgWZbWrVun7u5u01EAAACAbbS0tKi+vt50jJhyu9264IILdOaZZ+qtt97Shx9+qFWrVqmyslLBYFAtLS167LHH9OKLL+r222/X7rvvLkn65JNPJEmbN2/W97///UGfPxgMSpJqa2sH/Hx2drbKy8sH/FxBQUH/nzdv3txf1FdUVPTve7/PPvsMeu199tlHy5cv3+bjK1as6H/8hRdeOOjjv6q2tlZ5eXnbfHyvvfYa0uMBAMNDgQ8ANlBbW6vW1lbTMYCEwIomAADiq6urS+vWrTMdI26ysrJ09NFH6+ijj5a0ZZvLjz/+WI8//riWLl2q1tZWXXnllXrmmWfk9XrV2NgoaUtBP9jq9K8abAui7a1ed7lc/X/uOyRX2vLGSp/CwsJBHz/Y5/qySxpSdklb3QHwVbm5uUN6fCpgvAogmijwAcCw1tbWQVfhANgWEyIAAOInGAxud//3VOD1evWd73xH3/nOdzR37lwtWrRI9fX1eueddzRjxoz+LTC/+93v6o477jCcdnj6snu9Xi1btmxUz+V0sktzH8arAKKJ364AYFBPT48qKytNxwASSt+hagAAILYsy+rfPgZbHH/88f1/7rsrIT8/X9KWw2/j7aur3hsaGgb9uq+utP+qvuw9PT3asGFDdMOlMMarAKKJAh8ADOmbEPXtOQlgaL56CzkAAIidhoYGtbe3m45hK1/d5iYtLU3Sl3u/NzQ06OOPP45rnmnTpvWvfP/ggw8G/br3339/wI/vueee/avFX3755egHTFGMVwFEEwU+ABhSV1enzs5O0zGAhMOECACA2Ovu7lZ1dbXpGHFTXV2t9evX7/DrFi1a1P/nadOmSZIOOuig/pXst91226B7xPdpa2sbRdKt+f1+7bfffpKk+fPnD7i//nvvvTfgAbbSlhX8hxxyiCTp73//+w7/DqKZPZkxXgUQTRT4AGBAV1cX+94DI8QtyQAAxFbfnaKptO/92rVrddJJJ+l//ud/tGjRItXU1PR/LhQKqaKiQtddd50effRRSdJuu+2mvffeW9KW/eOvuuoqORwOVVRU6Nxzz9U777yz1dZD1dXVWrBggc466yw99dRTUc1+wQUXyOVyad26dbr0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AIDhS8bFJoBdsNUjALtgD3wAGESqTIiKi4v1y1/+Ut/+9rclSa+++iqH9yLmCgoK5HQyDAEAYDR6e3tNRwCSks/nS/i7kgEkD1bgA8AgUqXA77Pffvvp3//+t4LBoKqqqjRt2rStPt/T06OFCxfq9ddf15o1a9TZ2ans7GztvvvuOuGEE/Td7353u8+/cuVKLViwQB999JGamprkcrlUWFio3XffXTNnztR+++034OOWLFmiF154Qf/5z3/U2tqq9PR0TZ06VTNnztRxxx034J79c+fO1aJFi3Tsscf2H2Tb0tKio446SuFwWL///e91yCGHDJr1nnvu0YMPPqiysjI9++yz23y+oqJCTzzxhD788EM1NTXJ6XSqrKxMBx10kE4//XTl5ORs85i+cwi++c1v6r777tNrr72mZ555Rp999plaW1s1Z84c/fSnP93u32GycDgc7CcKAMAohcNhhUIh0zGApFRYWMhWjwBsgwIfAAaRagX+V7cLikQiW32uqqpKl156qaqqqiRtKWAzMzPV3NysN998U2+++aZOPPFEXXXVVds8bzgc1h//+Ec9/vjj/R9LT09XOBxWZWWlKisr9cYbb2jJkiVbPa6rq0u//OUvtXTp0v6PZWZmqqOjQx999JE++ugjvfjii7r99tuHtDomNzdX+++/v95++229+OKLgxb4lmXppZdekiQdffTR23z+3nvv1QMPPND/9+Xz+RQKhfT555/r888/1/PPP6/bb799mzdAvuqPf/yjHn30UTkcDvn9/pRbiZ6XlyePx2M6BgAACS3VxqpAvLjdbrZ6BGArFPgAMIhUmxS9++67kraU86Wlpf0fb29v10UXXaSamhrtu++++slPfqLddttNaWlp6ujo0HPPPad7771XTz/9tCZMmKDTTjttq+e96667+sv7WbNm6eyzz9aECRMkbVkVv3z58m325Zeka6+9VkuXLlV5ebl++tOf6qCDDlJmZqZ6enr07rvv6g9/+IOWL1+u66+/XrfddtuQvsdjjjlGb7/9tpYuXar29nb5/f5tvuaTTz5RdXW1pG0L/Mcee0z333+/MjMz9eMf/1jHHnus8vPzFQ6H9dlnn+mOO+7Q+++/r8suu0xPPfXUgIdeVVRU6MMPP9RZZ52lH/3oRxo7dqx6e3vV3Nw8pO8h0TkcDpWUlJiOAQBAwku1sSoQ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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig with a global cache.\n", + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow, FancyArrowPatch\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(4.5, 3.5, box_bg))\n", + "\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrowPatch( (3.4,4.4), (3.4,4.5), connectionstyle=\"arc3,rad=10.5\" ),\n", + " FancyArrowPatch( (3.3,4.2), (3.3,4.3), connectionstyle=\"arc3,rad=10.5\" ),\n", + " FancyArrowPatch( (3.2,4.0), (3.2,4.1), connectionstyle=\"arc3,rad=10.5\" ),\n", + " \n", + " FancyArrowPatch( (6.2,4.4), (6.2,4.5), connectionstyle=\"arc3,rad=-10.5\" ),\n", + " FancyArrowPatch( (6.3,4.2), (6.3,4.3), connectionstyle=\"arc3,rad=-10.5\" ),\n", + " FancyArrowPatch( (6.2,4.0), (6.2,4.1), connectionstyle=\"arc3,rad=-10.5\" ), \n", + " \n", + " FancyArrowPatch( (6.4,3.3), (6.5,3.4), connectionstyle=\"arc3,rad=-16.5\" ),\n", + " FancyArrowPatch( (6.3,3.1), (6.4,3.2), connectionstyle=\"arc3,rad=-16.5\" ),\n", + " FancyArrowPatch( (6.2,2.9), (6.3,3.0), connectionstyle=\"arc3,rad=-16.5\" ), \n", + " \n", + " FancyArrowPatch( (3.3,3.3), (3.2,3.4), connectionstyle=\"arc3,rad=12.5\" ),\n", + " FancyArrowPatch( (3.3,3.1), (3.2,3.2), connectionstyle=\"arc3,rad=12.5\" ),\n", + " FancyArrowPatch( (3.3,2.9), (3.2,3.0), connectionstyle=\"arc3,rad=12.5\" ), \n", + " \n", + " FancyArrowPatch( (3.1,1.6), (2.9,1.65), connectionstyle=\"arc3,rad=28\" ),\n", + " FancyArrowPatch( (3.3,1.5), (3.1,1.55), connectionstyle=\"arc3,rad=28\" ),\n", + " FancyArrowPatch( (3.5,1.4), (3.3,1.45), connectionstyle=\"arc3,rad=28\" ), \n", + " \n", + " FancyArrowPatch( (5.9,1.6), (6.1,1.65), connectionstyle=\"arc3,rad=-28\" ),\n", + " FancyArrowPatch( (6.1,1.5), (6.3,1.55), connectionstyle=\"arc3,rad=-28\" ),\n", + " FancyArrowPatch( (6.3,1.4), (6.5,1.45), connectionstyle=\"arc3,rad=-28\" ), \n", + " \n", + " \n", + " \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(4.24,4.3, 'Duplicate', fontsize=18)\n", + " plt.text(4.24,4.1, 'Suppression', fontsize=18)\n", + " plt.text(2.2,0.75, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'With a Global Cache',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "9ecbc4a5", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Global Duplication Suppression\n", + "---------------------------------------------\n", + "\n", + "* create a service for all the processes participating in a flow to contact.\n", + "* ask the service if a partipant has already seen a message with this checksum.\n", + "* if the answer is yet, discard the message.\n", + "* totally global is the worst case.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "0a866858", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Distributed Duplicate Suppression\n", + "-------------------------------------------------\n", + "\n", + "* hash the checksum to get a modulo N\n", + "* publish the message to an array of destinations modulo the checksum\n", + "* a single subscriber on each destination will get 1/N messages.\n", + "* Every occurrence of the same message will be sent to the same subscriber.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "501d4f4d", + "metadata": { + "slideshow": { + "slide_type": "slide" + }, + "tags": [ + "hide-input" + ] + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3xb1f3/8be89x7Ze4cMICHMEkbKDGGn7LChhZaWXaCMFiiUAm0pkLL3JmWkbBK+BEggA2iAEJLYceLE25a3LUv390dq/2JLtiVZ0r2SXs/HQ4/E17r3fCRLuud+dM7n2AzDMAQAAAAAAAAAACwlxuwAAAAAAAAAAACAOxL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AAAAAAAAAAAWRAIfAAAAAAAAAAALIoEPAAAAAAAAAIAFkcAHAAAAAAAAAMCCSOADAAAAAAAAAGBBJPABAAAAAAAAALAgEvgAAAAAAAAAAFgQCXwAAAAAAAAAACyIBD4AAAAAAAAAABZEAh8AAAAAAAAAAAsigQ8AAAAAAAAAgAWRwAcAAAAAAAAAwIJI4AMAAAAAAAAAYEEk8AEAAAAAAAAAsCAS+AAAAAAAAAAAWBAJfAAAAAAAAAAALIgEPgAAAAAAAAAAFkQCHwAAAAAAAAAACyKBDwAAAAAAAACABZHABwAAAAAAAADAgkjgAwAAAAAAAABgQSTwAQAAAAAAAACwIBL4AAAAAAAAAABYEAl8AAAAAAAAAAAsiAQ+AES5uXPnymazdd3mzp1rdkhhZfny5d2eP5vNpuXLl/e73y233OK2H6LXk08+6fZ6KC4uNjssmGTUqFHdXguLFi0K6n4AAPMUFxe79QGefPJJs8MCAFhInNkBAAAAAADM19bWpm+//VY//fST7Ha76urqJEmpqanKyMjQ8OHDNWrUKI0cOVJxcVxKAgAAhAK9LgAIgeLiYo0ePdqr+8bGxiopKUlJSUnKzc1VQUGBRo0apYkTJ2rGjBk64IADlJOTE+SIAUSDRYsW6amnnur3fjabTQkJCUpMTFR6eroKCgo0ePBgTZgwQZMmTdK+++6radOmKSaGyZ1AuGlsbNSLL76oJ598Ul9++aUcDke/+yQlJWnmzJmaPXu2DjnkEB1++OFKT08PQbQAAADRhwQ+AFiM0+lUU1OTmpqaVF1drY0bN2rFihVdv7fZbNpzzz11yimn6KyzztLQoUNNjDb45s6dq08++aTr54MPPtirEjWwtp4lg26++Wbdcsst5gSDfhmGoba2NrW1tam+vl6lpaVat26d/vOf/3TdJzMzU/Pnz9cvfvELHXXUUSTzYTmjRo3S1q1bu34+55xzor5MxeOPP66rrrpKtbW1Pu3X2tqqlStXauXKlfrHP/6h+Ph43X777br66quDFCkAAED04soKAMKMYRhau3atrr/+eo0ePVpnn322SkpKzA4LQJSz2+169tlndeyxx2rixIl66qmnZBiG2WEB8KCtrU0LFizQ+eef73Py3hOHw6HS0tIARAYAAICeGIEPACZJTU3VuHHjPP6uoaFBdrtddrtdHR0dvR7D4XDomWee0auvvqo77rhDV1xxRZCiBRAtxo4dq7S0NLftDodDdXV1stvtampq6vMYmzZt0qJFi7R48WI9//zzGjVqVJCiBeArp9OpE088sdsMmt1NmjRJBx10kKZMmaLc3FwlJyervr5eNTU12rBhg1avXq3vvvuuz/4JAAAAAocEPgCYZNasWV6VgikvL9eXX36pL7/8Uh988IFWrVrldp+Wlhb99re/1Zdffqmnn37ap4XlKEczMHPnzmWUMSLKo48+qrlz5/Z5H4fDofXr12vVqlVatWqVlixZIrvd7na/L774QnvttZfefPNNHXjggUGKGJ2Ki4vNDgFh4B//+IfH5P2hhx6qu+66S7Nmzer3GHa7XW+99ZZef/11LV26VO3t7cEIFYgKo0aNoi8JAOgTJXQAwOIKCws1f/58/fGPf9TKlSu1evVqnXnmmW41xCXphRde0FlnnWVClACiSXx8vPbcc09dcskleuKJJ7R9+3b985//9DjSvra2VkcddZRWr14d+kABdGO323XzzTe7bb/sssv04YcfepW8l3ateXHmmWfq9ddfV0lJiW699VYNGTIk0OECAABAJPABIOzsvffeeuaZZ/Tmm28qNzfX7fcvvvii7r33XhMiAxCt0tLS9Mtf/lLffPONTj31VLffNzY26uSTTw5IrW0A/nv77bdVX1/fbdtee+2l+++/3+PAAG8UFhbqD3/4g6655ppAhAgAAIAeSOADQJg69thjtXr1auXn57v97oYbbqCUAoCQy8jI0EsvvaSrr77a7Xdbt27V73//exOiAtDpnXfecdt26aWXKjY21oRoAAAA4A1q4ANAGBs1apReeuklzZs3T06ns2t7a2ur/vCHP+jpp58OWSwOh0M//PCD1q9fr5qaGtXX18tmsyk5OVlZWVkaMWKExowZExaLWRqGoe+++07r16/Xjh071NzcrKSkJI0bN07HH398SGOprq7WqlWrtHnzZtXX1yszM1NDhgzRjBkzNHbs2JDGEq5aWlr05ZdfaufOnaqoqFBjY6Nyc3OVn58ftOfR6XRq5cqVKioq0o4dOyRJubm5mjp1qmbNmuXTOhXh6M4779S6dev04Ycfdtv+yCOP6He/+53Gjx9vUmSB9f333+vrr7/Wzp071d7eroKCAg0bNkwHHHCAx4WAw1Hn5+FPP/2k6upqVVVVyWazKSMjQ8OHD9fkyZM1ZswYv0dvS7veL1u2bNGGDRtUWlqq+vp6OZ1OZWdnKzs7W5MmTdK0adMUExM+Y48cDodWr16tbdu2qbKyUna7XVlZWcrPz9eUKVM0depUU+LaunWr27a9997bhEisraGhQStXrtRPP/2kuro6paSkaMiQIZo6dWpI/nY7duzQV199paKiIjU1NSknJ0dDhgzR7NmzQ1KqqKSkROvWrdPWrVvV0NCg2NhYFRYWauHChUpJSfHqGKE+927btk3ffPNN12dIW1ubkpOTlZqaqiFDhmjUqFGaMGGCkpKSBtROeXm5vvnmG23dulX19fVqaWlRUlKSUlJSNGjQoK52zD4HOJ1OrV27VkVFRV2fQTk5OcrPz9eECRM0bdq0kMRRWVmplStXasuWLWpsbFRmZqYKCgo0Z84cjRw5MiQxAEBEMAAAQVdUVGRI6nY7+OCDA3b8yy+/3O34CQkJxs6dO/vd9+CDDx5QXB9//LGxcOFCIzk52S0GT7e8vDzj2GOPNf71r38ZlZWVHo/pzXH6uxUVFbkd19Pf4Yknnuj6fVVVlXH99dcbgwYN8njMkSNHuh1z2bJlbvdbtmxZv8/bzTff7Lbf7pYvX278/Oc/N2JjY3t9jHvttZfxyCOPGC6Xq9/2eup5rJtvvtnnYxiGYZxzzjn9PkeG4fl58vXW27E9cTqdxtNPP23MmzfPSEpK6vO4o0ePNq699tpeX4++qKysNC6//HIjPz+/1/ays7ON3/72t0ZFRUXXfk888YRXr+FA6/n38/b1640dO3YYCQkJbse//PLL+913oJ9Lnfx9XkeOHNltn3POOafrd21tbca9995rjB49ute/cXJysnHSSScZ33zzjV9x99V+MPbz5JNPPjF+8YtfGAUFBf2+NwsLC40zzjjDWLp0qeF0Or06/saNG40///nPxs9//nMjNTW13zYyMzONk08+2Vi5cqXXj8HTZ74/N1+88cYbxnHHHWekp6f3eczBgwcbl156qbF161afjj9QEyZMcIvlxx9/DFn7/p4zPfHn9d7fZ8K6deuMk046yeNnV+dt0qRJxl/+8hejvb3d55j7O2cuWbLE2G+//XptOyYmxjjooIOM1157zee2DaPv56y9vd148MEHjWnTpvXafn+fn6E+9+7YscO4/vrrjVGjRnn1Xk5ISDD22Wcf4/e//72xbt06r9upra01br/9dmPKlCletRMbG2vMmDHDuOKKK4wVK1b020/rr3/qi6+++sr4xS9+YeTk5PQZ45AhQ4wLL7zQ2LRpk1/t9HeeXrZsmTFv3jwjJiam1ximTJliPPvss371YwEg2pDAB4AQCHYCf8uWLR4TvX/729/63dffRFltba1xwgknDCgpsnDhQo/HHsgx+7rI7OsC6a233ur3YicUCfyOjg7jV7/6lU+P9cADDzS2bNni1d+tt+c4khL477zzjtcX2bvf0tPTjfvvv9+v58EwDOOFF14wcnNzvW4vNzfXeOeddwzDiMwEvmEYxrnnnut2/Ly8vH6TvFZN4G/atMmYOnWq13/juLg44/rrr/c6qd1f+8Hab3dff/21ccghh/j9Pp0/f36fx6+qqjL23HPPAX0WHHfccUZtbW2/jyWUCfxVq1YZ++67r8/HTkhI8Os14q/p06e7xfD++++HpG3DsHYC/09/+pMRFxfn9d9ujz32MNauXetTzL2dM5uamowTTzzRp9fOggULfE5+9/acbdy40dhjjz36bbOvz89Qn3sfeOABIy0tbUDv7ZaWln7befnll/v8Ut6b2w8//NBnG4FI4FdWVhoLFy40bDabT7HFx8cbv/71r43W1laf2uvtPN3a2mpccMEFPsXw85//3GhsbPSpfQCINuEzDxUA0KvRo0dr/vz5btvffvvtoLRXW1urgw8+WEuWLAnK8UPtpZde0oIFC1RTU2NqHIZh6Mwzz9Q///lPn/ZbsWKFDj74YG3ZsiVIkYWPe+65R8ccc4y+//57n/dtaGjQFVdcoQsuuEAdHR0+7fuvf/1Lp59+uqqrq73ep7q6WvPnzw/a+9QKfvOb37htq6qq0sqVK02IZmCKiop0wAEH6LvvvvN6n46ODt15551atGiRXC5XEKMLjJdeekn777+/li1b5vcxei6Q2lNDQ4PWrVvn9/El6c0339Q+++yj7du3D+g4gfL888/rZz/7mV+v6/b2dt1555067rjj1NjYGITouhs0aJDbtpdffjno7VrdtddeqxtvvNGnz/7169frkEMO0erVqwfUdltbm4466ii9/vrrPu33xhtv6LDDDvPpvOPJhg0btN9++2n9+vV+HyPU596bbrpJl112WdDfM48++qgWLlyoysrKoLYzUFu2bNH++++vl156SYZh+LSvw+HQ3//+d82bN2/AC823trbqqKOO0qOPPurTfu+//76OPvrobuVAAQDdRXYBVgCIIocddpj+/e9/d9u2cuVKuVyugNcN/t3vfqdvv/3WbfuECRN0+OGHa9KkScrNzVViYqIaGxtVV1enjRs3av369Vq1apXa2tr6PP6MGTO6/r9p0yY1NTV1/Zyamqpx48b1G2NCQoJXj2X9+vV64IEHupJrsbGxOvDAA3XYYYdp2LBhSk5OVmlpqdatWzfgpFN//vrXv+rFF1/s+jk9PV0LFizQ7NmzVVhYqLq6Om3YsEGvvfaatm3b1m3fbdu26dBDD9XXX3+trKysoMbpj7S0tG5/12+++abb7wsLCz0mlnbXX93f6667TnfddZfb9pycHM2bN0977723CgoKlJKSorq6On333Xd699139eOPP3a7/2OPPaasrCzdc889/T0sSdJrr72mSy65xO2iOTY2VocccojmzZunoUOHyuFwaNu2bXr77bf15ZdfStqV4D3ttNN05ZVXetVWuJkxY4by8vJUVVXVbfvnn3+u/fff36SofOdwOHTiiSeqvLy8a9uECRN04oknauzYsUpLS1NZWZk+/fRT/ec//1Fra2u3/Z955hllZ2frb3/7W6hD99rixYt1ySWXePzd0KFD9fOf/1zTpk1Tfn6+4uPjVVtbqy1btmj16tX64osv3B6zt9LS0jR79mxNnjxZ48ePV2ZmptLT09Xe3q7a2lp9//33WrZsmX744Ydu+/30009auHChPvnkk17XlEhISOj2ufP999/L4XB0/Zydna0RI0b4FXenhx56SL/85S/dtqempmrevHmaPXu2Bg8erPT0dNntdv3000/64IMPtHbt2m73X7p0qRYtWqRXX311QPH0Z//999f777/fbdsTTzyhY445JuRrvFjFK6+8orvvvrvr56SkJB111FE66KCDNHjwYDU2Nmrz5s1asmSJ2/nCbrdr3rx5WrNmjcaMGeNX+1deeaX+7//+r+vnvLw8nXjiiZo+fXrX5+d///tfvf76626J5G+//VZHHHGEvvjiC8XHx/vcdnNzs4477rhuXwLsscceOuqoozR27FhlZ2eroqJCGzdu1CuvvOLxGKE+9y5fvly333672/bMzEzNmzdPM2fO1PDhw5Wamqq2tjY1NDSopKRE3333nVatWqWysjJvnhpt3LhRl112mdu5PTk5WYceeqhmz56tkSNHKi0tTR0dHaqvr9fOnTv13Xff6auvvlJxcbFX7QxURUWFDjzwQO3cudPtd8OGDdOJJ56oyZMnKycnRxUVFfrmm2+0ZMkSty9+Pv30Ux1++OH6/PPPlZiY6Fcs5513XrcvgCdOnKijjjpKkyZNUk5Ojux2u9atW6fXXnut2/lUkv7v//5P9913n6666iq/2gaAiGfuBAAAiA7BLqFjGIaxevVqj9NS+6tt62upipKSErfpufn5+cabb77pVZyNjY3GkiVLjGOOOcY4/fTT+71/oEppGIbnv8PupYcOPvhg47vvvut1f09TrQNZQmf3erHnnnturyUinE6ncc8993isL7to0SKvnoue+wW7hE6w2u/0+uuvux0zOzvbWLx4cZ9T5F0ul/H66697rPP9xhtv9NtuZWWlx6n1++yzT5+vpU8++cQYO3Zs1/09rSERCSV0DMMwjj32WLc2TjvttD73sVoJnd3fa1lZWcaTTz7Z676lpaUeH7PNZvP6uQ11CZ3PP//cY83vMWPGGC+++GK/9Ynr6+uN559/3jjggAOMuXPn9nnfoqIiIysry7jsssuM5cuXe11H/LPPPjNmzZrlFuNf/vIXr/Y3jMCuEWAYu8rm9HzekpOTjTvvvNOw2+197rt8+fJunwGdt7///e8Diqk/3377rccSGzabzTj77LN9qgvuDyuW0Nn9/X3MMccYpaWlve7/1FNPGVlZWW7HOOSQQ7yq493zMzcxMbHr7xETE2Nce+21vZ6zWlpajOuuu85jTfFbbrml37YNw/05270PNGrUKOPtt9/udV+Hw2E4HI5u28w49x522GFu+1x55ZVGfX19v4/f5XIZq1evNq6++mojJyenzxjPP/98t3bOPPPMbuvX9OW7774zbr31VmPo0KFBK6HjcrmMo446ym3f5ORk49577zU6Ojo87tfc3GxcffXVHl9LV1xxhVePr+d5evf30aBBg4xXXnml130bGhqMs88+263trKwso7m52av2ASDakMAHgBAIRQLf4XAY8fHxbu101tjuja+JsgceeMCtjeXLl/sVc1NTU7/3CXYCv/N24okn+rUgXSAT+J236667zqu233rrLY/1ej/55JN+9+25Tzgn8MvLy43MzMxuxxs/fryxbds2r49RUlJiDBs2rNsxpkyZ0m9CxlON94MOOsirC9CysjJj/Pjxvb4OIiWBf+utt7q1MWfOnD73sVoCv/OWnp5ufPnll/3u73Q6jdNPP91t//Hjx3tV6zyUCfy2tjZjxIgRHs9RNTU1XrW7u/6e37a2Nr8TNC0tLcaRRx7ZLc7hw4e7JRV7E8gEfmtrq9vCmQUFBca3337r9THq6urcatLn5uYGvRb0Kaec0uvnjiRj7NixxqWXXmo89dRTxvfffx/Q+vxWTOB33k4//XSvHuuXX37pcZHip59+ut99PX3mdt4eeughbx628dBDD7ntGx8f79VipL19tk2cOLHPLy48MePca7fb3dZ88nbgQk/Nzc19nuPz8vK6tePtlzQ9ORwOo62trc/7+JvAf+6559z2S0pKMj788EOvYlu8eLHb/jabzfjqq6/63bfnebrzNmbMGKO4uLjf/V0ul3HEEUe47f/MM894FTsARBtq4ANAhIiLi1NOTo7b9kDXCO5ZZ338+PE6+OCD/TpWSkpKIEIasJEjR+qpp57ya/p5oM2dO1d33nmnV/c99thjdeONN7pt//vf/x7osCztb3/7m+x2e9fPKSkpevfddzVs2DCvjzF8+PBu5YukXeU23nzzzV73qaqq0vPPP99tW3Z2tl577TUlJyf322ZhYaGWLFnSa/mPSFFQUOC2zSq1y331j3/8Q7Nnz+73fjExMXriiSc0YcKEbtt/+ukn/ec//wlWeH558sknVVJS0m3bhAkTtHTpUmVnZ/t8vFGjRvX5+4SEBK/eH54kJSXpqaee6nbu2LZtm1tJmFB4+umnu5XIiImJ0ZIlSzRt2jSvj5GZmaklS5Z0K/lWXV3tc/1oXz300EN9lqLbvHmzHnroIZ1zzjmaMmWKMjMz9bOf/UzXXHON3nrrrQHXybaiCRMm6IknnvCq5ODs2bP1j3/8w237QEpkLVq0qNcSVj1dcsklWrRoUbdtDodDDz30kF9tx8XF6YUXXui3TF1PZpx7t27d6lYn/cILL/Qh6v8vOTlZNpvN4+8aGhrcSr9dcMEFvd6/L3FxcV6XdfTVvffe67bt7rvv1mGHHebV/hdddJHb684wDI/H9UZ8fLxefvlljRw5st/72mw2j+289957frUNAJGOBD4ARBBPtc/7W1DQVw0NDd1+zs3NDejxzXDbbbcpLS3N7DAk+Z58v/baa90ult944w2PtVAjUWNjox588MFu26688kq/ahEfcMABbhe9fS3U/MQTT7it53DLLbcoPz/f6zanTp2qSy+91LdAw0woPpdCYdasWTr77LO9vn9CQoL++te/um1/+OGHAxnWgO1e+7vTU089pdTUVBOi6V9BQYGOPPLIbttWrFgR0hgMw9Bf/vKXbtvOOOMMv9Z1GDNmjM4666xu24K9QHxubq6WLVum/fbbz6v7NzY26tNPP9Vf/vIXHXfccSooKNCxxx6rV155JWIWnfzrX//qU5L17LPP1qxZs7ptW7NmjV8L2iYnJ3usId+Xu+66y+2LME/nJG+cddZZ2nPPPX3ax6xzb88+qBScfmio2hmIlStXas2aNd22TZs2Tb/61a98Os6dd97p9mXtq6++6laj3hunn3669t57b6/vP2XKFO21117dtvV8TACAXUjgA0AE8TRasqWlJaBt9LyA+e9//9ttBFa4ycjI0CmnnGJ2GJKkfffd16fRm9KuEak9kz8dHR368MMPAxmaZX344Yeqq6vrtu3888/3+3jHHHNMt58/+eSTXu/7zjvvdPs5MTHRpwRvp4svvtjnfcJJKD6XQsGf0ZdHH3202xdsH330UbeFVM303XffafPmzd22HXTQQdp3331Nisg748eP7/bzypUrQ9r+N998o59++qnbtgsuuMDv4/X83PFmsfeBGjZsmD755BP9/e9/1+DBg33at6OjQ0uXLtWpp56qGTNmhP2I2SFDhujoo4/2aR+bzeZx5HfP84I3FixY4HGmUl8KCgrcFh2uqanpWiDdF/6cM80693pKogfjC7ycnBy3z/tQf1HYnw8++MBt28UXX+zVLJLdZWVl6bTTTuu2zeFwaPny5T7H5M9siH322afbzxs3bvT5GAAQDUjgA0AEcblcbtv8me7blzlz5nT7uampSb/4xS9UU1MT0HZCZc6cOX6Xcwi0nhfj3jrxxBPdtoU6oWWWnhf5Q4cO9Wrqdm9Gjx7d7efi4mK3JIW0673Wc6TlIYcc4nG0eX+mTp3qVmolkoTicykU/Hl/xsTEaMGCBd22tba26uuvvw5MUAPkKUHT8wvBUCgtLdUrr7yim266SQsXLtSRRx6p/fffX3vuuadmzpzpdnvyySe77d+zBFCw9fzciY2NdUtC+aLn505ra6t++OEHv4/nrfj4eF1++eXasmWLXnzxRc2fP9/n8+F3332nI488UjfccIPH93o4OO6443xOekqBO/eaee5PTk726ws7s86948ePdysXee211+rTTz/1u21PkpKSNH369G7b7rnnHr322msBbWcgPvvsM7dtJ510kl/HOvXUU706fl+Sk5P9+hwcO3Zst5+dTqcaGxt9Pg4ARLrILroKAFHG08VOoJPTRxxxhAYPHtytRMu7776rMWPG6IwzztDJJ5+sAw880BL15L3Rc+qumXyZdry7adOmKT4+vtuo3miZgtzzArO2tlYzZ870+3ieLhqrqqrcEvM//vij2xR7f/9+nftG6qizUHwuBdvQoUNVWFjo176ePmPWrFnjVS39YPviiy/cth1wwAEha//VV1/Vgw8+qE8++WRAyV9Pr7Fg6vm5Y7PZBjRrob293W1bz/rbwZSUlKSFCxdq4cKFam9v1+rVq7VixQqtXr1a69at0+bNm2UYRp/HuOOOO9Te3u5WWigc+PvZnZeXp+HDh2vbtm1d2/w59/rbfm+fLb6YPn26YmNjfW7brHNvTEyMzj77bN1///1d26qrq/Wzn/1M8+bN05lnnqmjjz5aeXl5fsfS6dxzz9UVV1zR9XNra6tOPvlk7bvvvjrnnHN07LHH+lTvP9DWrl3b7edhw4Zp0KBBfh1r7733VkxMTLfP4Z7H78/IkSP96vtnZma6bbPb7ZYpbQkAVkECHwAiiKeF5TIyMgLaRnJysh544AGdfPLJ3S7o7Xa7HnzwQT344INKSUnRfvvtpzlz5mjOnDk66KCD/FoMMRR8nbYeTBMnTvRrv8TERI0aNapbSYeKiopAhWVpPRdDbW5u1jfffBPQNqqrq90WfPT0/Pr795OkSZMm+b2v1YXicynYAv23tcr7s2eN49jYWE2ePDno7e7YsUNnnXWWPv7444AcL9Rl3Hp+7nR0dATlc8cMCQkJ2n///bvV86+vr9dnn32mjz76SK+88kqvMx7uuece/exnP9P8+fNDFW5ADPT9vXsCv7KyUoZheD3LKC4uzm0EsrfGjBmjhISEbl8A+frZ4m8fyKxzryTdeOONevPNN7Vly5Zu2z/44AN98MEHstlsmjp1qvbff3/Nnj1bBx10kF9/40suuUTPP/+8W1milStXauXKlbr00ks1btw4HXjggZo1a5YOPPBATZ8+PSQzzAzDcJv5OpDP7rS0NA0fPlxbt27t2ubrl4g9Z0Z4y1PS3ypl5gDASiihAwARor293WOibPjw4QFv68QTT9Szzz7b6yKHzc3N+uijj3THHXdowYIFysvL06xZs3TXXXd1u9C1AislEj2NQvJ331CPSDVLKEo3earX7un5DeTfL5J4WggvGJ9LwRTov61V3p893z9ZWVlBTz6VlpZq7ty5AUveS7sS6KEUiuS6ldaJyMjI0FFHHaV77rlHRUVFevHFF3t9D99www39jta3mkC+v10ul8cFUHuTnp4+oPdczz6Mr58t/vaBzDr3Srvq4H/44Ye9LrxrGIbWr1+vf/3rX7rwwgs1adIkDR48WJdcckmf69r0lJiYqKVLl+rwww/v9T6bNm3Sk08+qcsuu0wzZ85UXl6ezjrrLP3nP/8Jakmp+vp6twWk/Snht7ueA218/RuHy8xbAAhXJPABIEKsW7fOYxJjzJgxQWnv9NNP148//qhf/vKX/V4AulwurVmzRtddd53Gjh2rCy64QJWVlUGJy1dxcdaZjNbbFyL+7OtLAiFcNTc3B32hx954en4D+feLJJ4WVQzW51KwBPpva5X3Z319fbefQ1GyYNGiRW4LwErSzJkzdf3112vJkiVau3atysrKVF9fr/b2dhmG0e128803Bz3Ovnj6sjxaxMTEaOHChfrmm288lnD573//q1WrVpkQmf/MfH8P9LN/oOd+f/pAZp57O40ePVpffvmlFi9e7NXo+rKyMi1evFhz587V7NmztWzZMq/aycvL0/vvv6+XXnrJq1JHNTU1evbZZ3XMMcdoypQpQauZH+g+iKf9rXKeAgDsQgIfACKEp1rGmZmZfk/N9sbQoUP1z3/+U2VlZXr99dd12WWXacaMGX3WU3U4HHrsscc0ffp0n+trRrqmpqaA7Zuenj7QcCwvKSnJbeHB448/3i3ZN9Db3Llz3dr29PwG8u8XKQzD8JjMs9LaE94I9N/WKu/Pnl++BnvhwKVLl+rDDz/stq2goEDvvvuu1q1bpzvuuEPHH3+89txzTxUWFio9Pd3jqE6zR6f3XMNh5syZAf/cWbRokTkPzkvZ2dl67bXXlJCQ4Pa7jz76yISI/Gfm+3ugn/1mnPvNPPfuLi4uThdddJE2bNig1atX609/+pN+/vOf9zuoZPXq1TrssMN0++23e/V4bTabTj31VK1evVrff/+97rnnHs2fP1+5ubl97vfjjz/q5JNP1qWXXhrwWSmB7oN42t8q5ykAwC4k8AEgQvRMikjSfvvtF5JanMnJyTrhhBP0j3/8Q19//bXq6ur0wQcf6Oabb9acOXPcLvSkXaOhjjnmGNPq/FrRQOo499x3oFOpvWVmndKYmBi3x1lUVBSStj09v4H8+0WKNWvWeBytHKqFUgP1+gz03zZU78/+9ExA1dXVBbX8yQsvvNDt59jYWL311ls64ogjfDpOKMp39KXnApmh+tyxmlGjRum4445z2/7jjz+GpH0rvr9jYmJ8Snw2NDQM6D3XcxZNKD5bzDz39mbvvffWDTfcoPfee0+1tbX69ttv9c9//lMnn3yyx4S+YRi68cYb9dxzz/nUzuTJk3XllVfqzTffVFVVlTZu3KjHHntMZ511Vq8L5z788MO64447/HpcvcnIyHAbLDPQ0mw99/e3pj0AIDhI4ANABPjpp5/0zjvvuG03ayG5tLQ0HX744brlllu0cuVKbd26Vddff72SkpK63a+srEx33323KTFa0caNG/3ar729XcXFxd229bcwXc9p8/4mQsz+AqawsLDbzxs3bgzJ1H5Pz+9AklYbNmwYSDiW9be//c1tW2FhofbZZ58+9+s56trs16e/703J8+vCKotnDxo0qNvPTqdT33//fdDa++CDD7r9fOSRR/b7WvCk5+KVodbzc8dut/e6sGuk8/T362vxy0AuWBmoL3IC+f7Oz8/3aeBER0eH36/noqKibgvYSqH7bDHr3OuNmJgYTZs2Tb/85S/1yiuvqKKiQs8//7wmTJjgdt9rr712QGtojB8/Xuedd56efvpplZWV6a233tLs2bPd7nfHHXf4vChsX2w2m9sXsD/88IPfx2tqanL7DOvtCwkAgDlI4ANABLjvvvvcFstKTEzUKaecYlJE3Q0bNkx33HGH3n//fbcRQ8GqDxqO1qxZ49d+3377rVsCpL9arT1HpPUcxeetTZs2+bVfoPRMHrW0tGj58uVBb3fixIlu9cL9/fsNdF+r2r59u1566SW37WeccUa/CS6rvT63b9+uiooKv/b19Lf1ppZyKOy3335u2z777LOgtNXe3u72HB500EE+H8fpdHpcVyGUPCWtPX2JHg08LQDbV111T6Oh/Xl/b9++Xa2trT7v54m/n79VVVVuSU9/3tv+tm/mZ4tZ515/JCYm6rTTTtOaNWvcFr4tLS3VypUrA9JObGysjj32WH3++ec66qijuv2uublZ7777bkDa6dSzFN327ds9LhrvjTVr1rhdR1jlPAUA2IUEPgCEuQ8++ECLFy92237GGWcoPz/fhIh6d9BBB7nNCti8ebOam5t73adnIsDpdAYlNiv497//7dd+r7/+utu2fffdt899ek5/92cEYGlpqTZv3uzzfpLcvsjx9+86b948t23PPvusX8fyRUxMjNsou2XLlvk1hf37778f0AhQK3I6nTr77LPdvliKi4vTZZdd1u/+PV+fxcXFfpWZ+OSTT3zepzf+vD9dLpfeeOONbtuSkpI0c+bMwAQ1QJ5qTD/zzDNBacvT6FN/SjT85z//8btWf6DOJ2Z97liRp4Rhz9HZu/NU4sWf808g39tvvvmmW/LSG/6cez0J5bk/UMLxPZCWluZxAexvv/02oO3ExcXpzjvvDHo7+++/v9u2V1991a9jvfLKK14dHwBgHhL4ABDGtmzZotNOO83twjMlJUW33nqrSVH1bdKkSW7b+qo/27OWbLAXWTTTF198oe+++86nfdra2twSbnFxcTr88MP73G/ixIndfl69erXPCYx//etfPt1/d4H6ux5xxBFupZleeOGFkNRg7jnCztPfwhsDeR6t6uqrr9ayZcvctv/yl7/U6NGj+92/5+uzsbHR59Iuy5YtC+gMkUcffdTnfd555x1t376927bDDjvMYxkRM0yePNntuV6xYoXHRdEHKjU11W2bPyUl7r33Xr9jCNTnzpw5c9yS1CtWrPC4Fk2k+/jjj922jR07ttf7Dx061G32kj8zKgL5uVlaWurXDIrHHnvMbVvP84I3/v3vf6uystKnfSorK92+HMzJyfGrJJU/zDz3DoSvfVArt+Np7ZB//etfPvfl7Ha7nn/++W7b4uPjdcghhwwoPgBAYJHAB4Aw1Vln01ON57vuukvDhg0zIar+7dy5s9vPNputzzqb2dnZ3X4uKioK6iKLZvvNb37j0/3vvvtutwThggULNHjw4D736zk1uqKiQh999JHX7ZaUlOjvf/+794H20PPv6m8N4Ly8PF100UXdtjmdTp1++ulqaWnxOz5vnHvuuUpMTOy27dZbb/Wp7vr333+vBx98MNChmcZut+uUU07Rfffd5/a7MWPG6LbbbvPqOJ6m7vdMMPTF4XDouuuu8/r+3vjqq698+oLG4XDoqquuctt+ySWXBDKsAbv22mvdti1atEhNTU0BbSczM1MpKSndtr3//vs+HePRRx8dUJmOQH3uxMXFeXzezj//fJ8TsaH24osv+l1mo6fPPvvM40j4Y489ttd9YmJi3GagLF261Kfk5ptvvqn/+7//8/r+3rjqqqt8qsX/zDPPuH3xsNdee2nWrFk+t93S0uLz59V1113nNnvR0zkpWMw89w5Ezz6opKDMVg1FO/vss4/b6+3bb7/Vww8/7NNxbrjhBrf1JE499VTLrNUCANiFBD4AhJnVq1frrLPO0nHHHedxAbdzzjnHqxIV/rrlllu0atUqv/bdtm2blixZ0m3b5MmT+xyNOm3atG4/2+12ff755361Hw4++ugj3XjjjV7d95133tEf//hHt+2//vWv+93X0yjB6667zqsERm1trU4++WS/ysV06vl3/eSTT/xOGF5//fVuo3vXrl2rE044QbW1tX4dc+vWrbr88su1fv36Xu+Tl5en008/vdu26upqnXzyyV7VZq6oqNBJJ53k9wKOVtLY2KgHH3xQM2fO9DiFPzMzU6+99prHetme7L///m73/fvf/66tW7f2u6/L5dJll10WlDrpl112mdauXdvv/QzD0Pnnn++2OPG4ceN09NFHBzyugTjzzDM1ZsyYbts2btyoY445xq/3eM8FtXd34IEHdvt5+fLl+s9//uPVcd99912vPtv60vNzZ/369dq2bZtfx7r00kvdvigvKSnRUUcd5falqrcqKyt14403ui32G0hPPvmkRo8erd/+9rd+P3Zp14KZp512mtsX6nvssYf22GOPPvftef5paWnx+rz37bff6txzz/UtWC9s2LBB559/vlcDBNauXeuxn+XrF/C7e/zxx/XII494dd9HHnlEjz/+eLdt8fHxuvTSS/1u3x9mnHvffPNNPfHEE34vmOtp4MGMGTPctn3++ef629/+poaGBr/a8bSAu6d2Bup3v/ud27arrrrK6xJTjz/+uNsgApvNpt/+9rcBiQ8AEEAGACDoioqKDEndbgcffLBX+1ZUVBhvv/22cdNNNxlz5sxxO87ut3POOcfo6OjwKbaDDz7Yp7hmzJhhSDLmzJlj3H///cbWrVu9auezzz4zxo0b5xbzHXfc0ed+K1eudNtnwoQJxscff2w4nU5vH6ZhGJ7/Dk888YRPx+hp2bJlbsdctmxZv/vdfPPNbvslJSV1/f+CCy4w6urqPO7rdDqN++67r9v9O2+LFi3yOvYpU6a47X/sscca1dXVve7z8ccfGxMnTvQYsyRj5MiRXrX95z//2a3tY445xvj++++9jn93r776qmGz2dyOOXr0aOOZZ54xHA5Hv8dobGw0XnzxReOEE04w4uLiDEnGV1991ec+FRUVRl5enlu7++23X5+P5dNPPzXGjx/fdf/k5GS3YxQVFfn6NPjsnHPO8ev163A4jHXr1hmLFy82Fi1aZGRkZPT6uZSbm2usWrXK59guvfRSt2ONGzfO+O6773rd56effjKOPvroXl+f3j6vI0eO7PW9mZ2dbTzzzDO97rtjxw5jwYIFbu3abDbj448/9uqx92z/nHPOCep+X331lZGYmOgW89ixY42XX37ZcLlcfe7f+d456KCDjLlz5/Z6v0cffdStjbS0NOOVV17pdZ+Wlhbj1ltvNRISErr28fR688aLL77ott+cOXP6fZ/35vPPP/f4vOXn5xv/+Mc/jObm5n6P0dbWZrz11lvGWWed1fU50NfzMVBHHHFEV5wxMTHGoYceajzxxBN9fu7vrra21rjzzjuN9PR0j+93bz4/SktLjdjYWLd9//CHP/T6Wd3R0WE8+uijXe3abLZurwlvX+9PPPFEn+fe4447ztixY0ev+z/77LNGdna22zEOOeSQft8nhuH+mZuYmNh17oqJiTF+//vfG62trR73bW1tNX7/+98bMTExbu3fcsst/bZtGP5/RvQm1Ofe++67z5BkFBQUGL/97W+NTz/91Ku+YGVlpXH22We7xTlhwgSP91+yZEnXZ83FF19svPfee0Z7e3u/7TQ0NBjXXHONWzuZmZl9fh4MpH+6+zmv85aammr84x//6PW5aWlpMa677jqPr6Xf/va3XrXr6/VDbzy9J0PR/wGAcNN9JScAQMisXr2614UMm5qaVFdXJ7vd7tXo3NTUVN1111361a9+FeAoe7dq1SqtWrVKV1xxhSZOnKiZM2dq2rRpys/P71qkrq6uThs3btSyZcs8jlodP358vyPW5syZoylTpnSrf71x40YdeuihSk5O1rBhw9zKMki7FjocMmTIwB5kiN1222265pprJO0qFfHyyy/r+OOP1+zZs1VQUKC6ujpt2LBBr732mkpKStz2HzlypMfSJb254447dPzxx3fb9vbbb2vs2LE6+eSTNXv2bGVnZ6u+vl6bNm3Se++9p3Xr1nXd98ADD9TIkSP13HPP+fxYzz77bN14443q6Ojo2rZ06VItXbpU2dnZKiwsdCsFMGTIkF5H65500km67bbbdNNNN3XbXlRUpLPOOktXXXWV5s6dq1mzZik/P1+pqamqr69XXV2dNm3apNWrV+vbb7/1eVRffn6+Hn74YZ1yyindRm5+8cUXmj59ug499FAdfvjhGjp0qDo6OlRSUqKlS5dq1apVXfdPS0vTlVdeaZl1Ky644AK3GtWS1NHRIbvdLrvd7vWoxIMOOkjPPfechg8f7nMc119/vZ577jnV19d3bdu0aZNmzJih+fPna+7cuRo0aJBaW1tVWlqqZcuWadmyZV2vqby8PP3mN79xe03448QTT9R3332nb775RrW1tTrrrLN0++2364QTTtC4ceOUmpqqsrIyrVixQkuXLvVYQuLyyy+3bE3hWbNm6YEHHtBFF13U7XW8efNmnXrqqRo2bJiOOOIITZs2TXl5eYqLi1NdXZ2Kioq0du1affbZZ13lPA4++OBe2zn77LN15513dlv8urGxUaeccor22msvzZ8/X+PGjVN8fLwqKiq0Zs0avf32293KUk2ZMkXz58/XXXfd5fPjXLBggXJycrrNXlu1apVmz56t9PR0DRkyxK2utyR9/fXXHo+333776V//+pcWLVrU7XmrrKzU5ZdfrptuukkHH3yw9t13XxUUFCgjI0ONjY2qq6tTcXGx1qxZo3Xr1gW8XJG3XC6XPv74Y3388cey2WyaMGGC5syZowkTJig3N1c5OTlyuVyqr6/Xli1btG7dOi1fvlzt7e0ej3fNNdd4XBi5pyFDhujyyy/X/fff3237bbfdpueee04nnXSSJk+erJSUFFVXV+u///2vli5d2u28d+211+qFF17walZOf3Y/97755pv64IMPdPTRR+vAAw/U4MGD1dTUpE2bNmnJkiVus2qkXQvzPvroo7LZbD63PWjQIB1zzDF68MEH5XK5dMcdd+iRRx7RSSedpOnTpys3N1fV1dX69ttv9frrr6uiosLtGHvvvbd+//vf+/7AA8Csc29FRYXuu+8+3XfffcrNzdVee+2lmTNnasSIEcrKylJycrKam5u1detWffXVV/rggw/cPpdtNpseeOCBPtupr6/X4sWLtXjxYqWnp2uvvfbSnnvuqdGjRysrK0upqalqbW3V9u3btXbtWr333nsey0Hdc889Sk5O9ukxeuuJJ57QzJkzu5XtaWpq0uWXX66//OUvOvHEEzV58mRlZWWpqqpKX3/9tZYsWeJxDZK99trL4wK8AAALMPf7AwCIDp5G1gTiFh8fbyxatMjYtm2b37H5OwJ/oLdhw4YZ69ev9yrGjz76yONovb5unkbvWH0EvsvlMk499VS/n8/Nmzf7HP+5557rV3uTJ082Kisr3UYTejsC3zAM45ZbbvGpTW+O/dhjj3kcde3vzduRuQ8//LDHUYj93eLi4ow333zTtBFonkbgB+I2YcIE45lnnvFqRGpfnnzySb/aT0tLM1auXOn38+pplOqWLVuMwsJCv+I588wzfZoxFOoR+J1efPFFj7NBfLn1dw5Zu3atkZKS4texhw4dahQVFXn8/PTWU0895XO7/Xn77bc9jsr29xaqEfiBvN1www0+xdHU1GTssccefrW1cOFCw+l0+vV67+0z4eqrr/YrlszMTOPLL7/0+nF7Ome2trYaBx10kF/tT58+3aiqqvK6/UCPwO8UqnNv5wj8gd5sNptx33339fp4OkfgB+LmzYj2gfZPN2/e3G1Wnz+3Aw880KipqfG6TUbgA0BoUQMfAMKMzWbTrFmzdOedd6q4uFhPPPFESBesHTRo0ICPMX/+fK1cuVJTp0716v6HHnqolixZosLCwgG3bWU2m03PPfecz4tcHnDAAfrkk0/c6lh745FHHtGFF17o0z7z5s3TihUr+lx82Bs33XSTbr/9diUkJAzoOLs777zz9MUXX+jQQw8d0HGSkpL0i1/8QiNGjPDq/hdffLGee+455eTkeN1Gdna23njjDc2fP9/fMC0lOztbZ511lv7zn/9ow4YNOvPM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vmTPgSVxcXGaPn26pk+frmuuuUYtLS1BizVcNDc3a8uWLWaH4bfU1FTl5+ebHQYA9IsR+ADgQSRdFO23335asmSJrrrqKh1++OEaPXq0EhIS1NDQoKSkJI0YMULz5s3THXfcoaefftqtExsbG6v7779fF198sUaNGqX4+HjZbDbtu++++uc//6mzzjrLq0TymDFjtG7dOv3qV79Sfn6+2tvbVVBQoNNOO03r1q3TMccc47bP+++/r+uvv14HHXSQhg8f3nWhNG7cOJ177rn66quvdMUVV7jtFxsbqwceeEArVqzQGWecoREjRsjhcCghIUFTp07V+eefr9dee82/J9RL2dnZ+vLLL3XddddpxIgRamtrU05OjubPn6/PPvtMF154Ya/7Xn311fr00091+umna/z48UpISFBjY6NSUlI0ZcoUnX322Xr88cd15JFHBvUxDERKSooKCwvNDgMAgIgUSX3VUOkvgf/11193/X/BggX9Hi85Odnj9s2bN+vyyy/X5MmTlZaWppSUFE2ePFlXXHGFSkpKPO7Tc4HZNWvW6NRTT9XgwYOVmJioMWPG6He/+51qa2v7jGnlypU6/vjjlZeXp+TkZE2cOFE33HBD16CZ/tjtdt1+++2aM2eOsrOzlZiYqOHDh+u0007TypUru923ublZGzdu1LZt2zRr1izNmjVLO3bs0Pbt23X77bfruOOO03777af58+d71Xao2Ww2jRw50vIDYgBAkmwGS9cDgJv169eH7SJMZkhNTe1WNx8DU1ZWptLSUrPD8JvNZtOkSZO6LYwMAAACp7S0VGVlZWaHEVamTZvWZx38V155RaeeeqqkXYNI5s2b53MbjzzyiH71q1/J4XBI2vWlQUxMTNcglIyMDL366qtux37yySd17rnnauTIkbrjjju0aNEiORwOZWZmqqGhQS6XS9KuUpcrV65UWlqaW9uPP/64Lrzwwq77ds5cbW9v16RJk3TRRRfpd7/7nUaOHKni4mK3/VetWqUFCxaovLxc0q4BMSkpKV2zbm02m26//XZdf/31Xcl7p9OpHTt26LjjjpO0a+bCHXfcoebmZiUlJclmsykrK6trZoOVDB48WEOGDDE7DADwCiPwAcADRjX5hjIpgeFyuVRSUhLWyXtpVy1/kvcAAAQPfVXf2Gy2fuucz549u2s0dmf9e1/8+9//1kUX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rNooy9sawl7T09Oam5szHaMm/f397H0PwHEo8AFgDV47FCwQCOjGG2/Uv/7rv+rWW2/V61//el100UUrq+WnpqZ055136k1vepOefPLJmq9zvgL/pS996QU/Nzk5qaNHj658/Hd+53ckSR/4wAd0ww036L777tPs7Ox5Myyv2n/5y19e8wTtqquuqulxz7SR51upVFZto/OjH/1IkvTkk0+qp6dnzX/S6fTK9kDrvbExMDCgjo6OLT+Heuvt7d3yGyMAAOAMVuBvHqvv7Vcul3XixAnTMWoSj8dXFhABgJOwhQ4ArMGrE6J4PK5XvepVetWrXiXpzPN89NFH9eUvf1kPPvigZmZm9P73v1933333ecv49Zxva5T+/v4NfW58fHzloN2bbrpJjz32mL761a/q9ttv1+233y7LsnTppZfqla98pf7rf/2vuuiii1Z9reXbdbdv377p/MtSqVTNj122mee77OTJk5LObDe0sLBwwWsUCoU1P+7E8j4cDqu7u9t0DAAAPMOr41U7UeDb7+TJkyqVSqZj1OR843cAMIkV+ACwhmaZEIXDYT3/+c/X3/zN36ysds9kMnr44Ydr+nrnu920lltRg8GgvvKVr+jRRx/VRz7yEb30pS9VLBbTk08+qc985jO65JJL9NnPfrZu11tWj1XitbwW5XJZknTjjTeqWq2e88+JEyf085//fOWfb33rW2t+HSceutXX18ftyAAA1Em1WmULnRokEgnTETxtfn5+1eIUN2lra1M8HjcdAwDW5LwZPgA4QDNOiH73d3935c/Hjh1b+fNymX2+12Qje1yOjIys+7nR0dGVP6+1+v3yyy/XLbfcou9973uamZnRAw88oN/8zd9UuVxeWaW/bPmg1mc+BxPO93yf+blnPt/l/eGfeOKJcx4zNzenTCZTx4SNE4vF1N7ebjoGAACeUSqVVK1WTcdwlUAgoECATQjsUqlUVm2F6SaWZbH6HoCjUeADwBq8tP/9RsVisZU/P3MrnOWVSuuVx/Pz8xsqy3/wgx9c8HMdHR0r2+esJxAI6GUve5m+/e1vKxwOq1qt6oEHHlj5/Ite9CJJ0ne/+911t5hphI08X5/Pp+c+97krH1/ee/8nP/mJjh8/vvLxUqnk2gmRdGZPflbfAwBQP804Vt2qWraHxMYdP37ctXcxd3V1cbgxAEejwAeANSxv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\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# make a fig here...\n", + "# should have multiple readers for queue...\n", + "# might show multiple subscribers for reading from different pumps.\n", + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def triple_circle(x,y,box_bg):\n", + " return [\n", + " Circle((x+0.4, y+0.9), 0.5, fc=box_bg),\n", + " Circle((x+0.2, y+0.7), 0.5, fc=box_bg),\n", + " Circle((x, y+0.5), 0.5, fc=box_bg),\n", + " \n", + " Circle((x+0.16, y+0.52), 0.2, fc=\"white\"),\n", + " Circle((x+0.36, y+0.72), 0.2, fc=\"white\"),\n", + " Circle((x+0.56, y+0.92), 0.2, fc=\"white\")\n", + " ]\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [\n", + " Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, head_width=0.5, head_length=0.2)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " \n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=0.2\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " y=1.8\n", + " #patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " patches.extend(triple_circle(x+2.4,y,box_bg))\n", + " patches.extend(triple_circle(4.5, 1.8, box_bg))\n", + " len=0.5\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 3.9, len+0.3, -0.6, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.2, 1.5, len, +0.4, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(5.25, 3.1, len, +0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(5.25, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(5.2, 2.0, len+0.2, -0.5, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 )\n", + " \n", + " ])\n", + " patches.extend(triple_circle(6.5, 3.6, box_bg))\n", + " patches.extend(triple_circle(6.5, 1.8, box_bg))\n", + " patches.extend(triple_circle(6.5, 0.2, box_bg))\n", + " \n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " plt.text(4.25,2.45, 'Message', fontsize=18)\n", + " plt.text(4.25,2.25, 'Broker', fontsize=18)\n", + " plt.text(2.2,0.75, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,2.35, 'Subscriber', fontsize=18)\n", + " plt.text(2.2,4.15, 'Subscriber', fontsize=18)\n", + " plt.text(6.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(6.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(6.5,4.1, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(2, 5.2, 'Distributed Duplicate Suppression',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "851b0d84", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Each process has it's own duplicate suppression (the white component within each instance.) As The number of instances grow, the duplicate suppression performance grows naturally. This meets the \"embarassingly parallel\" goal. but it does cost complexity for analysts to set up." + ] + }, + { + "cell_type": "markdown", + "id": "1a1ad83a", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "Observations\n", + "----------------\n", + "\n", + "* If you are going to use a global cache, it better be really good.\n", + " * need to have a scaling architecture for the cache itself.\n", + "* If you are using a distributed cache, a fair one is fine.\n", + " * scales with the data transmission naturally.\n", + " * overall performance is far less sensitive to it's performance.\n", + " " + ] + }, + { + "cell_type": "markdown", + "id": "789fb938", + "metadata": {}, + "source": [ + "\n" + ] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/SundewDesign.ipynb.txt b/_sources/Contribution/Philosophy/SundewDesign.ipynb.txt new file mode 100644 index 000000000..3f0f3b34f --- /dev/null +++ b/_sources/Contribution/Philosophy/SundewDesign.ipynb.txt @@ -0,0 +1,376 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "543de844", + "metadata": {}, + "source": [ + "Sundew Algorithmic Design\n", + "=======================\n", + "\n", + "Sundew was built because PDS was too slow for message routing, which typically had:\n", + "\n", + "* hundreds of thousands of different products to route (order of 300,000 products in the routing table.)\n", + "* several dozen destinations.\n", + "* need to route 20 messages per second during the day, but during an outage, need to recover the failure quickly (say 100 messages per second, so a one hour outage would be recovered in 12 minutes or so.)\n", + "* we were seeing peak routing speed in the teens to low 20's. It could keep up with routine traffic, but would often fall behind during traffic spurts, and took a very long time to recover after outages.\n", + "\n", + "\n", + "Sundew, a replacement implementation of PDS, was eventually made to solve this problem. It made the following alrogithimic design changes:\n", + "\n", + "\n", + "Eliminate Dispatcher\n", + "-----------------------------\n", + "\n", + "Having a single process doing the routing limits scaling.\n", + "Instead, have each receiver identify the outputs for files it receives.\n", + "This means operating multiple routing engines in parallel, which gives the potential for name clashes in the destination directories (if two receivers choose to same names for different data) \n", + "\n", + "* Make sure all file names chosen for routing are unique.\n", + "\n", + "On systems with more cores available, having more processes able to route is a benefit.\n", + "Comparing regular expressions is a cpu-bound problem. With 10 receivers, one can expect a 10-fold improvement in routing performance... eg. the routing performance of the server should scale with the number of products being received.\n", + "\n", + "It should also be noted that while scanning directories is an i/o bound process with lots of waiting, routing is a completely cpu bound process, so combining the two doesn't worsen the overall load of the receivers substantially. \n", + "\n", + "\n", + "\n", + "Eliminate Log Locking\n", + "--------------------------------\n", + "\n", + "Contention for writing the combined log of all processes is performance limiting, and wastes a great deal of cpu on locks to serialize access. if each process writes its own log, there is no contention for the log file. Dropping the constraint of a single log file unlocks a great deal of performance potential.\n", + "\n", + "From an architectural perspective, this is just a pure win, there was a hard limit on absolute performance, and it is completely eliminated.\n", + "\n", + "\n", + "\n", + "Dictionary Pre-Routing\n", + "---------------------------------\n", + "\n", + "Even if we parallelize routing, the algorithm itself is quite intensive. for 10 destinations, with 10 regular expressions each, that means 100 regular expressions\n", + "to evaluate (slightly less, because of shortcuts from exclusion masks (rejects) but the order of magnitude is about right.)\n", + "\n", + "Added the use of a python dictionary listing product keys, and a set of destinations associated with each key. So there are now two steps to routing, the first narrows down the number of destinations to a subset... say 2/10 of destinations, and then the regular expressions are only applied on that subset.\n", + "\n", + "The cost to route a single product through the application (take it from seeing it in a reception directory to queuing it up for sending.) can be modelled as follows:\n", + "It is the regular expression comparisong that dominate the routing time, so \n", + "just calculate how many regular expressions are needed, on average, to \n", + "complete routing of a product.\n", + "\n", + "\n", + "\n", + "* Sundew Routing cost: log(d) * s * (re/2) ... log(300000)* 10 * (10/2) = 18*10*5 = 900 \n", + "\n", + " * each receiver only routing for a single receiver.\n", + " * d = 300000, the size of the product dictionary (around 300000) \n", + " * re = 10, the dictionary allows us to reduce the number of regex's used for product \n", + " * s is the subset of destinations chosen for a typical key (10)\n", + " * dictionary lookup time is a log of the number of different product keys to assign.\n", + " \n", + "It is important to note that S grew substantially once Sundew was in place,\n", + "we peaked at about 500 senders... with PDS, that would have meant 25000 regex comparisons per product routed, while Sundew routing algorithmic cost is independent of the number of senders extant.\n", + "\n", + "Note that a single process does all routing in PDS, where each receiver (initial configurations had R=10) distributed routing duties to each receiver, giving 10x available algorithmic speedup, as routing is embarassingly parallel (completely perallelizable.)\n", + "\n", + "**NOTE:** this algorithm only benefits \"message\" type routing with the dictionary. for file type routing, used in PDS traditionally, we fall back on pure regexes.\n", + "\n", + "\n", + "\n", + "\n", + "Write it in Python\n", + "------------------------\n", + "\n", + "Sundew was built in Python. Industry lore is that Python is much slower than C, the measurable amounts vary, but as a rule of thumb, one can attribute approximately an order of magnitude there as well. It was judged safe to use Python in this case because the application is entirely i/o bound, and thus the speed of C was not material to the problem at hand. When using python, code is much simpler to write, and much shorter. the PDS application was approximately 150,000 lines of C clode. The Python application is 25,0000 lines.\n", + "\n", + "Sundew also replaced another message router, called \"Tandem Apps\", which was 450,000 lines of code in proprietary language. replacing 600,000 lines of code is a major project, writing 25,000 lines is a biggish script. So a 96% reduction in the source code size.\n", + "This choice:\n", + "\n", + "* made the project feasible.\n", + "\n", + "* made the algorithms in use far simpler to understand and implement.\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "ac0019b8", + "metadata": {}, + "source": [ + "\n", + "\n", + "Eliminate Dispatcher\n", + "-----------------------------\n", + "\n", + "* replace single task routing for all receivers\n", + "* every receiver queues products for senders.\n", + "* routing performance expands as a function of the number of receivers.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7b8c8799", + "metadata": {}, + "source": [ + "\n", + "Dictionary Pre-Routing\n", + "---------------------------------\n", + "\n", + "* map bulletin names to a key, list the keys in a table (aka python dictionary) that lists senders.\n", + "* selects a subset of senders to which their regex's are applied.\n" + ] + }, + { + "cell_type": "markdown", + "id": "1581cfcc", + "metadata": {}, + "source": [ + "Sundew Routing cost:\n", + "--------------------------------\n", + "\n", + "* Rp = 900 = log(d) * s * (re/2)\n", + "* r = 10, each receiver only routing for a single receiver.\n", + "* d = 300000, the size of the product dictionary (around 300000) \n", + "* n = the length of each key.\n", + "* re = 10, the dictionary allows us to reduce the number of regex's used for product \n", + "* s is the subset of destinations chosen for a typical key (10)\n", + "* dictionary lookup time is a log of the number of different product keys to assign.\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "e1210444", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total routing cost, pre product, done by each receiver: 181946 instructions\n", + "in terms of load distribution, the pre-routing falls on the receivers: 36 \n", + "and the rest 5000.0 falls on the senders\n" + ] + } + ], + "source": [ + "from math import log\n", + "\n", + "r=10 # number of receivers, each receiver is routing for itself alone. naturally de-composing the routing problem.\n", + "d=300000 # number of products in the routing table, pre-existing table used by previous system.\n", + "re=10 # number of regexes in each sender, given that pre-routing with the dictionary has occurred.\n", + "s=10 # number of destinations selected by Pre-Routing.\n", + "n=10 # the number of characters in each key.\n", + "\n", + "# cost to look up each string to find a corresponding integer is in the log(d)\n", + "Lum=log(d,2) # map key string to a token number.\n", + "Luk=log(d,2) # lookup the token in the dictionary.\n", + "\n", + "# cost to search a dictionary to find a match. (integer ops...)\n", + "Rpr = 2*log(d,2)\n", + "\n", + "#convert integer ops, to RE ... \n", + "RE=100\n", + "\n", + "#Client side routing cost:\n", + "\n", + "Rpc=s*RE*re/2\n", + "\n", + "RP=Rpr+Rpc\n", + "\n", + "print(f\"total routing cost, pre product, done by each receiver: {round(Rp)} instructions\")\n", + "\n", + "print( f\"in terms of load distribution, the pre-routing falls on the receivers: {round(Rpr)} \" )\n", + "print( f\"and the rest {Rpc} falls on the senders\" )\n", + "\n", + "# for files, instead of messages.\n" + ] + }, + { + "cell_type": "markdown", + "id": "c61e039a", + "metadata": {}, + "source": [ + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "5daf576f", + "metadata": {}, + "source": [ + "Write it in Python\n", + "------------------------\n", + "\n", + "* made the project feasible (10x smaller)\n", + "\n", + "* made the algorithm simpler to understand an implement.\n", + "\n", + "* problem i/o bound, python performance not an issue.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "570d1d3e", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABfAAAAXwCAYAAAAdOmp3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/YYfK9AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdd3gUZfv28XNDQhICJJTQkSIixQJIl14VsAEq5VFRUVHBguURC2JBVFRUsKKCPCoiIlKlI9KkF6UpSA2BFEjvm3n/8Je8bHaT7CabzCT5fo7DQzK7M3NtMjubnHPPddsMwzAEAAAAAAAAAAAsxcfsAgAAAAAAAAAAgDMCfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AAAAAAAAAAAsiAAfAAAAAAAAAAALIsAHAAAAAAAAAMCCCPABAAAAAAAAALAgAnwAAAAAAAAAACyIAB8AAAAAAAAAAAsiwAcAAAAAAAAAwIII8AEAAAAAAAAAsCACfAAAAAAAAAAALIgAHwAAAAAAAAAACyLABwAAAAAAAADAggjwAQAAAAAAAACwIAJ8AACAEmz27Nmy2WwO/504ccLssgAAQAFNmjTJ6bMdAFB2+ZpdAAAAsKaMjAwdPHhQhw4dUkxMjGJiYmS32xUUFKSKFSuqXr16atiwoRo2bCh/f3+zywVKrOPHj+uvv/7SqVOnFBsbq5SUFAUFBalKlSqqUqWKmjVrpqZNmxLgAAAAAGUQAT4AAMiWmpqqhQsX6quvvtKmTZuUnJyc7zp+fn666qqr1K5dO3Xv3l39+vVT9erVi6FaoGRKT0/XokWL9OOPP2rdunWKjIzMd53g4GC1bdtWt9xyi4YNG6bQ0NBiqBRAWdajRw9t2LAh3+fZbDb5+/vL399fwcHBqlGjhurUqaOmTZuqRYsW6tSpk5o1a1YMFQMAUDoR4AMAAEnS4sWL9eijj+rMmTMerZeenq49e/Zoz549+vzzz+Xj46NHH31UH374YRFVCpRM6enpmjFjht555x2dPXvWo3VjY2O1du1arV27VuPHj9fAgQM1adIktWrVqmiKBVDsRo0apa+//jr76wYNGpSIlmiGYSglJUUpKSmKjY3VqVOnnJ5To0YN3XbbbRoxYoS6detmQpUAAJRc9MAHAKCMMwxDjzzyiG655RaPw3tXMjMzXf7xDpRlu3bt0tVXX63x48d7HN7nlJGRoUWLFqlNmza66667FB0d7aUqAaBoRERE6LPPPlP37t3Vpk0bLVmyxOySAAAoMRiBDwBAGTdmzBh9/vnnLh+77LLL1KtXL7Vs2VKhoaEKCgpSQkKCLl68qL///lu7du3Svn37lJqaWsxVAyXH//73P40ePVppaWkuH69YsaK6d++uVq1aKTQ0VKGhoSpXrpzi4uJ0/Phx7du3T5s2bVJcXJzDeoZh6JtvvtH999+vHj16FMMrAVDWNW/eXOXLl3danpaWposXLyo2Njbf9nt79uzRzTffrEGDBmnWrFm03QMAIB8E+AAAlGE///yzy/C+TZs2evvtt9WrV698J85MSkrSihUrtHDhQi1cuFCJiYlFVS5Q4nz++ecaM2aMDMNweqxjx456+eWX1atXL5eB2KXS09O1cuVKffLJJ/rll19cbg8Aitry5cvVsGHDPJ+TkpKivXv3avv27dqyZYsWLVqklJQUp+ctXbpUbdq00erVq3XllVcWUcUl06RJkzRp0iSzywAAWAQtdAAAKKMMw9CTTz7ptHzw4MHasmWLevfunW94L0kVKlTQ4MGD9b///U9hYWGaNm2arrjiiqIoGShRVqxYoUceecQpbK9UqZJ++OEHbd26VTfccEO+4b3072TRgwYN0rJly7Rz50516dKlqMoGgEIJCAhQx44d9dhjj+n777/XmTNn9Oabb7qcfPv06dPq0aOHjh8/bkKlAACUDAT4AACUUVu2bHGaHK9u3bqaPXu2/P39C7TN4OBgPfHEE5o6daoXKgRKroiICN19992y2+0Oy2vVqqUNGzbo9ttvL/C227Rpo99++01Tp06Vn59fYUsFgCJVrVo1/fe//9W+ffvUs2dPp8fPnTun22+/Pdc2YwAAlHUE+AAAlFG//PKL07JRo0apUqVKJlQDlC5PP/20IiMjHZaVL19eK1asUOvWrQu9fZvNpqefflorVqxQcHBwobcHAEWtdu3aWrNmje68806nx3bt2qVp06aZUBUAANZHD3wAAMqokydPOi277rrrTKjE2jIzM7V792798ccfioiIkM1mU/Xq1dW4cWN17tzZrfYnnrDb7fr99991/PhxnT17VtK/oxdbtmyptm3byte3eH59O3r0qA4ePKjIyEhFRkbK399f1atXV7169dSxY0cFBgYWSx0l0Z9//qlvv/3Waflrr72ma6+91qv76tWrV4HXTUtL0/bt23XmzBlFREQoMTFR1apVU40aNdSyZctia4X1999/a/fu3Tpz5oxSUlJUuXJlNW/eXJ06dVJQUJBb2zAMQ/v27dO+ffsUEREhu92umjVrqkWLFmrfvr1b7cAKKiMjQzt37tSBAwcUFRUlHx8f1a5dW40aNVLHjh1Vrlw5r+/Tbrdr9+7dOn78uCIjIxUbG6uqVasqNDRUTZs21dVXX+31fboSGRmp33//Xf/8848SEhIUHBysGjVqqEOHDmrQoEGR7DMzM1N79uzRiRMnFBkZqQsXLqhy5coKDQ1VkyZN1Lp1a/n4FO04tfT0dG3fvl0HDx5UVFSU/Pz8sr/37du3L5KfeWnh4+OjL7/8UgcOHNCff/7p8NjkyZM1ZsyYQl+UjIqK0s6dOxUREaHIyEjZ7XZVr15dNWvWVMeOHVWtWrVCbf9Sdrtdf//9t/744w9FRkYqLi5OdrtdFSpUUOXKlVW/fn01atRIl19+eZEfl3lJTk7W5s2bdebMGZ07d07lypVTrVq1dPXVV+vaa68t0nOkK3///bd27dqlsLAwpaamqlq1aqpTp466dOmiKlWqFGstAFAiGAAAoEzq16+fIcnhv5UrVxbb/o8fP+60/1mzZhVoW927d3fYTvfu3fNdZ/369U77X79+ffbjsbGxxsSJE42aNWs6PS/rv6CgIGPUqFHGqVOnClT3pSIjI41x48YZoaGhue6vSpUqxpNPPmlERERkrzdr1iyn5x0/frxANfzzzz/G2LFjjcaNG+dagyQjICDA6Nu3r7F8+XK3t/3pp586bScsLMytddPT041KlSo5rf/ggw+6vf+33nrLYV1fX18jNjbW7fU9MXr0aKdar7jiCsNutxfJ/jy1evVq4+abbzYqVqyY58+5cePGxvjx443w8PAC7adBgwYO27vnnnuyH8vIyDA+/fRTo1mzZrnuv2LFisa4ceOM6OjoXPcRFxdnvPLKK0bdunVz3U7NmjWNd955x0hPT/eo/vzOUefOnTOeeOIJIyQkJNd916hRwxg7dqzDe7YwduzYYQwbNsyoWrVqnj+7OnXqGA888IBx9OjRAu0nv3Pq+vXrjb59+xo+Pj651tCiRQvjm2++MTIzM73wyg3jt99+M+688858X3vVqlWN//znP8aBAwc83kd+59Pw8HDj8ccfN4KDg3Pdf0hIiPHEE08YUVFRbu0z5/ukIP9d+tnlTTmPg8J8vuS0a9cul6/l3XffLdD2YmNjjcmTJxtt27Y1bDZbrt8rm81mtGnTxvjoo4+M1NTUAte/c+dO4/7778/zWLj0v8qVKxt9+vQxpk2bZpw+fTrf7b/88stO2yiIP//807j99tuNoKCgPM8Xr776qpGQkFDo/edc5+WXX85+zG63G1999ZVx1VVX5VpLuXLljN69extbt24t0OsFgNKKAB8AgDLq5ptvdvrD6fPPPy+2/Vs5wN+wYUOegWDO/wIDA41FixYVqHbDMIy5c+ca1apVc3t/1apVM3755RfDMLwT4MfGxhqPPvqo4efn53Fw1KVLF+PkyZP57uPo0aNO63799ddu1bd582aX+27UqJHbr7Fv374O63bq1MntdT2RnJzsMhgvaCjlTceOHXN54S6//4KCgoxXX33V4wsQuQX4YWFhRocOHdzef7169Yx9+/Y5bX/z5s3GZZdd5vZ2OnToYFy8eNHt+vM6Ry1btizfIDnne3bu3Lkeff8uFRkZadx55515BpOu/vPz8zMee+wxIyUlxaP95XZOTUlJcXmBKq//+vXr5xAMeurIkSPGjTfe6PFx6+PjY9x///1GcnKy2/vK63z6448/5nmxxtXP3J0QsqwG+IZhGD179nTafuvWrT3aRmZmpvH+++979Bma9V+DBg2MtWvXerS/lJQU48EHH8zz4pU756L8FDbAt9vtxvPPP+/R53qDBg2MnTt3Fmr/OdfJCvBPnz5tdOrUyaPv0/PPP+/RawaA0owWOgWwa9cus0uAxdByAkBJVKtWLadlP/zwgx544AETqrGOpUuXavDgwUpPT3d7neTkZA0ZMkRLlizRDTfc4NH+Pv/8c40ZM0aGYbi9TnR0tG666SYtXLjQo325cvLkSQ0aNMiplYG7Nm3apPbt2+vnn39Wx44dc33e5ZdfrkaNGun48ePZy9asWaO77747332sWbPG5fLjx4/rn3/+UePGjfNcPzU1VZs2bXJY1qdPn3z3WxDr1q1TQkKCw7Ly5ctr1KhRRbI/d+3atUsDBgxQRESEx+smJiZq4sSJ2rdvn7755hsFBAQUuI6zZ8/q+uuvd5pAOy9nzpxRv379tGPHDtWvX1+StGrVKt16661KTk52ezvbtm3TDTfcoE2bNhWqFdWSJUs0ePBgZWRkuL1OdHS0RowYobi4OD344IMe7e+ff/7RDTfcoL///tvTUpWenq4PP/xQe/bs0aJFiwrVmiIlJUUDBgzQ+vXrPVpv1apVGjBggNatW+dxa5m1a9dq6NChiomJ8Wg96d9WO19++aX++OMPLV68WDVr1vR4G1k+/fRTPfLIIx6fp/v06aNNmzapVatWBd53afb44487HU979uxRWFiY6tatm+/6KSkpuueee/TDDz8UaP8nT55U//799fHHH7v1u09aWpoGDhyotWvXFmh/xSUzM1OjRo3S//73P4/WO3nypLp3765ff/3Vq/X8888/6tatm8LCwjxa74033pDNZtPrr7/u1XoAoCQiwAcAoIzq3LmzPv/8c4dla9as0fTp0zVu3DiTqjLX3r17NWHChOzwPjAwUL1791a3bt1Uq1Yt+fr66vTp01q1apXTH/AZGRkaPXq0Dhw44Hb/3gULFrgM78uVK6eePXuqb9++qlu3rtLT03X69GktXbpU27dvz97f8OHD9dRTTxX49Z48eVIdOnTQ+fPnnR5r3769rr/+el155ZWqUqWK0tLSFB4eri1btuiXX35Rampq9nPPnz+vgQMHavfu3Xn2ve7du7e++OKL7K/dDUFyC/AlafXq1XrooYfyXH/z5s1OQW9RBfiuws1WrVqpatWqRbI/dxw6dEg9evRwurAgSVdccYVuu+02NWnSRJUrV9a5c+e0fft2LV682On5CxYsUGpqqhYvXlygfskZGRm69dZbs8N7m82mrl27qm/fvqpfv778/f115swZrVixwunYOH/+vMaMGaNly5bp0KFDGjp0aPbPNDAwUP369ct+n9rtdh07dkw//vijDhw44LCdbdu2adq0aXrmmWc8rl/696LRuHHjssN7m82mzp07a8CAAapXr55sNptOnz6tX375RZs3b3Z4bxuGoTFjxqhatWoaMmSIW/uLiIhQly5dFB4e7vRYvXr1NHjwYDVv3lxVq1ZVRESE9u3bp4ULFyo6OtrhuRs3blSfPn20ZcsW+fv7F+i133fffQ7H95VXXqkbb7xRzZo1U9WqVRUbG6s9e/ZowYIFTueU3377TdOmTdPTTz/t9v6WLFmiIUOGOF1MLV++vHr16qUOHTqofv36Cg4OVkJCgk6cOKF169Zp48aNDs/fvn27br31Vv3222/y8/Pz+HX/8ssvGjt2bPbPMjg4WP369VPnzp1Vo0YNZWZm6sSJE1q6dKm2bdvmsG5iYqLuvfde7dixI9eLRi1atFBISIgk6dSpU7p48WL2Y35+fmrRokW+NVasWNHj12UFPXv2lI+PjzIzMx2Wb926VUOHDs1z3fT0dPXr18/p5y39e8G4Z8+euuaaa1S1alX5+voqKipKO3bs0PLlyx0mGM/IyNBDDz2kWrVq6aabbspzn1OmTHH5uVW/fn3169dPLVq0UM2aNRUQEKCkpCTFxcXp6NGj+vPPP7V161YlJibmuX1vGT9+vMvwPigoSAMGDFDnzp1Vq1YtJScn6+TJk1q2bJl27twp6d9j9tZbb9Xtt9/ulVri4+N14403Zof3WefMPn366LLLLlPFihUVGRmpzZs3a+HChUpJSXFYf8qUKbrpppvUoUMHr9QDACWVzfBkGAEkMQIfzhiBD6AkioyMVIMGDVyOYL3pppv07LPPqkuXLkW2/xMnTqhRo0YOy2bNmlWgkco9evTQhg0bsr92ZwTZr7/+qp49ezosCwgIyP7j8a677tJbb72l2rVru1x/w4YNGjx4sC5cuOCwfMqUKXruuefyrTkqKkotWrRwCBKkf4PzWbNm5Rra/Pbbb7rvvvt07NgxSf+Glzl/hsePH1fDhg3z3H9aWpq6dOmiHTt2OCwfNGiQ3n77bTVv3jzXdc+dO6dnnnlG33zzjcPydu3aaevWrbmOsp03b56GDRvmsOzAgQN5BlSJiYmqUqVKdohns9kcQtGhQ4dq/vz5ua4vSS+88ILeeOON7K+DgoJ08eLFAoV5+endu7fWrVvnsGzs2LGaPn261/fljtTUVHXo0EH79u1zWF61alV98MEH+s9//uNyvYsXL2r8+PGaPXu202Pvv/++Hn/88Xz33bBhQ4fJsv39/bMv/LRu3VozZ87M9XeolStXasiQIU6B12+//abHH39ce/bskSQNGzZM7733nsv3aWZmpqZMmaIXX3zRYXlwcLDCw8PznYjZ1Tnq0nNEs2bNNHv27FyDpe3bt2vUqFE6dOiQw/LQ0FAdPHhQ1atXz3P/hmFo4MCB+uWXXxyWBwYGavLkyXrsscdcvteSk5P18ssv691333UKRp944glNmzYtz/1KzufUS193rVq1NH369FwD1oSEBD366KOaM2eOw/KQkBCdPXvWrQmwjx8/rjZt2jiMvPf19dWTTz6pZ555RqGhobmuu3fvXo0ePdrpb7bx48fr3XffzXO/s2fP1r333uuwLOu122w2jR8/Xi+++GJ24J7T/PnzdffddzuFkHPnznU697kyatQoff3119lfN2jQwKO7Vbwt53Eguff54omrr77a6Q6wCRMmOJyzXXn88cf14YcfOixr0aKFpk2bpr59++Z6kTE5OVkffPCBJk6c6HBxKCQkRPv27dNll13mcr2UlBSFhoY6XNisUKGCZsyYoXvuuSffCWpTU1P122+/adasWQoLC3P6vuY0adIkvfLKKw7L3IluNmzYoJ49ezo9d+TIkXr//fdzPe/8+uuvGj16dJ6/W7iz/5zf90vPHR06dNDHH3+sNm3auFz3xIkTGjJkiHbv3u2wvH///lqxYkW++waA0sy8adABAICpQkNDNXbsWJePLVmyRF27dlW9evV03333aebMmdq3b59HLSNKoqw/Ml977TXNmTMn1/Be+vcigauRyF9++aVb+3r22WedwvuuXbvq119/zTPQ7tatmzZv3qwrrrhCkjxqIXKpSZMmOYX3b775ppYsWZJneC/9G+D973//08svv+ywfMeOHfrxxx9zXa93795O36+8RtdL/4YRl4YsgwYNchhBvG7dOqeQMqec++jWrVuRhPeSdOTIEadlbdu2LZJ9uePdd991Gd6vX78+1/BekqpUqaJZs2bp+eefd3rsv//9r8etECRlh/fdunXTb7/9lucAiP79+2vmzJlOy4cOHZod3j/zzDOaO3duru9THx8fvfDCCxo9erTD8tjY2AK3n8o6R7Rs2VKbNm3Kc1Ro+/bttXHjRrVs2dJheWRkpP773//mu6+5c+c6hfcBAQFasmSJnnzyyVwvlAUGBurtt9/WJ5984vTYBx98kD3S1hNZr7tx48b6/fff8xwdXbFiRc2ePVv9+/d3WB4TE6MFCxa4tb+RI0c6hPcVKlTQypUr9fbbb+cZ3kv/3vGyZcsW9e3b12H59OnTdfr0abf2f6ms8P7rr7/WO++8k2t4L0m33367y8+AS+88gqN27do5Lfvnn3/yXGflypVO4f0tt9yiPXv2qF+/fnneIRQYGKjnnntOy5Ytc/gciImJ0ZQpU3Jdb+3atU53JX388ce699578w3vpX8vYPbt21ffffed0/vaWwzD0MMPP+wUtI8fP17ffPNNnhcNe/TooU2bNqlp06aSCv67RU5Z545Bgwbp119/zTW8l/696Lt69WqndlerV6/WqVOnvFIPAJRUBPgAAJRhr776qjp16pTr42FhYZo1a5YefPBBtWrVSpUqVVKHDh302GOP6ccff3TZeqWkGzx4sNOI3dxcf/31TreZHz16NHsEW26ioqL03XffOSyrUqWKFixY4Nbo1Jo1a2rhwoUF7uN98eJFpxHhY8aMcStUvNSkSZOcQrI333wz1+dXr15d1157rcOy1atX57mPnOH7zTffrM6dO2d/feHCBafRepeKiYlxGolbVO1z7Ha7zp0757Tc1XwTxSE9PV0zZsxwWj579mxdc801bm1j8uTJTvM6pKam6qOPPipQTdWqVdP333/vVsuP4cOHO4XfWT38u3fvnuexdqlXXnnFKWArTIBWvnx5/fTTT6pWrVq+z61WrZp++uknlS9f3mH5t99+q6ioqDzXfe+995yWvf322+rdu7dbdT744IMaM2aMwzLDMFxu1x1+fn764Ycf8myTlcVms7ncz8qVK/Ndd/Xq1dq6davDsq+++kq9evVyu9by5ctr/vz5DoFlenp6gV/7k08+qbvuusut544YMULt27d3WLZhwwanUfn4V40aNZyWnTlzJs91Xn31VYevr7nmGs2fP9/pfZaXvn37Ol2EnjVrVq6/1+S8qBAYGKiRI0e6vb9LVahQoUDr5WfdunVOd/x07NhR77zzjlvr16pVS/PmzfN4ror8NGzY0O35U6pWrer0c8nMzMz3dwUAKO0I8AEAKMMCAgK0fPnyfPu+ZklJSdH27ds1ffp03X777apdu7Z69Oihr776qlSEEz4+Pnr77bc9WsfVKOb82u3NmjXLoYe89G8Ynt/I0ku1bNlSDz/8sNvPv9RHH33kMJKwYsWKeuuttwq0rYkTJzp8vXfv3jxbPuQMHzds2JDnnR05A/w+ffo4BfB5jeJfv3697HZ7njV4S2xsrNO+JOU5YrcoLViwwKl3+oABA9x+v2eZMWOGU6Dz+eefezTRc5Ynn3wyzztbcho8eLDL5W+88YZbo14lqU6dOg4XfSTledEnP+PGjcsepeqOpk2bOs0rkpqa6rI9UZbff//d6Txy9dVX69FHH/Wo1ilTpjhNXFvQi68jRozwqG1kixYtnEbbutOKNOe5qGvXrrrzzjvd3m+W4OBgp1ZPBbnzolKlSk6BYn5yfi5kZGRo//79Hu+7LHB1foyLi8v1+Zs2bdKWLVsclk2bNq1Ad1WNHz9elSpVyv46NTU114t78fHxDl8HBwcXajLsopBzXiPp3++NJ3OWtGrVyqmNVGG9/PLLbs8NJP3bGi3nZw5tjAGUdQT4AACUcSEhIVq0aJHmzJmjJk2aeLSuYRjasGGD7r//fl155ZX69ttvi6jK4tGrVy9dfvnlHq3jqoWGqzYql8oZEPj7++vuu+/2aL+S8p28NTc529zcfvvtqly5coG21blzZ6cAJq/evjnD9/j4eKeJH7NEREQ49Ea+/PLL1bBhQ6dR/3kF+Dkfq1Gjhtujzz2VW8sBT4ILb3I1YvGRRx7xeDuXX365UzuU6OjoAoXg9913n0fPb926tdOyZs2aOQXynm7nr7/+8mj9Sz3wwAMer/Pggw86LcvrLgBXP7uHHnrI7YsWWUJCQjR8+HCHZenp6fnOEeJKQV53zpHo+X3fL1y44DSHRM4WSJ4YOHCgw9cnT550mJfBHXfeeafH58ecr1vK/3OhrMp5gUnKu31Lzs+vRo0aeXR3xqUCAwOd5sLJ7fMr5x0358+f19GjRwu036KScxL1Fi1aqGPHjh5v5/777/dWSQoKCtKIESM8WqdKlSrZbQKz8P4BUNYR4AMAANlsNt111106dOiQlixZouHDh3scWJw6dUr/+c9/dO+99zqNLi8punfv7vE6NWrUUFBQkMOy2NjYXJ+fmZnp1IO6Z8+eBRql3bJlS49GAkv/ts/5448/HJZ5GoZeysfHx6mlRlaPcle6devm1OYgtwB+zZo1Dr18s4L76667ziH02bRpU66Bz9q1ax2+7tWrl0ejET2R2wR/RbW//GzevNnh66CgIKd2OO6644478t1+fq644gqPRt9LctmupWvXrh5tQ5LTpJsZGRlO/azd0axZM1155ZUer9e0aVOndkA7duzIdf4GV9/bIUOGeLxfyTs/u8DAQJehdH5yXhC12+15ft83btzo9D4qzPkp5yTEUt7nJ1cK8rng6kJwXp8LZZmr90Be58ycAXthjg/J+RjJ7fjIebHeMAwNGzasQPMqFIVjx445zaszYMCAAm2rY8eObrUIc3dbnrQ2ypLzPcT7B0BZZ617vgAAgKl8fX01aNAgDRo0SHa7XXv37tWmTZu0c+dO7d69W0eOHHHZIuRSs2fPVmJion744Ydiqtp7co74cldwcLASExOzv87rD80jR4443YrvSVuKnK677jqPRhNv3brVKTCZMmWKy17p7so5CjGv3t4VKlRQp06dHEKYNWvWuGxR4ap9jvTvRYOePXvqp59+kvRv24NNmzY5jcw/c+aM06i9oup/LynX+QsunYyzuCQmJjodF61bty5wb2NXE016OgLf0zt8JDm0t/D2dmJjY93qxX+pwrxX27RpowMHDmR/HR8fr7/++kvNmjVzem7O7229evUKPJfCddddJx8fH4f3vac/uwYNGhSoRYmru0/y+r7nduHCmxfB8pt7IKeCfC64ugBOAOmaq/Nj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+Khdu3YOy9avX1+gNhEHDx70eERwgwYN1KRJE4dl27dv9/rt/O7I2ZM+q4d9zgC/b9++uW7DVRudbdu2OQU6xRXgS9Jjjz3mNPr9r7/+0nvvvVdsNUhyakORkJCgFStWFGhb8+fPz3f7ZcGhQ4cKdMfMX3/9pQMHDjgsa9eunXx8XP855up7++OPP3q8X6lk/excvdfnzZunjIwME6opfjkvehf0gpvVJSQkuGy7FxIS4nL5pXKey9PS0vTDDz94sTrvu/LKK3X//fc7LEtKStKxY8e8to+goCBdffXVDsuWLl1aoIsbZek9BwAlBQE+AADwinXr1jktu/zyy3N9vs1mcxp9WpDJPj///HOP1zHbjTfe6PB1amqq/ve//3m8nYK+9ltuucXh68zMTL366qsF2lZh5AxiwsLCtHv3bm3ZsiV7WWhoqK699lq3t7FmzRqnyZMDAgLUpUsXL1TsnquvvlojR450Wv7iiy8WqqWBK+vWrdOePXtcPta/f3+nZZ9++qnH+zh+/LhT8F+9evVcJ5os7b744guP15k5c6bTspzngUu5+tl9/vnnHt/FERsbq++++85hmZ+fn3r27OnRdopL3bp1ne4OOH78eLG3oDJLzlHaCQkJJlVSdOx2u+69916XbaEmTpyY7wTLOT+/JGny5MlKS0vzWo1FoVmzZk7LCjOnhit33nmnw9dJSUn66KOPPNpGenq6y9ZGAABzEeADAFBGLVmyxGuTvx07dszlCLhBgwbluV7OoGbnzp0ejQTftWuX5s6d6/bzreLee+916un+yiuveNTL/+DBg/r4448LtP+nnnpKAQEBDsu+/fZbzZs3r0DbKyhXo+Jffvllh7YCvXr1chrNfqlu3bo5fC/XrVunVatWOTzn+uuvd3q9Re3dd99VaGiow7LU1FTdcMMNXuktbBiGpk6dqhtuuCHXEOi2225TnTp1HJYtXbpUy5cv92hf48aNcxqN+dBDD7mcx6IsmD59uo4ePer2848eParp06c7LPP3989zpHH79u3Vtm1bh2X79+/3+ALMCy+8oAsXLjgsu+OOO1z23reKF154wWnZ008/bcpdQsUt50SkMTExxT4BdlE6e/asevfu7fJuko4dO2rs2LH5bmPgwIFq1aqVw7Ljx4/r8ccf91aZRSI8PNxpWc7PiMK6//77Vb58eYdlr732mkd3DU2ePFkHDx70al0AgMIjwAcAoIxatmyZmjZtqnvvvbdQvU7Pnj2r2267TUlJSQ7LQ0ND821b4moE6tNPP+3Wfk+dOqU77rijwBPfmql69eoaMWKEw7Lo6GgNHTpUKSkp+a4fERGhIUOGFPi1165dW48++qjT8vvuu08LFiwo0DbtdrvmzZvnMnzLq47mzZs7LFu6dKnD13m1z5H+7fXeqVOn7K8vXryozZs3OzwnZ6ue4lCjRg19/fXXTm2izp49q27duumnn34q8LZ37dqlLl266Nlnn83zGPDz83MZiN1zzz1uBzQTJ07UsmXLHJYFBATokUce8azoUiQ1NVWDBw92K1i9ePGiBg8e7NTresSIEflOJDt+/HinZU8//bTT/A65+eqrr5wu8tlsNj355JNurW+W2267zeniRWxsrG688UanNkTuio+P19SpU/XNN994o8Qik7MFiiSPL7hZUXR0tN566y21atXK5fFbr149/fDDD25dFLTZbHrttdecln/66aeaMGFCgeca2bp1q9Pn8qWmTZuWPU+Lp+Li4pzuIgkODtZll11WoO3lJjQ0VGPGjHFYlpSUpD59+rj1e960adP0yiuveLUmAIB3EOADAFCGZWRkaPbs2WrevLk6duyoGTNmuBwl5kpSUpI+/fRTtW7d2mVbkKlTp+Y76nnw4MGqWrWqw7IlS5bogQcecLogcKmffvpJHTt2zO6ZX9yjq73hrbfecgrwfv31V/Xq1ctla4EsmzZtUpcuXbL/GA8MDCzQ/l9//XWnCSOTkpI0dOhQjR492u3evH/++acmTpyopk2batiwYdq3b59HdeR3kced3vXe2EZRuPHGG/Xxxx873UEQGxurIUOG6Prrr9fKlSvduhCTnp6uZcuWadCgQWrXrp1Dm6G8PPXUU06jVaOiotSjRw99//33ua4XExOj0aNHuwzK3nrrLaeR/WVF1rnmjz/+UJcuXfJs+7Vjxw517drV6fwYGhqqt956K999DR8+XAMGDHBYlpycrIEDB2rGjBm5BpUpKSmaMGGCHnjgARmG4fDYE088YdkJbC81d+5cp8+Gf/75Rx06dNDkyZPdaj2SmZmp9evXa8yYMbrsssv07LPP6ty5c0VVsld07NjRaV6Ep556SosWLSpRF6tTUlK0bds2TZ8+XcOGDVO9evX03HPPKTIy0um5jRo10vr161W/fn23tz9o0CCNGzfOafmbb76pnj176rfffnNrO+Hh4Zo+fbo6d+6szp07a/Hixbk+d8OGDerXr5+uuuoqvfHGG24PfDhw4ID69OmjkydPOiy/8847nUbLe8Mbb7yhxo0bOyw7c+aMWrVqpQkTJjjVnZaWpl9++UW9e/d2uGjYsWNHr9cGACg43/yfAgAAyoJt27Zp27ZtGjdunBo2bKgOHTqoRYsWql69uqpVqyabzaa4uDidPHlS+/fv19q1a5WYmOhyW3fccYfuueeefPcZEBCgiRMn6oknnnBY/sUXX2jZsmUaOnSoWrVqpUqVKunixYs6fPiwli9f7nA7+LBhwxQeHu72qFSrCA0N1aeffqrbb7/dIWTbunWrrrnmGvXq1Ut9+vRR3bp1lZGRoVOnTmnZsmXatm1b9vMrVqyop556qkAj5gICArRw4UJ17NhRp0+fdnjsyy+/1OzZs9W2bVt1795dDRs2VNWqVWW32xUTE6OIiAjt3btXO3fuVFhYWKG+D3369HFqL5KlSZMmatCggVvbePHFF10+VqVKFVMDywcffFABAQF64IEHnHo0b9myRTfccIMqVqyonj17qlWrVqpevbpCQ0Pl6+uruLg4HT9+XHv37tWmTZsK1C+5fPny+u6779S+fXuHftqRkZEaPny4XnnlFd16661q0qSJKlWqpPPnz2vbtm1avHixy8kPBw4c6DI4KyueffZZvffee0pISNDBgwfVsWNHdenSRTfeeGN2AHn69GmtWLFCGzdudArQbTabPvnkE7dbZ8yaNUutWrVyuLCamJiocePGaerUqRo8eLCaN2+ukJAQRUVFae/evVq4cKGioqKcttWmTRtNmTKlEK+++DRp0kQ//PCDBgwY4PC+SUxM1IsvvqgpU6aoS5cuuv7661W7dm2FhIQoKSlJMTExOn36tHbv3q3du3cXaHJwM9WuXVs33HCDw6j78+fP69Zbb1X58uVVv359BQUFOV0U/OKLL5zuWigqAwYMcBk8p6WlKTY2VrGxsbn+bpDTbbfdpi+//NKpdZA7pk2bpr/++ksrV650WP7bb7+pe/fuuvLKK9WjRw+1bNlSVatWlZ+fn2JiYnThwgUdOHBAu3bt0pEjRzwesX/gwAG98MILeuGFF9SwYUO1bt1a1157rWrWrKmQkJDsc/fRo0e1ceNGbd682ek8UK1atSIb6R4UFKR58+apT58+Dp8ZqampevPNN/Xmm28qJCREtWrVUnJyss6dO+d0h9Ajjzyi0NBQ/f7779nLcptwGwBQPAjwAQCAkxMnTujEiRMFWveee+7Rl19+6fbzx40bp6VLl2rNmjUOy7NGxuWlW7du+uqrr/KcDNLKhgwZok8++UQPP/ywwx/4GRkZWrVqlVMv90v5+vrqu+++86hvfk516tTR9u3bdeeddzqNWLTb7dkXdYpSjx495Ovr69RjXcq/fU6Wtm3bKiQkxGVY17NnT9ODh7vvvlstWrTQf/7zH5e9iBMSErRkyRItWbLEo+2WK1dO999/v9MI+5yaN2+u9evXa+DAgYqIiHB47PDhw3rzzTfd2t/gwYP17bff5jknQWnXqFEjffvttxo8eLDsdrsMw9DGjRu1cePGfNe12Wz69NNPNWTIELf3V6NGDW3atEk33HCD/v77b4fHTp06pffff9+t7XTp0kWLFy92mnvDynr37q2NGzdq6NChThcZExMTtXLlSqfwtjSYOnWqNmzY4BSCp6Wl5XpnVHFOdpvXHWLuuu666/Taa68V6rO7XLlyWrJkicaPH68ZM2Y4PX7kyBGPer8XRNbvSgsXLnR7neDgYP3000+qVatWkdXVtm1brV69Wv3793fZ6ismJibXi1vDhg3TBx98oEmTJjksr1y5chFUCgBwF5dRAQAoo+666y6NHDlSISEhXtle48aNtWjRIs2ePdup73defHx8tHjx4nwnvM1p5MiRWrlyZYFbyFjFQw89pG+//dapXUReqlSpokWLFummm24q9P5r1aqltWvX6vXXX/eoBleaN2+uO+64w6N1KleurHbt2rl8zN3WN+XKlVOPHj0KtY2i1rZtW+3fv1/vvPOOateuXaht+fv7a9iwYfrzzz/12WefufUebtu2rbZu3Vqg70dQUJBeeeUVzZ8/v0S2q/K2m2++WT///LNH586qVavq22+/1YMPPujx/ho3bqzNmzfrjjvu8PjiiZ+fn8aNG6fVq1cXaJSz2dq3b6/du3fr3nvvLdSkyTabTT169FDXrl29WF3RaNGihVavXq0mTZqYXYpX1apVS2PGjNFvv/2mnTt3euXCu5+fn6ZPn6758+eradOmhdpWjRo18pzbwxuBe5cuXbRlyxZ169at0NvKT7t27XTgwAENGzbM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IU7rZM1D0KZNG33++edau3atfvrpJ/3111+KiYnR6NGj9dBDD+X5PSysknhclmUE/kDJ4FveX8Pe+F4/TX5Qd911l2JjY/XII4+YXZbXJSYm6uLFi2aXAQvImgehbt26ZpcCAECZQIAPAB7o/cBE7V0+R2+//bYmT55sdjledWm4m5mZ6fDYqVOn9MQTT+jUqVOSJJvNpqCgIEVHR2vDhg3asGGDhg4dqueee85pu3a7XdOmTdP333+fvSwwMFB2u13Hjx/X8ePHtX79ev36668O6yUlJemFF17Qxo0bs5cFBQUpISFBe/bs0Z49e7R8+XK9//77bvXsr1q1qjp16qRNmzZp+fLluQb4hmFoxYoVkqQBAwY4Pf7ZZ5/piy++yP5+BQQEKCMjQ3///bf+/vtvLV68WO+//77TBZBLTZs2Td9++61sNpsqVapEH1l4BYE/YJ5yvr4a8tIXCqgYrEcffVSxsbF67rnnZLPZzC7Na8LCwswuARZy/vx5hYaGqnx5zusAABQ1AnwA8FBpHcX8+++/S/o3nK9Tp0728vj4eI0dO1Znz55Vu3bt9OCDD6ply5YqX768EhIStGjRIn322Wf68ccf1aBBAw0fPtxhux999FF2eH/zzTfrnnvuyQ4YL1y4oP379zv15ZekiRMnauPGjapfv74eeughde3aVUFBQUpNTdXvv/+u9957T/v379err76qd955x63XOHDgQG3atEkbN25UfHy8KlWq5PScffv2ZYcUOQP87777TjNnzlRQUJDuvfdeDRo0SNWrV5fdbtdff/2lDz/8UDt27NBTTz2l+fPnu2xDdPjwYe3evVt333237rrrLlWpUkVpaWmKjo526zUA3kLgD3iXj4+PBo1/T4GVq+j555/XxYsxeuutN0tFiB8bG6v4+Hizy4CFGIah8PBwtz9DAABAwRHgA4AH1s58VUEVAvXss8+aXYrXZE1iu2PHDklS165dHVrAfPXVV9nh/fTp0x1a1lSsWFEjR45UnTp19Mwzz+jLL7/U7bffnv2ckydP6ptvvpEk3X333Xrssccc9l21alX16NFDPXr0cFi+adMm/frrr6pWrZo+++wzh4nS/P391b17dzVr1kxDhw7Vr7/+qiNHjujKK6/M97V269ZNFStWVEJCglavXq3Bgwc7PWfZsmWSpFatWqlevXrZy2NiYvTx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6b4EAMDOeNUJADbUoUMHCnx8R2RkpFq1amX1GJahxAdwrhoaGlRZWXnqDvodO/TFjm/K+q/37D5ZIhotWigiuovadeqqdh37auCVP1ZEp64K7xSn8E5dFXpRx9P+nXPo6y+1e1uJZj062dO/PK+0bdu2k/997bXXnvXjg4ODT/vju3bt0oIFC/TRRx9p7969ampqUseOHTV48GAlJycrOjr6O5+zbNkyPfnkk+rYsaOWLVumf//73/rLX/6ijRs36vDhw4qKitK1116re++993sfFr9582b9+c9/1qeffiqn06kOHTroxhtv1D333NOM34Fv3tnx17/+Ve+//76+/PJLOZ1ORUREqH///kpKStJll132nc/ZvXu3br31VknSm2++qaamJv3lL3/Rhx9+qP379+uiiy7SsmXLmnV9AADgXXjFCQA2FBYWxi58nMIwDHXs2NHqMSxHiQ/g21wul77++uuTxfyJXfTlX+zQFzt2aPeur9TQ0HDy48Pad1R4p65q16mrfvCj63RVxziFd+6qiE5dFdYhRgEtz/2bpCUr/qqg4GDddtttJv7K/MPevXvVtWvXc/68xYsXKycn5+TXNjAwUIZhqKKiQhUVFXrzzTeVk5OjQYMGnTHj7bff1hNPPKGGhgaFhISosbFRlZWVWrBggf71r3/pz3/+sxwOx3c+b+nSpZoxY4aampokSSEhIdqzZ4/mz5+v1atX6/bbb//e2UtLSzV58mQdOHBA0jdn/wcHB2vv3r1asWKFVq5cqd/85jff+82ATZs2aebMmaqrq1NwcDD/BgEA4OP4lx4AbKpz587aunWr1WPAJjp06KDAwECrx7AFSnzAv82ZM0fvvrtKX1RU6MudFar/1hErIe0ivynjO3VV16FX6or/2z0f0amr2nWMVaug0+/mvhCbVyzULbfcotDQUNOzfdGll14qwzDkcrmUl5ennJwcxcbGNvvz16xZoxkzZqhly5b6+c9/rjvuuOPkbvudO3fqD3/4g959911lZGTor3/962l34h88eFBPPfWUbrnlFt17772Kjo6W0+nUm2++qWeeeUZffPGFXnrpJf36178+5fO2bt2qmTNnqqmpSQMHDtQjjzyiuLg4NTQ0aNWqVZo1a5b+9Kc/nXH23bt368EHH9SRI0dO7tjv3r27WrZsqaqqKr322muaP3++fve736lr16667rrrTpszc+ZMXXLJJZoyZYouvfTSk792AADgm3ilCQA21aZNG4WHh+vgwYNWjwKLtWzZ8rQFhD+jxAf8U11dnaZNm6a20bHqfc1I9brlm3L+xFn0QW08W6Lvr/hcu7ZuVP6MRz163XNlGIbVI5zUqVMn3XbbbVq8eLHKysp05513qmfPnurXr5969+6tPn36qFu3bqed+fjx48rNzZUkPfLII/rJT35yys/HxcVp1qxZmjRpkt5//30VFRVp8uTvHm3kdDp1yy236NFH//t1Cw4O1k9/+lNVVlaqqKhI77zzzncK/Hnz5qmxsVEXX3yx8vPzTx7v07JlSyUmJqpt27Z68MEHz/hrz8/P15EjR3TzzTfrqaeeOuXnIiIi9Otf/1qhoaF69tln9fzzz5+xwA8LC9O8efNOeYfAuXwTxC7sdF8CAGBn9nzKEgBA0je78Hlxg44dOyogIMDqMWwnOTlZL730kjb+4y96Y/q9J48z8KQTJf7ixYuVNGbMKUd1ADCfw+HQqDvukOFq0siJT+uapId06bW3Krr7ZR4v7yXp03cWKrRtW918880ev7Y3y8jI0L333qvWrVvL5XLp888/1+uvv67p06dr9OjRSkxM1DPPPHPymJkT1q5dq3379ikyMvLkefCnM3LkSEnSP//5zzN+zC9/+cvT/viJc/m/+uqrUx6ge+TIEf3rX/+S9M03j093Nv/gwYPVr1+/0+ZWV1dr9erVkvSdB/eebvZt27Z959d/wk9/+tPTHu/jbVjjAgDQPGwTAwAbCwoKUvv27bVv3z6rR4FFTtwDOD124gP+Z9LDD+uvryZoy/tvqu/133/euDu5XC5tXrFQo26//YwPWrULuxWlLVu21K9//WulpKTo/fff1yeffKItW7Zox44dOn78uKqqqrRgwQK99dZbysvLU9++fSVJJSUlkqTDhw/rpptuOmP+8ePHJUl79uw57c+HhYWpS5cup/25b/+be/jw4ZNf261bt578RvGVV155xmtfeeWV2rRp03d+fPPmzSc//4EHHjjj53/bnj17FBkZ+Z0f79+/f7M+HwAA+AZeXQKAzXXs2FEHDhxQY2Oj1aPAAp06dbJd8WI3lPiAf4mPj9dVV1+jdQuetbTA3/35Ru3buU1JSQWWzdBcdv13JCQkRDfffPPJdzDU19fr008/1auvvqoPPvhAhw4dUkZGhhYtWqSgoCDt379f0jcF/Zl2p39bfX39aX/8+3avf/sdb99+V1VVVdXJ/46Kijrj55/p507MLqlZs0s65R0A3xYREdGsz7c7u96XAADYDa8sAcDmTpx/XllZafUo8DCHw6Hw8HCrx/AKlPiAf0mbPEmjRo3SV599rC59fmjJDCXvvKqIyIt0ww03WHL9c+EtRWlQUJASEhKUkJCgJ554Qn//+9+1d+9e/fOf/9R11113cjPDVVddpYIC+3/j5NtOzB4UFKS1a9deUJan/31zF2+5LwEAsJpv/MsPAD4uKipKrVq1snoMeFhMTAwvbs8BZ+ID/uPWW29VXNdLtHbBs5Zcv6mpSZtXvqqf3nWXV/z77I3PUbn99v++u6KiokKSdNFFF0mSysrKPD7Pt3e9f9/Rht/eaf9tJ2avr6/XV199Ze5wXopvcgMA0DwU+ADgBVq0aKHOnTtbPQY8KCwsTKGhnn8go7ejxAf8Q0BAgB6e+JA2vfuaDn3t+TL0y03rdPDrrzRmTJLHr30+vLEo/fYxN4GBgZL+e/b7vn379Omnn3p0nt69e5/c+b5+/fozftzHH3982h/v16/fyW/Kv/POO+YP6IW88RtLAABYgQIfALxERESEQkJCrB4DHmAYxhkfroezo8QH/MM999yjkJAQ/fO1uR6/9qfvLFSnzjG6+uqrPX7t82GnorSyslI7d+4868f9/e9/P/nfvXv3liQNGTLk5E72OXPmnPGM+BOqq6svYNJThYaGatCgQZKkV1555bTn63/44YenfYCt9M067tprr5Ukvfzyy2f9PTBzdruy030JAICdUeADgJcwDEOxsbEcqeIHOnXqpKCgIKvH8GqU+IDvCw0N1f333aePF/9R9XU1HrtuY0ODPlv1usYkjfaas8jttAP/iy++0F133aWHHnpIf//737V79+6TP9fQ0KCtW7fqySefVFFRkSSpT58+uvzyyyV9c3781KlTZRiGtm7dql/84hf65z//qePHj5/MqKys1BtvvKFx48bp9ddfN3X2X//61woICFBFRYUmTpx48mifhoYGrVy5Uo888sj3vntu4sSJCgsLU21tre69914tXbpUNTX/vXcPHTqk4uJipaenKzMz09TZ7chO9yUAAHbGv5gA4EWCg4PVsWPHU17swrc4HA516NDB6jF8Ag+2BXzfgw8+qGeeeUbr35yvq0c/6JFrln+8Skeq9ispyTuOz5HstdO5ZcuWampq0tq1a08+zLVVq1ZyOBw6fPiwXC7XyY/t3bu35syZc8rf3dddd52eeuopzZgxQ9u2bdODDz6ogIAAhYSE6OjRozp27NjJjz2x490sl156qTIyMpSdna2PP/5Yd955p0JCQnTs2DEdO3ZMcXFxuv322/Xss6d/NkNMTIx+97vfacqUKdq9e7emT5+urKwshYaGqqGhQXV1dSc/Nj4+3tTZ7chO9yUAAHbGK0gA8DLR0dE6ePCgjh49avUoMBnvsjAfJT7g27p06aI777pLxX/N1+C7fqMWHigES1a8qm7de2jAgAFuv5ZZ7FSUDh48WIsXL9batWv16aefqry8XPv27dORI0cUHBys9u3bq1evXrr++us1bNiw0/6d/aMf/Ug//OEP9frrr+uf//ynvvrqK9XU1Kh169aKi4vT5Zdfruuuu05XXHGF6fOPGjVK3bt31/z587Vp0yY5nU5FR0frxhtv1M9//nMVFxd/7+f37t1br732mt58802tWbNG27dv1+HDh9WqVStdfPHFuvTSSzV06FCvOZ7pQtjpvgQAwM4M17e3OHyPDRs2uHsW4LwMHDjQ6hEAj6urq9PWrVvVzL/C4SU6duyoTp06WT2GTyoqKtK4ceM0YOTdlpT4kvTZmiVaMPUu3X777ZT4gIlyc3OVkZGhe3+/St1/eINbr3W83qmZiR2UPmminnzySbdey0zHjh3T5s2brR4DOEVMTAzvOgQAoBm849BGAMApHA6HoqOjrR4DJnI4HOrYsaPVY/gszsQHfFNRUZEeeeQRDfzxPbpk4HVuv97n65braM1hrzo+R2KnM+yJ+xIAgOahwAcAL9WxY0c5HA6rx4AJDMNQXFycVx+d43Q69eqrr2rPnj1Wj3JGlPiAb7HinTUl7yxUv/6Xq3fv3m6/lpkoSmFHvBMNAIDmocAHAC9lGIa6du3q1aUvvtG5c2e1bt3a6jEuSGrqg0pKStIDD/zG6lG+FyU+4BusKO/ra49o6wfLlDzGu3bfn0BZCrvhG0sAADQPBT4AeLHg4GDFxMRYPQYuQGhoqKKioqwe44IUFhbqxRf/pMtuvFNLly456wP8rEaJD3g3q55pseW9pTpe79TPfvYzj1zPbJSlsBvuSQAAmocCHwC8XFRUlCIiIqweA+chMDDQ699F8cknn+iB3/xG8bf/SmNmvaa4foP10MSH1djYaPVo34sSH/BOVj6QetOKVzVo8FWKjY312DXNxA582A33JAAAzUOBDwA+IDY2lvPwvUyLFi3UrVs3tWrVyupRzltVVZVuH3WHoi7pqx+nFcgwDI2clKfSzZtUWFho9XhnRYkPeBcry/vaQwe07V/veO3xOdI33zQG7MIwDK9eAwEA4EkU+ADgA06Uwexk8h7e/k2XpqYmpaSM1YFDh5Wc+4ZaBQVLkrr0jdcVN4/VI9MyVV1dbfGUZ0eJD3gHK8t7SSotfkOupibdddddHr2umYKCgqweATgpMDDQq9+BCACAJ1HgA4CPCAwMVLdu3Xgx5AWio6O9/tij6dOz9Pbby/WzrAUK73jqcRKJqdmqqa1VVtYMi6Y7N5T4gL1ZXd5L0qYVC3XDDTeqQ4cOHr+2WSjwYSfcjwAANB8FPgD4kJCQEF188cVWj4HvERYWpk6dOlk9xgV5++239eSTT+jG+55Qz8GJ3/n5sKjOGnr3VOXn56msrMyCCc8dJT5gT3Yo76v3VeqLDe9pjBcfnyNRmMJeuB8BAGg+CnwA8DEXXXSR2rdvb/UYOI3g4GCvf2htRUWFksaMUa+rfqQbfvnoGT9uSMpkhURGKy0t3YPTXRhKfMBe7FDeS9Kmla+pZatWuv322y25vlkoTGEn3I8AADQfBT4A+KAuXbooNDTU6jHwLQEBAerWrZsCAgKsHuW8OZ1OjbrjTgU42umnT738vWVaYLBDiak5Wrp0iYqLiz045YWhxAfswS7lvSRtXrFQN//oZrVr186yGczQqlUrr/4GMnwLD1UGAKD5eNohztvAgQOtHgHAGRiGoUsuuURbt25VfX291eP4vRNfj+DgYKtHuSCpqQ+q9LPP9OsX18kRdvYz/Psnjta/XntOD018WJ9u/MRrvnmRnJwsSRo3bpwkWVIenijxF0y9S0ljxmjhggU8pBp+w07l/YFd5frys4+V+9hky2Ywi2EYCgwMZF0AW2AHPgAAzccOfADwUS1btlSvXr14gWSxE+V927ZtrR7lghQWFurFF/+kn2TMU+feA5r1OYZhaOSkPJVu3qTCwkI3T2guduID1rBTeS9JJe+8KkebNvrxj39s6RxmYU0Au+BeBACg+SjwAcCHtWrVihLfQifKe28/duGTTz7RA7/5jeJv/5WuvPWec/rcLn3jdcXNY/XItExVV1e7aUL3oMQHPMtu5b30zfE5P/nJT+RwOKwexRSsB2AHLVu29Jp35QEAYAfWr4oBAG5FiW8NXynvq6qqdPuoO9Thkr76cVrBeWUkpmarprZWWVkzTJ7O/SjxAc+wY3n/ddlm7Sn/TGOSkqwexTSsBWAH3IcAAJwb61fGAAC3o8T3LF8p75uampSSMlYHDh3WmNw31Cro/M7wD4vqrKF3T1V+fp7KyspMntL9KPEB97JjeS9Jn76zUO3CwzVixAirRzENDw6FHXAfAgBwbuyxOgYAuB0lvmf4SnkvSVlZWXr77eX6WdYChXeMvaCsISmTFRIZrbS0dJOm8yxKfMA97Freu1wula54VXfecadPlY3e/jB1+AbuQwAAzo09VsgAAI+gxHcvXyrv3377bT3xxBMadv+T6jk48YLzAoMdSkzN0dKlS1RcXGzChJ5HiQ+Yy67lvSR99dlH+k/lDiUljbZ6FFMFBwfLMAyrx4Cf85VnSgAA4Cn2WSUDADziRInPiydzBQQEqFu3bj5R3ldUVGhMcrJ6X32zrv9Fpmm5/RNHK67fYD008WE1NjaalutJlPiAOexc3ktSyTsL1SG6o6699lqrRzGVYRj8+w/LcQ8CAHBu7LVSBgB4xIkSPzIy0upRfEJwcLB69+6tsLAwq0e5YMeOHdOoO+5Ui9Zhuuupl00t1QzD0MhJeSrdvEmFhYWm5XoaJT5wYexe3jc1Nqp05V81+mc/VUBAgNXjmI7yFFZq2bKlTx1LBQCAJ9hrtQwA8JgWLVooLi5OXbp04e30F6Bdu3bq3bu3z5zn2qpVK7Vq1UrBbdoquE1b0/O79I3XFTeP1SPTMlVdXW16vqdQ4gPnx+7lvSR98cl7qv7P10pKSrJ6FLdo06aN1SPAj3H/AQBw7uy3YgYAeFRUVJR69Oihli1bWj2K1+nUqZMuueQSn9qhaRiGCvLzVLmtROuXzXfLNRJTs1VTW6usrBluyfcUSnzg3NihvG9qxvFdJe8sVFzXSxQfH++BiTyPHfiwEvcfAADnjgIfAKDQ0FD94Ac/4EVVMwUEBKh79+7q2LGjT757ISEhQckpKVo5L1POmsOm54dFddbQu6cqPz9PZWVlpud7EiU+0Dx2KO8/W7NET1wbqneff/KMH9Nw/Ji2FL+hMUmjffLvd+mbY9/s+M4H+AfWmgAAnDtWbgAASVJgYCDn4jeDL513/31mZWerwVmj1YXu2SU/JGWyQiKjlZaW7pZ8T6LEB76fXcr7BVPvUlhoiNYtfFb1dTWn/bjt/1qh2sMHffb4HIkH2cJaHKEDAMC5o8AHAJx04lz8rl27qlWrVlaPYzsdOnTwqfPuv09MTIymZmRo7cI8HdhVbnp+YLBDiak5Wrp0iYqLi03P9zRKfOD07FTe33777frwww9VX1ej9W+e/oiwkncW6geX9lHfvn09PKVnUeDDCieeswMAAM4NBT4A4DsiIiLUp08ftW/f3upRbKFNmzb6wQ9+oJiYGJ867/5s0tLSFBUVpeUFU9yS3z9xtOL6DdZDEx9WYzPOpbY7SnzgVHYr7xcuWKCuXbvqzrvu0j9fzfvOefjHnHX69/tLlTzGd3ffn0CBDytw3wEAcH4o8AEApxUQEKCLL75YvXv3VuvWra0exxInfg969erlly86HQ6HZufmqLR4kcrXrzE93zAMjZyUp9LNm1RYWGh6vhUo8YFv2LG8P/Gw9kkPP6z/7PpCW95/85SP//f7y1RfV6vRo0d7fFZP4xgTWIH7DgCA82O4XC5Xcz5ww4YN7p4FXmbgwIFWjwDAQ1wul/bt26fdu3dbUkhaITw8XF26dPH7t3q7XC4NGnyVKg8e1fiXN6iFG96B8Nrj47Tzo7dVXrbdZ54tYOfyEnA3b7j/r75miL6uM3TfC++f/LGXJ9+m1nV79PFHH3p6XI9zuVz69NNP/ebfdNhD9+7dfebfeQAAPIkd+ACAszIMQx06dFCfPn3Url07q8dxq+DgYPXo0UOXXHKJ35f30jdf+4L8PFVuK9H6Zac/M/pCJaZmq6a2VllZ7nlgrhXYiQ9/5Q3lvSSlTZ6kLzZ+oK8++1iSdPTIIX2+brlfHJ8jffN3e0hIiNVjwM+wAx8AgPNDgQ8AaLbAwEB169ZNl156qSIjI2UYhtUjmSYkJOTkr61t27ZWj2MrCQkJSk5J0cp5mXLWHDY9Pyyqs4bePVX5+XkqKyszPd8qlPjwN95S3kvSrbfeqriul2jtgmclSaXFi9TYcFw//elPPTmupfi3Dp7Upk0b3gkGAMB5osAHAJyz1q1bKy4uTpdddpmio6O9+sGu7dq1U+/evdWrVy+1a9fOp74pYaZZ2dlqcNZodaF7dskPSZmskMhopaWluyXfKpT48BfeVN5L3zzj5OGJD2nTu6/p0NdfafOKVzV06LXq1KmTBye2FgU+PIn7DQCA80eBDwA4b61atVLnzp112WWXKSYmRoGBgVaP1CyGYah9+/bq06ePunXrxlu6myEmJkZTMzK0dmGeDuwqNz0/MNihxNQcLV26RMXFxabnW4kSH77O28r7E+655x6FhIRoxe8fVdnHqzTGT47POaF169YcFQePocAHAOD88RBbnDceYgvgf7lcLlVXV+vQoUOqrq62VUloGIZCQ0MVFhamiIgI3sZ9Hurq6tSjZy9F9IpXSu4bpue7XC798ZdXK8RVq083fuLV7+w4HW8tOYHv4+33dXr6FM2ZM1stW7bU119/rcjISDdOaj8VFRU6cOCA1WPAxwUEBKh///68yxEAgPPEDnwAgGkMw1C7du0UFxenfv36qVevXoqOjlbr1q0tmadly5aKjIzUJZdcov79+6tHjx6KioqitDxPDodDs3NzVFq8SOXr15iebxiGRk7KU+nmTSosLDQ932rsxIev8fbyXpImTHhQAQEBGjEi0e/Ke4ld0fCM0NBQynsAAC4AO/Bx3tiBD+Bc1NfXq7q6WocPH5bT6dSxY8fUzH+Cmq1Vq1YKCgpSSEiIwsLC1KZNG14wmszlcmnQ4KtUefCoxr+8QS3csEv+tcfHaedHb6u8bLvCwsJMz7eaL5SegC/dxxUVFWrXrp3atWtn7oBeoKGhQSUlJVaPAR938cUXq3379laPAQCA16LAx3mjwAdwIVwul+rr6+V0Ok/5//r6ejU1NampqUkul0sul0uGYZzyv8DAQAUHBysoKEjBwcEn/9vXjlyxqw8//FCDBg3SqEdfUPxt95qeX72vUs/c0VMPjh+v2bNzTc+3A18qP+F/uH99y7///W/V1dVZPQZ8WN++fRUUFGT1GAAAeC0KfJw3CnwA8F8pY8fqzbdWaPKi7QoOMf8IhlV/mq41L07Xli1b1L17d9Pz7YASFN6I+9b3VFZW6uuvv7Z6DPiooKAg9e3b1+oxAADwapyBDwAAztms7Gw1OGu0unCGW/KHpExWSGS00tLS3ZJvB5yJD29Dee+bOAcf7sT9BQDAhaPABwAA5ywmJkZTMzK0dmGeDuwqNz0/MNihxNQcLV26RMXFxabn2wUlPrwF5b3vCgkJseTrCf/gi8+yAQDA01ipAQCA85KWlqaoqCgtL5jilvz+iaMV12+wHpr4sBobG91yDTugxIfdUd77NsMw2CUNtzAMQyEhIVaPAQCA16PABwAA58XhcGh2bo5KixepfP0a0/MNw9DISXkq3bxJhYWFpufbCSU+7Iry3j9ERERYPQJ8ULt27RQQEGD1GAAAeD0KfAAAcN6SkpIUnzBIbz0zUU1u2CXfpW+8rrh5rB6Zlqnq6mrT8+2EEh92Q3nvP8LCwihaYTq+MQQAgDko8AEAwHkzDEMF+Xmq3Fai9cvmu+UaianZqqmtVVaWex6YayeU+LALynv/0qJFC7Vr187qMeBDAgICOP8eAACTUOADAIALkpCQoOSUFK2clylnzWHT88OiOmvo3VOVn5+nsrIy0/PthhIfVqO890/sloaZIiIiZBiG1WMAAOATKPABAMAFm5WdrQZnjVYXumeX/JCUyQqJjFZaWrpb8u2GEh9Wobz3X6GhoWrVqpXVY8BH8A0hAADMQ4EPAAAuWExMjKZmZGjtwjwd2FVuen5gsEOJqTlaunSJiouLTc+3I0p8eBrlvX8zDIPSFaYIDAxUmzZtrB4DAACfQYEPAABMkZaWpqioKC0vmOKW/P6JoxXXb7AemviwGt3wwFw7osSHp1DeQ2LXNMzB8TkAAJiLAh8AAJjC4XBodm6OSosXqXz9GtPzDcPQyEl5Kt28SYWFhabn2xUlPtyN8h4nOBwOBQcHWz0GvBzfCAIAwFwU+AAAwDRJSUmKTxikt56ZqCY37JLv0jdeV9w8Vo9My1R1dbXp+XZFiQ93obzH/6J8xYVo3bq1WrdubfUYAAD4FAp8AABgGsMwVJCfp8ptJVq/bL5brpGYmq2a2lplZbnngbl2RYkPs1He43Qo8HEhuH8AADAfBT4AADBVQkKCklNStHJeppw1h03PD4vqrKF3T1V+fp7KyspMz7czSnyYhfIeZxIUFKTQ0FCrx4AXMgxDkZGRVo8BAIDPocAHAACmm5WdrQZnjVYXumeX/JCUyQqJjFZaWrpb8u2MEh8XivIeZxMVFWX1CPBC4eHhatWqldVjAADgcyjwAQCA6WJiYjQ1I0NrF+bpwK5y0/MDgx1KTM3R0qVLVFxcbHq+3VHi43xR3qM5wsLCFBQUZPUY8DIdOnSwegQAAHwSBT4AAHCLtLQ0RUVFaXnBFLfk908crbh+g/XQxIfV6IYH5todJT7OFeU9msswDMpYnJPQ0FA5HA6rxwAAwCdR4AMAALdwOByanZuj0uJFKl+/xvR8wzA0clKeSjdvUmFhoen53oASH81FeY9zFRkZqYCAAKvHgJfgGz4AALgPBT4AAHCbpKQkxScM0lvPTFSTG3bJd+kbrytuHqtHpmWqurra9HxvQImPs6G8x/lo0aKF2rdvb/UY8AJBQUFq27at1WMAAOCzKPABAIDbGIahgvw8VW4r0fpl891yjcTUbNXU1ioryz0PzPUGlPg4E8p7XIioqCgZhmH1GLC5Dh06cJ8AAOBGFPgAAMCtEhISlJySopXzMuWsOWx6flhUZw29e6ry8/NUVlZmer63oMTH/6K8x4Vq1aqVwsPDrR4DNhYQEKDIyEirxwAAwKdR4AMAALeblZ2tBmeNVhe6Z5f8kJTJComMVlpaulvyvQUlPk6gvIdZONsc36d9+/aW/P0CAIA/4V9aAADgdjExMZqakaG1C/N0YFe56fmBwQ4lpuZo6dIlKi4uNj3fm1Dig/IeZnI4HAoNDbV6DNiQYRiKioqyegwAAHweBT4AAPCItLQ0RUVFaXnBFLfk908crbh+g/XQxIfV6IYH5noTSnz/RXkPd2AXPk4nMjJSrVq1snoMAAB8nuFyuVxWDwEAAPzDggULlJycrF/9YbW6XXmd6flflX6k3/08Qc8//7x+9atfmZ7vbShz/Qtfb7jTtm3bdOTIEavHgE0YhqG+ffsqMDDQ6lEAAPB5FPgAAMBjXC6XBg2+SpUHj2r8yxvUIiDA9Gu89vg47fzobZWXbVdYWJjp+d6GUtc/8HWGu9XW1mrr1q1WjwGbiI6OVufOna0eAwAAv8AROgAAwGMMw1BBfp4qt5Vo/bL5brlGYmq2amprlZXlngfmehuO0/F9lPfwhDZt2ig8PNzqMWADLVu2VHR0tNVjAADgNyjwAQCARyUkJCg5JUUr52XKWXPY9PywqM4aevdU5efnqayszPR8b0SJ77so7+FJnTt3lmEYVo8Bi3Xs2FEBbngHHQAAOD0KfAAA4HGzsrPV4KzR6kL37JIfkjJZIZHRSktLd0u+N6LE9z2U9/C0oKAgtW/f3uoxYCHuAQAAPI8CHwAAeFxMTIymZmRo7cI8HdhVbnp+YLBDiak5Wrp0iYqLi03P91aU+L6D8h5WYfe1f+vUqRPvwgAAwMN4iC0AALBEXV2devTspYhe8UrJfcP0fJfLpT/+8mqFuGr16cZPKJy+hfLXu/H1g9W+/vprVVZWWj0GPMzhcKh3794U+AAAeBg78AEAgCUcDodm5+aotHiRytevMT3fMAyNnJSn0s2bVFhYaHq+N2MnvveivIcdREVFqVWrVlaPAQ+LiYmhvAcAwALswAcAAJZxuVwaNPgqVR48qvEvb1ALN+ySf+3xcdr50dsqL9uusLAw0/O9GWWwd+HrBTs5cOCAKioqrB4DHhIWFqbu3btbPQYAAH6JHfgAAMAyhmGoID9PldtKtH7ZfLdcIzE1WzW1tcrKcs8Dc70ZO/G9B+U97CYiIkIhISFWjwEPMAxDXbp0sXoMAAD8FgU+AACwVEJCgpJTUrRyXqacNYdNzw+L6qyhd09Vfn6eysrKTM/3dpT49kd5b46KigruLRMZhqHY2FiOVPEDnTp1UlBQkNVjAADgtyjwAQCA5WZlZ6vBWaPVhe7ZJT8kZbJCIqOVlpbulnxvR4lvX5T3F+7IkSNKS0tXjx49lDQm2epxfEpwcLA6duxo9RhwI4fDoQ4dOlg9BgAAfo0CHwAAWC4mJkZTMzK0dmGeDuwqNz0/MNihxNQcLV26RMXFxabn+wJKfPuhvL8wLpdLr732mnr1/oGe+93v1DK4jVqwWdx00dHRat26tdVjwA14lwUAAPZAgQ8AAGwhLS1NUVFRWl4wxS35/RNHK67fYD008WE1Nja65RrejhLfPijvL8zWrVs1bPgI/exnP1Nkr3g9/Pq/FdIuQnFxcVaP5nMMw1BcXBwlrw+Kjo6Ww+GwegwAAPweBT4AALAFh8Oh2bk5Ki1epPL1a0zPNwxDIyflqXTzJhUWFpqe7yso8a1HeX/+amtrNXXqI+rXr582bduhe/LfUsrsRQqLitHBr7+iwHcTh8Oh6Ohoq8eAiRwOB8cjAQBgExT4AADANpKSkhSfMEhvPTNRTW7YJd+lb7yuuHmsHpmWqerqatPzfQUlvnUo78+Py+XSokWL1Kv3D/RMXp6u++VjeujVUvW6+keSpMP7d6uxoUGxsbEWT+q7OnbsyG5tH8G7KgAAsBcKfAAAYBuGYaggP0+V20q0ftl8t1wjMTVbNbW1yspyzwNzfQUlvudR3p+f7du366abfqQ77rhDYZdcrol//Uw33vuYWgUFn/yYg3sqJIkd+G5kGIa6du1K6esDOnfuzHMNAACwEQp8AABgKwkJCUpOSdHKeZly1hw2PT8sqrOG3j1V+fl5KisrMz3fl1Diew7l/bmrq6vTY489pj59+2pD6eca98ybGvfMm4qMueQ7H3toz05JYge+mwUHBysmJsbqMXABQkNDFRUVZfUYAADgWyjwAQCA7czKzlaDs0arC92zS35IymSFREYrLS3dLfm+hBLf/Sjvz43L5dLSpUvV+weXKid3tobePVUTX9uiS4f++IyfU7W7QhGRF6lNmzYenNQ/RUVFKSIiwuoxcB4CAwN5FwUAADZEgQ8AAGwnJiZGUzMytHZhng7sKjc9PzDYocTUHC1dukTFxcWm5/saSnz3obw/N+Xl5brllh/rtttukyPmB3ror6Uafv+TahX8/cd9HPp6J7vvPSg2Npbz8L1MixYt1K1bN7Vq1crqUQAAwP+gwAcAALaUlpamqKgoLS+Y4pb8/omjFddvsB6a+LAa3fDAXF9DiW8+yvvmO3r0qJ544gld2qeP/rVxs8bOWayf57+li7p0b9bnH9pToUu6xrl3SJx0ogy26/2E7+KbLgAA2BcFPgAAsCWHw6HZuTkqLV6k8vVrTM83DEMjJ+WpdPMmFRYWmp7viyjxzUN533z/+Mc/dGmfvpoxc6auHjNZE1/boj7X3XZOx3xU76lgB76HBQYGqlu3bhzH4gWio6M59ggAABujwAcAALaVlJSk+IRBeuuZiWpywy75Ln3jdcXNY/XItExVV1ebnu+LKPEvHOV981RUVOgnP7lNt9xyiwKjuumhhZuVOH6GAluf2zn2TU1NqtrzpeLi4twzKM4oJCREF198sdVj4HuEhYWpU6dOVo8BAAC+BwU+AACwLcMwVJCfp8ptJVq/bL5brpGYmq2a2lplZbnngbm+iBL//FHen119fb1mzJihH1x6qd7/13olz3pd98x9R+3jep1XXs2Br9Vw/BgFvkUuuugitW/f3uoxcBrBwcE8tBYAAC9AgQ8AAGwtISFBySkpWjkvU86aw6bnh0V11tC7pyo/P09lZWWm5/sqSvxzR3l/du+8844u7dNXv33iCSXc9aAe/ttWXTbszgsqGA/u2SlJHKFjoS5duig0NNTqMfAtAQEB6tatmwICAqweBQAAnAUFPgAAsL1Z2dlqcNZodaF7dskPSZmskMhopaWluyXfV1HiNx/l/ff78ssvdccdd+qmm25Si/AuemjhJv1oQo6CHCEXnH1wd4UkCnwrGYahSy65REFBQVaPAv336xEcHGz1KAAAoBko8AEAgO3FxMRoakaG1i7M04Fd5abnBwY7lJiao6VLl6i4uNj0fF9GiX92lPdnduzYMc2aNUu9f/ADrfpgnZJmLNQvf79KUV1/YNo1Dn29U2Ht2iksLMy0TJy7li1bqlevXpT4FjtR3rdt29bqUQAAQDMZLpfLZfUQAAAAZ1NXV6cePXspole8UnLfMD3f5XLpj7+8WiGuWn268ROOFThHlNSnx+/Lma1atUoP/Ga8ysvLdNXohzTsV79VcIj5peLimb9WXfmH2vTpRtOzce6OHz+uzz//XPX19VaP4ndOlPft2rWzehQAAHAO2IEPAAC8gsPh0OzcHJUWL1L5+jWm5xuGoZGT8lS6eZMKCwtNz/d17MT/Lsr706usrNRPf/YzDRs2TI0hHTSh6FPd8vDTbinvpW924HeN4/gcu2jVqhU78S1AeQ8AgPeiwAcAAF4jKSlJ8QmD9NYzE9XU2Gh6fpe+8bri5rF6ZFqmqqurTc/3dZT4/0V5/13Hjx/XnDlz1Kt3b72z6j397KmX9as/rlF0975uvW71ngp1jYtz6zVwbijxPYvyHgAA70aBDwAAvIZhGCrIz1PlthKtXzbfLddITM1WTW2tsrLc88BcX0eJT3l/OmvWrFG//pcrIyND/W/5hR7+21YNuDlFhmG49boul0tVe3YqjgLfdijxPYPyHgAA70eBDwAAvEpCQoKSU1K0cl6mnDWHTc8Pi+qsoXdPVX5+nsrKykzP9wf+XOJT3p9qz549Sk5J0fXXXy9nYDulvvKJbk3LV+vQdh65fu3B/TrmPKrYWI7QsSNKfPeivAcAwDdQ4AMAAK8zKztbDc4arS50zy75ISmTFRIZrbS0dLfk+wN/LPEp778rLX2KXn31Vd35eKHue+EDderZ36PXP7i7QpLYgW9jJ0p8h8Nh9Sg+JSAgQN26daO8BwDAB1DgAwAArxMTE6OpGRlauzBPB3aVm54fGOxQYmqOli5douLiYtPz/YU/lfiU96f341tGqqmxUXXVByz5PTn49U5JYge+zZ0o8SMjI60exScEBwerd+/eCgsLs3oUAABgAgp8AADgldLS0hQVFaXlBVPckt8/cbTi+g3WQxMfVqMbHpjrL/yhxKe8P7PRo0crI2Oq3n4uQ5+ve9vj1z+4u0IhoaEKDw/3+LVxblq0aKG4uDh16dLF7c9G8GXt2rVT7969FRwcbPUoAADAJBT4AADAKzkcDs3OzVFp8SKVr19jer5hGBo5KU+lmzepsLDQ9Hx/4sslPuX92c2YkaWbbvqR/po5Wvt3bvPotQ/t2amLL46lEPYiUVFR6tGjh+3uY2/QqVMnXXLJJQoICLB6FAAAYCLD5XK5rB4CAADgfLhcLg0afJUqDx7V+Jc3qIUbSovXHh+nnR+9rfKy7RxHcIF8rez2tV+PO1VXVys+YZCq61164M8fKjjEM3+W/jzxFnVrZ+jvy5Z55Howz7Fjx1ReXq66ujqrR7G9gIAAde3alX+jAADwUezABwAAXsswDBXk56lyW4nWL5vvlmskpmarprZWWVnueWCuP/GlnfiU9+cmLCxMy95cqqMHv9ZfH01Wk4eOpTr89U7Fcf69VwoMDORc/GbgvHsAAHwfBT4AAPBqCQkJSk5J0cp5mXLWHDY9Pyyqs4bePVX5+XkqKyszPd/f+EKJT3l/fnr27Km/vvqqPl+3XCt+/6jbr+dyuVS1u0JxcXFuvxbc48S5+F27dlWrVq2sHsd2OnTowHn3AAD4AQp8AADg9WZlZ6vBWaPVhe7ZJT8kZbJCIqOVlpbulnx/480lPuX9hbnpppuUk5OjNX+epU/fXujWa9VVV8lZV0OB7wMiIiLUp08ftW/f3upRbKFNmzb6wQ9+oJiYGM67BwDAD1DgAwAArxcTE6OpGRlauzBPB3aVm54fGOxQYmqOli5douLiYtPz/ZE3lviU9+aYPHmyklNStCjrl6rc+onbrnPo652SpFiO0PEJAQEBuvjii9W7d2+1bt3a6nEsceL3oFevXnI4HFaPAwAAPISH2AIAAJ9QV1enHj17KaJXvFJy3zA93+Vy6Y+/vFohrlp9uvETdj2axFtKcW+Z01vs3r1bMTExunrMw7rl4afdco3S4kV6Zcod2rdvHzu3fYzL5dK+ffu0e/duS775Z4Xw8HB16dKFo4QAAPBD7MAHAAA+weFwaHZujkqLF6l8/RrT8w3D0MhJeSrdvEmFhYWm5/srb9iJT3lvvmeeeVZBrdvo2nFT3HaNg3t2Krh1a1100UVuuwasYRiGOnTooD59+qhdu3ZWj+NWwcHB6tGjhy655BLKewAA/BQ78AEAgM9wuVwaNPgqVR48qvEvb1ALN+ySf+3xcdr50dsqL9uusLAw0/P9lV1LcrvO5c0qKirUs1cvXXtPpob96nG3XefNOQ/pP5+u1Of/3uK2a8Aejh49qr1796qqqkq+8vI2JCREHTp0UFhYmAzDsHocAABgIXbgAwAAn2EYhgry81S5rUTrl813yzUSU7NVU1urrCz3PDDXX9lxJ/5LL71Eee8Gjz72mBxtIzQkeZJbr3Noz0515fx7v9C6dWvFxcXpsssuU3R0tFcfcdauXTv17t1bvXr1Urt27SjvAQAAO/ABAIDvSRk7Vm++tUKTF21XcEhb0/NX/Wm61rw4XVu2bFH37t1Nz/dndtnx/sqUOySXS1fccrfueOxFynuTbNy4UQMHDtRtj/xBCaPuc+u1nku+XD++frB+//vfu/U6sJ/Gxkb95z//0b59+3Ts2DGrxzkrwzB00UUXKSoqSsHBwVaPAwAAbIYd+AAAwOfMys5Wg7NGqwvds0t+SMpkhURGKy0t3S35/swOO/GPHa2VXKK8d4P0KRmKiuulK2/9hduvdXDPTsWyA98vBQQEqEOHDurbt6+6deumyMhI2/0ZMgxDbdu2VZcuXdSvXz9dfPHFlPcAAOC07LWKAQAAMEFMTIymZmRoetYMxY+6T5Ex3UzNDwx2KDE1R68+OkbFxcW64YYbTM33d8nJyZKkcePGSZJHd+JvXF6k13477v/Ke47NMdOKFSu06t2VGjtniQLc/Gs6euSQ6g4fUlxcnFuvA3szDEPt2rVTu3bt5HK5VFtbq+rqalVXV+vo0aMen6dly5YKCwtTWFiY2rZt69VH/QAAAM/hCB0AAOCT6urq1KNnL0X0ildK7hum57tcLv3xl1crxFWrTzd+QhHjBp4+Tudkec+Z96ZramrS5QOuUE2LUN33wvtuP9d797YSFYy5XP/85z81aNAgt14L3qm+vl7V1dU6fPiwnE6njh07ZvoDcFu1aqWgoCCFhIQoLCxMbdq04Ux7AABwznzjFQEAAMD/cDgcmp2bo+TkZJWvX6NuV15nar5hGBo5KU+/+3mCCgsL9atf/crUfHh2Jz7lvXsVFRVp86YSPVC4ziMF5qE9OyWJI3RwRkFBQYqKilJUVJSkb74pW19fL6fTecr/19fXq6mpSU1NTXK5XHK5XDIM45T/BQYGKjg4WEFBQQoODj7533xjFwAAmIEd+AAAwGe5XC4NGnyVKg8e1fiXN6iFG8qU1x4fp50fva3ysu0KCwszPR/u34lPee9eTqdTPXr2UniPHyo5928euebaVwv0znNTdLSuzpKvJwAAAGAWVrMAAMBnGYahgvw8VW4r0fpl891yjcTUbNXU1ioryz0PzIV7H2xLee9+c+fO1e7dlRoxfqbHrnlwz0516XIx5T0AAAC8HitaAADg0xISEpSckqKV8zLlrDlsen5YVGcNvXuq8vPzVFZWZno+vuGOEp/y3v2qqqqUNWOG4kfdr/axPT123YO7K9SVB9gCAADAB1DgAwAAnzcrO1sNzhqtLnTPLvkhKZMVEhmttLR0t+TjG2aW+JT3njFzZrbqjzXoxnsf9+h1D+/dqbg4zr8HAACA96PABwAAPi8mJkZTMzK0dmGeDuwqNz0/MNihxNQcLV26RMXFxabn47/MKPEp7z2joqJCBc8V6Jqx6QqN7ODRax/cXaE4duADAADAB1DgAwAAv5CWlqaoqCgtL5jilvz+iaMV12+wHpr4sBobG91yDXzjQkp8ynvPefSxx+RoG6EhyZM8et36uhrVHDpAgQ8AAACfQIEPAAD8gsPh0OzcHJUWL1L5+jWm5xuGoZGT8lS6eZMKCwtNz8epzqfEp7z3nI0bN2pBUZFuuO9JBTlCPHrtQ3t2SpJiYzlCBwAAAN7PcLlcLquHAAAA8ASXy6VBg69S5cGjGv/yBrUICDD9Gq89Pk47P3pb5WXbFRYWZno+TlVUVKRx48ZpwFlKecp7zxo2fIRKy7/ShIWbFeDhX+fW//cP/XniLfrqq68UExPj0WsDAAAAZmMHPgAA8BuGYaggP0+V20q0ftl8t1wjMTVbNbW1yspyzwNzcarm7MSnvPesFStWaNW7KzVi/Cy3lPcfvPKM/vbbcTruPHranz+4Z6datmypjh07mn5tAAAAwNMo8AEAgF9JSEhQckqKVs7LlLPmsOn5YVGdNfTuqcrPz1NZWZnp+fiu7yvxKe89q6mpSWnpU9T18mt06bW3mp6/v+JzLX9uitb/42UtmnGfTvdm4oO7KxTT5WIFuOEdNgAAAICnUeADAAC/Mys7Ww3OGq0udM8u+SEpkxUSGa20tHS35OO7TlfiU957XlFRkTZvKtFNE3JlGIbp+e/Mm6ZOnTpr/vz52rj8FX1Q9Mx3PubQ1zsVx/n3AAAA8BG+/QoCAADgNGJiYjQ1I0PTs2YoftR9iozpZmp+YLBDiak5evXRMSouLtYNN9xgaj5OLzk5WZI0btw4HdhVrp0l/4/y3oOcTqemZT6qy264Q7H9Bpuev7NknUqLF+mll17S2LFjtXXr55o9e4qiu/VVz8GJJz+uek+FBv6wj+nXBwAAAKzAQ2wBAIBfqqurU4+evRTRK14puW+Ynu9yufTHX16tEFetPt34Ccd5eNBvfvMb/f73f9DAH9+tOx570bLy/pUpd6pz507avm2bgoODPT6Dp82ZM0cZU6fq4de2qH1sT1OzXS6X/njvNQr9vz9PLVq0UGNjo2699Sda88H/0wN//ujkNWcmRmvSgw/ot7/9rakzAAAAAFbgCB0AAOCXHA6HZufmqLR4kcrXrzE93zAMjZyUp9LNm1RYWGh6Pk6vqKhIf/zjHy0v74sy7tLFlyVo9+7dGjtunBoaGjw+hydVVVUpa8YMxY+63/TyXpK2vLdUFSXr9PSc2Se/pgEBAVqwoEgxnTrqlbSfyFlTrePOozp8YK/i4uJMnwEAAACwAgU+AADwW0lJSYpPGKS3npmopsZG0/O79I3XFTeP1SPTMlVdXW16Pk5VVFSkcePGacBI68v7Ptffrvv++J6Sc/6mxYsXK2nMGJ8u8WfOzFb9sQbdeO/jpmc3NhzXO89laNiw4Ro+fPgpPxcWFqZlby7V0ao9+uujyaravUOSKPABAADgMyjwAQCA3zIMQwX5earcVqL1y+a75RqJqdmqqa1VVpZ7HpiLb5xa3lt35v2J8n501gIFtGypPtfdpjGzXvfpEr+iokIFzxXomrHpCo3sYHr+x0te1P4vtys3N+e0P9+zZ0+99te/6vN1y7V45v2SpFgeYgsAAAAfQYEPAAD8WkJCgpJTUrRyXqacNYdNzw+L6qyhd09Vfn6eysrKTM+Hfcv7E3y9xH/0scfkaBuhIcmTTM+ur6tR8QtPKDklRQMGDDjjxyUmJionJ0cVn/4/BQQEKCYmxvRZAAAAACvwEFsAAOD3du3apZ69ein+zlT9aMLpd/leiGPOOj17Z28NTRioJUsWm57vz+xe3v/vxy2Yepduv/12LVywQC3P8HHeZOPGjRo4cKBue+QPShh1n+n57z7/pN7780xt37btrLvqXS6XHnxwglxGC/3uuXzTZwEAAACsQIEPAAAg6amnntL0rBl6+PUtiozpZnr+p28v1KuPjtGqVat0ww03mJ7vj7ypvP/2x/tSiT9s+AiVln+lCQs3n/XXfq6OHNirObd3U+oDD2jOnNmmZgMAAADeggIfAABAUl1dnXr07KWIXvFKyX3D9HyXy6U//vJqhbhq9enGTxQQEGD6NfyJN5b33/48XyjxV6xYocTERI2ds0R9rvuJ6flLZv1GW1e9qi/KyxUeHm56PgAAAOANOAMfAABAksPh0OzcHJUWL1L5+jWm5xuGoZGT8lS6eZMKCwtNz/cn3lzeS75xJn5TU5PS0qeo6+XX6NJrbzU9f3/F5/po8fPKnDaN8h4AAAB+jR34AAAA/8flcmnQ4KtUefCoxr+8QS3csEv+tcfHaedHb6u8bLvCwsJMz/d13l7e/2+Ot+7Ef/nllzVu3Dg9ULhOsf0Gm57/ypQ7dGj7em3f9rmCg4NNzwcAAAC8BTvwAQAA/o9hGCrIz1PlthKtXzbfLddITM1WTW2tsrJmuCXfl/lSeS957058p9OpaZmP6rIb7nBLeb+zZJ1Kixdp5owsynsAAAD4PQp8AACAb0lISFBySopWzsuUs+aw6flhUZ019O6pys/PU1lZmen5vsrXyvsTvLHEnzt3rnbvrtSI8TNNz3a5XFpekK7L+vVXcnKy6fkAAACAt6HABwAA+B+zsrPV4KzR6kL37JIfkjJZIZHRSk+f4pZ8X+Or5f0J3lTiV1VVKWvGDMWPul/tY3uanr/lvaWqKFmnp+fMtuTrDAAAANgNZ+Cfhw0bNlg9Amxm4MCBVo8AADDZU089pelZM/Tw61sUGdPN9PxP316oVx8do+LiYl1//fWm5/sKXy/v//c6dj8TPy0tXb/7/R80eXGZQiM7mJrd2HBc+T/rq349YrVy5QpTswEAAABvxbYWAACA00hLS1NUVJSWF7hnl3z/xNGK6zdYEx6aqMbGRrdcw9v5U3kv2X8nfkVFhQqeK9A1Y9NNL+8l6eMlL2r/l9uVm5tjejYAAADgrSjwAQAATsPhcGh2bo5KixepfP0a0/MNw9DISXkq3bxJhYWFpud7O38r70+wc4n/6GOPydE2QkOSJ5meXV9Xo+IXnlBySooGDBhgej4AAADgrSjwAQAAziApKUnxCYP01jMT1eSGXfJd+sbripvH6pFpmaqurjY931vZp7y/U5ded5vHyvsT7Fjib9y4UQuKinTDfU8qyBFiev4Hrzyto0cOKmv6dNOzAQAAAG9GgQ8AAHAGhmGoID9PldtKtH7ZfLdcIzE1WzW1tcrKcs8Dc72Nfcr7u+RqcikwuI0MC2awW4mfPiVDUXG9dOWtvzA9+8iBvfrgldma8OAExcbGmp4PAAAAeDMKfAAAgO+RkJCg5JQUrZyXKWfNYdPzw6I6a+jdU5Wfn6eysjLT872JXcr7BVPv0qhRt+vPf56vT5e/rDem36umpiaPz2KXEn/FihVa9e5KjRg/yy3vRFj1wpNqHRSozMxppmcDAAAA3o4CHwAA4CxmZWerwVmj1YXu2SU/JGWyQiKjlZ7ungfmegM7lfe33367Fi5YoHHjxumll17Sxn/8xW9L/KamJqWlT1HXy6/Rpdfeanr+/orP9dHi55U5bZrCw8NNzwcAAAC8HQU+AADAWcTExGhqRobWLszTgV3lpucHBjuUmJqjJUsWa/Xq1abn250dy/uW/7fTPDk52a9L/KKiIm3eVKKbJuTKMAzT89+ZN02dOnVWamqq6dkAAACALzBcLpfL6iG8zYYNG6weATYzcOBAq0cAALhZXV2devTspYhe8UrJfcP0fJfLpT/+8mqFuGr16cZPFBAQYPo17MjO5b03zmkmp9OpHj17KbzHD5Wc+zfT83eWrNPvf3m1XnrpJY0dO9b0fAAAAMAXsAMfAACgGRwOh2bn5qi0eJHK168xPd8wDI2clKfSzZtUWFhoer4deVMp7o878efOnavduys1YvxM07NdLpeWF6Trsn79lZycbHo+AAAA4Cso8AEAAJopKSlJ8QmD9NYzE9XU2Gh6fpe+8bri5rF6ZFqmqqurTc+3E28q70/wpxK/qqpKWTNmKH7U/Wof29P0/C3vLVVFyTo9PWe2JV97AAAAwFuwWgYAAGgmwzBUkJ+nym0lWr9svluukZiarZraWmVlueeBuXbgjeX9Cf5S4s+cma36Yw268d7HTc9ubDiud57L0LBhwzV8+HDT8wEAAABfQoEPAABwDhISEpSckqKV8zLlrDlsen5YVGcNvXuq8vPzVFZWZnq+1by5vD/B10v8iooKFTxXoGvGpis0soNpuSd8vORF7f9yu3Jzc0zPBgAAAHwNBT4AAMA5mpWdrQZnjVYXumeX/JCUyQqJjFZ6+hS35FvFF8r7E3y5xH/0scfkaBuhIcmTTMn7tvq6GhW/8ISSU1I0YMAA0/MBAAAAX0OBDwAAcI5iYmI0NSNDaxfm6cCuctPzA4MdSkzN0ZIli7V69WrT863gS+X9Cb5Y4m/cuFELiop0w31PKsgRYtKU//XBK0/r6JGDypo+3fRsAAAAwBdR4AMAAJyHtLQ0RUVFaXmBe3bJ908crbh+gzXhoYlqdMMDcz3JF8v7E3ytxE+fkqGouF668tZfmDjhN44c2KsPXpmtCQ9OUGxsrOn5AAAAgC+iwAcAADgPDodDs3NzVFq8SOXr15iebxiGRk7KU+nmTSosLDQ931N8ubw/wVdK/BUrVmjVuys1YvwsBZj8eyRJq154Uq2DApWZOc30bAAAAMBXGS6Xy2X1EN5mw4YNVo8Amxk4cKDVIwAALOByuTRo8FWqPHhU41/eoBYBAaZf47XHx2nnR2+rvGy7wsLCTM93J38o77/Nm3+9TU1NunzAFappEar7XnhfhmGYOtf+is/17M/6KGfWLKWlpZmaDQAAAPgyduADAACcJ8MwVJCfp8ptJVq/bL5brpGYmq2a2lplZbnngbnu4s1l9vny5p34RUVF2rypRDdNyDW9vJekd+ZNU6dOnZWammp6NgAAAODLKPABAAAuQEJCgpJTUrRyXqacNYdNzw+L6qyhd09Vfn6eysrKTM93B38s70/wxhLf6XRqWuajuuyGOxTbb7Dp8+wsWafS4kWaOSNLwcHBpucDAAAAvowCHwAA4ALNys5Wg7NGqwvds0t+SMpkhURGKz3dPQ/MNZM/l/cneFuJP3fuXO3eXakR42eaPofL5dLygnRd1q+/kpOTTc8HAAAAfB0FPgAAwAWKiYnR1IwMrV2YpwO7yk3PDwx2KDE1R0uWLNbq1atNzzcL5f1/eUuJX1VVpawZMxQ/6n61j+1p+gxb3luqipJ1enrObEvuBwAAAMDbsYoGAAAwQVpamqKiorS8wD275PsnjlZcv8Ga8NBENTY2uuUaF4Ly/ru8ocSfOTNb9ccadOO9j5t+7caG43rnuQwNGzZcw4cPNz0fAAAA8AcU+AAAACZwOByanZuj0uJFKl+/xvR8wzA0clKeSjdvUmFhoen5F4Ly/szsXOJXVFSo4LkCXTM2XaGRHUy/7sdLXtT+L7crNzfH9GwAAADAX1DgAwAAmCQpKUnxCYP01jMT1eSGXfJd+sbripvH6pFpmaqurjY9/3xQ3p+dXUv8Rx97TI62ERqSPMn069XX1aj4hSeUnJKiAQMGmJ4PAAAA+AsKfAAAAJMYhqGC/DxVbivR+mXz3XKNxNRs1dTWKivLPQ/MPReU981ntxL/ph/9SAuKinTDfU8qyBFi+rU+eOVpHT1yUFnTp5ueDQAAAPgTCnwAAAATJSQkKDklRSvnZcpZc9j0/LCozhp691Tl5+eprKzM9Pzmorw/d3Yq8VevXq3ILt115a2/MP0aRw7s1QevzNaEBycoNjbW9HwAAADAn1DgAwAAmGxWdrYanDVaXeieXfJDUiYrJDJa6enueWDu2VDenz87lPitgh1qamzUjybMVoAbft9WvfCkWgcFKjNzmunZAAAAgL+hwAcAADBZTEyMpmZkaO3CPB3YVW56fmCwQ4mpOVqyZLFWr15tev73oby/cFaW+E1NTVpeMEVxl1+jS6+91fT8/RWf66PFzytz2jSFh4ebng8AAAD4Gwp8AAAAN0hLS1NUVJSWF7hnl3z/xNGK6zdYEx6aqEY3PDD3dCjvzWNVif/p8iLt2VaiH03IlWEYpue/M2+aOnXqrNTUVNOzAQAAAH9EgQ8AAOAGDodDs3NzVFq8SOXr15iebxiGRk7KU+nmTSosLDQ9/39R3pvP0yX+8XqnVvz+UfW94Q7F9htsev7OknUqLV6kmTOyFBwcbHo+AAAA4I8Ml8vlsnoIb7NhwwarR4DNDBw40OoRAAA25HK5NGjwVao8eFTjX96gFgEBpl/jtcfHaedHb6u8bLvCwsJMz5co793NU7+/7788R2/PnaqHX9ui9rE9Tc12uVz6473XKNRVq083fmLJPQIAAAD4IlbWAAAAbmIYhgry81S5rUTrl813yzUSU7NVU1urrCz3PDCX8t79PLETv666SsWFMxQ/6n7Ty3tJ2vLeUlWUrNPTc2ZT3gMAAAAmYnUNAADgRgkJCUpOSdHKeZly1hw2PT8sqrOG3j1V+fl5KisrMzWb8t5z3F3ir/lztpoaG3TjvY+bmitJjQ3H9c5zGRo2bLiGDx9uej4AAADgzyjwAQAA3GxWdrYanDVaXeieXfJDUiYrJDJa6enmPTCX8t7z3FXiV+2u0NpXCzR0bLpCIzuYkvltHy95Ufu/3K7c3BzTswEAAAB/R4EPAADgZjExMZqakaG1C/N0YFe56fmBwQ4lpuZoyZLFWr169QXnUd5bxx0l/srfPyZH2wgNSZ5kwoSnqq+rUfELTyg5JUUDBgwwPR8AAADwdxT4AAAAHpCWlqaoqCgtLzBvl/y39U8crbh+gzXhoYlqbGw87xzKe+uZWeJXbt2oT98u0rD7n1SQI8TEKb/xwStP6+iRg8qaPt30bAAAAAAU+AAAAB7hcDg0OzdHpcWLVL5+jen5hmFo5KQ8lW7epMLCwvPKoLy3D7NK/Lefy9BFsb105a2/MHlC6ciBvfrgldma8OAExcbGmp4PAAAAgAIfAADAY5KSkhSfMEhvPTNRTRewS/5MuvSN1xU3j9Uj0zJVXV19Tp9LeW8/F1rib/vXCm3/cKVuSp2lADf8Xq564Um1DgpUZuY007MBAAAAfIMCHwAAwEMMw1BBfp4qt5Vo/bL5brlGYmq2amprlZXV/AfmUt7b1/mW+E1NTVpeMEVxl1+jS6+91fS59ld8ro8WP6/MadMUHh5uej4AAACAb1DgAwAAeFBCQoKSU1K0cl6mnDWHTc8Pi+qsoXdPVX5+nsrKys768ZT39nc+Jf6ny4u0Z1uJfjQhV4ZhmD7TO/OmqVOnzkpNTTU9GwAAAMB/UeADAAB42KzsbDU4a7S6sPm75M/FkJTJComMVnr69z8wl/Lee5xLiX+83qkVv39UfW+4Q7H9Bps+y86SdSotXqTpTz2p4OBg0/MBAAAA/BcFPgAAgIfFxMRoakaG1i7M04Fd5abnBwY7lJiaoyVLFmv16tWn/RjKe+/T3BL/n6/N1eH9lUocP9P0GVwul/6Rl6YWAS3193/8Qw0NDaZfAwAAAMB/UeADAABYIC0tTVFRUVpe8P275M9X/8TRius3WBMemqjG/3lgLuW99zpbiV9XXaXiwhmKH3W/2sf2NP36W95bqi83/1PX/zJTS5YsUdKYMZT4AAAAgBtR4AMAAFjA4XBodm6OSosXqXz9GtPzDcPQyEl5Kt28SYWFhSd/nPLe+31fib/mz9lqamzQjfc+bvp1GxuOa3lBhnokDNfw+57QmFmva/HixZT4AAAAgBtR4AMAAFgkKSlJ8QmD9NYzE9X0P7vkzdClb7yuuHmsHpmWqerqasp7H3K6Er9qd4XWvlqgoWPTFRrZwfRrfrzkRR34artuejBHktTnutso8QEAAAA3M1wul8vqIbzNhg0brB4BNjNw4ECrRwAAeKkPP/xQgwYN0qhHX1D8bfeanl+9r1LP3NFTN15/vd5+eznlvY/59jdlGhuOqfyjVUpbvF1BjhBTr1NfV6PZt3dXj4QR+tlTL53yc3xdAQAAAPdhBz4AAICFEhISlJySopXzMuWsOWx6flhUZ/UY/CO99dZbGjByHOW9jzmxE/+Tv/9Zny5foGH3P2l6eS9JH7zytI4ePqgRD0z/zs+xEx8AAABwHwp8AAAAi83KzlaDs0arC2eYnr1xeZE+W7NYA398t+547EXKex+UnJysPn36KrJLN1156y9Mzz9yYK/ef3m2rh49QeEdY0/7MZT4AAAAgHtQ4AMAAFgsJiZGUzMytHZhng7sKjctd+PyIr3223G6YiTlvS9bsWKFSks36+aH5ijADb+/q154UgGtAnXdPdO+9+Mo8QEAAADzedUZ+Jw9D7viDHwAwIWqq6tTj569FNErXim5b1xw3qnlPcfm+KqmpiZdPuAK1bQI1X0vvC/DMEzN31/xuZ79WR/dlDpLQ8emNetz+LoDAAAA5mEHPgAAgA04HA7Nzs1RafEila9fc0FZlPf+o6ioSJs3leimCbmml/eS9Pbvpqlt+84a/NPUZn8OO/EBAAAA81DgAwAA2ERSUpLiEwbprWcmqqmx8bwyKO/9h9Pp1LTMR3XZDXcott9g0/N3lqzTZ6sXacQDWWoVFHxOn0uJDwAAAJiDAh8AAMAmDMNQQX6eKreVaP2y+ef8+ZT3/mXu3LnavbtSI8bPND3b5XLprfx0dezZX5f/KPm8MijxAQAAgAtHgQ8AAGAjCQkJSk5J0cp5mXLWHG7251He+5eqqiplzZih+FH3q31sT9Pzt7y3VDs3rdPNE2Zf0L1EiQ8AAABcGAp8AAAAm5mVna0GZ41WF85o1sdT3vufmTOzVX+sQTfe+7jp2Y0Nx7W8IEM9Eoarx6DhF5xHiQ8AAACcPwp8AAAAm4mJidHUjAytXZinA7vKv/djKe/9T0VFhQqeK9A1Y9MVGtnB9PyPl7yoA19t100P5piWSYkPAAAAnB8KfAAAABtKS0tTVFSUlhdMOePHUN77p0cfe0yOthEakjzJ9Oz6uhq9+8ITuvxHKerce4Cp2ZT4AAAAwLmjwAcAALAhh8Oh2bk5Ki1epPL1a77z85T3/mnjxo1aUFSkG+57UkGOENPzP3jlaR09fFAjHphuerZEiQ8AAACcKwp8AAAAm0pKSlJ8wiC99cxENTU2nvxxynv/lT4lQ1FxvXTlrb8wPfvIgb16/+XZunr0BIV3jDU9/wRKfAAAAKD5KPABAABsyjAMFeTnqXJbidYvmy+J8t6frVixQqveXakR42cpwA2/56teeFIBrQJ13T3TTM/+X5T4AAAAQPNQ4AMAANhYQkKCklNStHJepj5a/CfKez/V1NSktPQp6nr5Nbr02ltNz99f8bk+Wvy8rr9nmhxtw03PPx1KfAAAAODsKPABAABsblZ2tpw1h7R45n2U936qqKhImzeV6KYJuTIMw/T8t+c+orbtO2vwT1NNz/4+lPgAAADA96PABwAAsLn33ntPDccbdMUtlPf+yOl0alrmo7rshjsU22+w6fk7S9bpszWL1S46VgGtAk3PPxtKfAAAAODMKPABAABsrKioSOPGjdMVt4zTHY+9SHnvh+bOnavduys1YvxM07NdLpeWF6Tr4thY7Sz5f3pj+r1qamoy/TpnQ4kPAAAAnB4FPgAAgE2dKO8HjLyb8t5PVVVVKWvGDMWPul/tY3uanr/lvaWqKFmnP73wgl5++WVt/MdfKPEBAAAAG+EVGAAAgA2dWt5zbI6/mjkzW/XHGnTjvY+bnt3YcFzvPJehYcOGa/jw4Sd/fNy4cZJkyX13osRfMPUuJY0Zw30HAAAAv8cOfAAAAJuhvIckVVRUqOC5Al0zNl2hkR1Mz/94yYva/+V25ebmnPyx5ORkvfTSS+zEBwAAAGyCAh8AAMBG7FLevzLlTg0fPpzy3kKPPvaYHG0jNCR5kunZ9XU1Kn7hCSWnpGjAgAGn/BwlPgAAAGAfFPgAAAA2YZfyvijjLrUKClZgq0DKe4ts3LhRC4qKdMN9TyrIEWJ6/gevPK2jRw4qa/r00/48JT4AAABgDxT4AAAANmCn8r7P9bfrtql/0JtvLtXq1as9Pgek9CkZiorrpStv/YXp2UcO7NUHr8zWhAcnKDY29owfR4kPAAAAWI8CHwAAwGJ2K+9HZy3QgJuTFddvsCY8NFGNjY0en8efrVixQqveXakR42cpwA3vgFj1wpNqHRSozMxpZ/1YSnwAAADAWhT4AAAAFrJjeR/QsqUMw9DISXkq3bxJhYWFHp/JXzU1NSktfYq6Xn6NLr32VtPz91d8ro8WP6/MadMUHh7erM+hxAcAAACsQ4EPAABgEbuW9yd06RuvK24eq0emZaq6utrjs/mjoqIibd5Uopsm5MowDNPz35k3TZ06dVZqauo5fR4lPgAAAGANCnwAAAAL2L28PyExNVs1tbXKyprh8fn8jdPp1LTMR3XZDXcott9g0/N3lqxTafEizZyRpeDg4HP+fEp8AAAAwPMo8AEAADzMW8p7SQqL6qyhd09Vfn6eysrKPDylf5k7d652767UiPEzTc92uVxaXpCuy/r1V3Jy8nnnUOIDAAAAnkWBDwAA4EHeVN6fMCRlskIio5WePsVDE/qfqqoqZc2YofhR96t9bE/T87e8t1QVJev09JzZF3zP+VqJX1FRoUOHDpk3HAAAAGAiCnwAAAAP8cbyXpICgx1KTM3RkiWLtXr1ag9M6X9mzsxW/bEG3Xjv46ZnNzYc1zvPZWjYsOEaPny4KZm+UuJ/9dVX6t69u5KTU9wwIQAAAHDhKPABAAA8wFvL+xP6J45WXL/BmvDQRDU2NrpxSv9TUVGhgucKdM3YdIVGdjA9/+MlL2r/l9uVm5tjaq4vlPgFBc+psbFRK1a8owMHDrhpSgAAAOD8UeADAAC4mbeX95JkGIZGTspT6eZNKiwsdNOU/unRxx6To22EhiRPMj27vq5GxS88oeSUFA0YMMD0fG8u8Y8cOaLnX3heV9xyt5qamvTGG2+4eVIAAADg3FHgAwAAuJEvlPcndOkbrytuHqtHpmWqurra5Cn908aNG7WgqEg33Pekghwhpud/8MrTOnrkoLKmTzc9+wRvLfHnz5+vmpoajfj1dHX/4Y0qWrDQA5MCAAAA54YCHwAAwE18qbw/ITE1WzW1tcrKmmHShP4tfUqGouJ66cpbf2F69pEDe/XBK7M14cEJio2NNT3/27ytxG9sbNSzefnqN+ynahfdRZeNGK0P3n9Pu3fv9uDEAAAAwNlR4AMAALiBL5b3khQW1VlD756q/Pw8lZWVmTCl/1qxYoVWvbtSI8bPMuVr879WvfCkWgcFKjNzmunZp+NNJf6bb76pih1f6Jr/O7ao7w2jFNCylV577TVPjgsAAACcFQU+AACAyXy1vD9hSMpkhURGKz19immZ/qapqUlp6VPU9fJrdOm1t5qev7/ic320+HllTpum8PBw0/PPxFtK/DlPP6NLBgxRzKVXSpJah7ZTr6t+xDE6AAAAsB0KfAAAABPZqbyXy6URD0w3fXd3YLBDiak5WrJksVavXm1qtr8oKirS5k0lumlCrgzDMD3/nXnT1KlTZ6WmppqefTZ2L/E/+ugjrVv7/3TVmIdP+Zx+iUla//FHKi8v9/S4AAAAwBlR4AMAAJjELuX9gql36Se33qoO0dF653fuOT6lf+JoxfUbrAkPTVRjY6NbruGrnE6npmU+qstuuEOx/Qabnr+zZJ1Kixcpe+YMBQcHm57fHHYu8Z959lldFHOJLh166jsffjD0xwpytNGrr77q8VkBAACAM6HABwAAMIGdyvvbb79dr732V82ZnavS4kUqX7/G9GsZhqGRk/JUunmTCgsLTc/3ZXPnztXu3ZUaMX6m6dkul0vLC9LVr//lGjNmjOn558KOJf4XX3yhv73+ugaPnqgWAQGnfGxgsEM/GPoTjtEBAACArRgul8tl9RDNtWHDBqtHAE5r4MCBVo8AALCQ3cr7hQsWqGXLlnK5XBo0+CpVHjyq8S9v+E5haYbXHh+nnR+9rfKy7QoLCzM939dUVVXpkm7d9IPhY3Rbxu9Mz/9szRK9nHa7VqxYoeHDh5uefz7s9OcjIjxctc5jyvjHLgU5Qr7zcf/+4O/6y8M/1ubNm9W3b1+PzwkAAAD8L3bgAwAAXAA7lZPfLu+lb3bJF+TnqXJbidYvm++WayemZqumtlZZWTPcku9rZs7MVv2xBt147+OmZzc2HNc7z2Vo2LDhtinvJXvtxK+uqdVVSQ+ftryXpB6DRqhN23AtXMgufAAAANgDO/ABE7ADHwD8k53L+29LGTtWb761QpMXbVdwSFvTZ1j1p+la8+J0bdmyRd27dzc931dUVFSoZ69euvaeTA37lfkF/r/+9gctzfmNNmzYoAEDBpief6Hs8OelqbHxrO9EeSPrV9pfUqwvysvc8oBhAAAA4FywAx8AAOA82KGMbE55L0mzsrPV4KzR6kL37JIfkjJZIZHRSk+f4pZ8X/HoY4/J0TZCQ5InmZ5dX1ej4heeUHJKii3Le8keO/Gbc4xU/8QkVez4Qh999JEHJgIAAAC+HwU+AADAOfKm8l6SYmJiNDUjQ2sX5unArnLTZwkMdigxNUdLlizW6tWrTc/3BRs3btSCoiLdcN+TZzy+5UJ88MrTOnrkoLKmTzc920x2KPHP5pIrrlVY+44cowMAAABboMAHAAA4B95W3p+QlpamqKgoLS9wzy75/omjFddvsCY8NFGNjY1uuYY3S5+Soai4Xrry1l+Ynn3kwF598MpsTXhwgmJjY03PN5vdS/wWAQHqO+ynWvjqX7mXAQAAYDkKfAAAgGby1vJekhwOh2bn5qi0eJHK168xfS7DMDRyUp5KN29SYWGh6fnebMWKFVr17kqNGD9LAc38ep2LVS88qdZBgcrMnGZ6trvYvcTvn5ikfXu/1nvvvWf1KAAAAPBzFPgAAADN4M3l/QlJSUmKTxikt56ZqCY37Czu0jdeV9w8Vo9My1R1dbXp+d6oqalJaelT1PXya3Tptbeanr+/4nN9tPh5ZU6bpvDwcNPz3cnOJX6XPvG6qHNXLVz4qtWjAAAAwM9R4AMAAJyFL5T30je75Avy81S5rUTrl813w5RSYmq2amprlZXlngfmepuioiJt3lSimybkyjAM0/PfmTdNnTp1VmpqqunZnmDXEt8wDPUdMVp/e+NvOnbsmNXjAAAAwI9R4AMAAHwPXynvT0hISFBySopWzsuUs+awiVN+Iyyqs4bePVX5+XkqKyszPd+bOJ1OTct8VJfdcIdi+w02PX9nyTqVFi9S9swZCg4ONj3fU+xa4l+emKRDBw9qxYoVVo8CAAAAP0aBDwAAcAa+Vt6fMCs7Ww3OGq0udM8u+SEpkxUSGa30dPc8MNdbzJ07V7t3V2rE+JmmZ7tcLi0vSFe//pdrzJgxpud7mh1L/Ojul6ljtz5asHCh1aMAAADAj1HgAwAAnIavlveSFBMTo6kZGVq7ME8HdpWbkvltgcEOJabmaMmSxVq9erXp+d6gqqpKWTNmKH7U/Wof29P0/C3vLVVFyTrNmZ1ryb3pDnYs8S8bkaSlS5eqrq7O6lEAAADgp3xjtQ8AAGAiXy7vT0hLS1NUVJSWF7hnl3z/xNGK6zdYEx6aqEY3PDDX7mbOzFb9sQbdeO/jpmc3NjRoxdypGjZsuIYPH256vpXsVuL3TxytutpaLVu2zNI5AAAA4L+a/Upww4YN7pwDAOBnXC6X6uvr5XQ6T/6/0+nUsWPH1NjYKJfLdfJ/ktSiRQsZhiHDMBQYGKigoCAFBwcrODj45H8HBARY/KuCL/CH8l6SHA6HZufmKDk5WeXr16jbldeZmm8YhkZOytPvfp6gwsJC/epXvzI1384qKipU8FyBrr0nU6GRHUzPX7/0Re3buU25i3zzaJfk5GRJ0rhx4yTJsj+HkhQZ000X9/mhFi58VT/72c8smQGewboEAADYleE6sQI5Cwp84MwGDhxo9QiA7dXX16u6ulqHDx8++eLYbC1btlRwcLBCQkIUFhamNm3ayDAM068D3+Uv5f0JLpdLgwZfpcqDRzX+5Q1q4Yay6bXHx2nnR2+rvGy7wsLCTM+3o5SxY/X3t9/VpEXbFeQIMTW7vq5GT9/eXbfePEIvv/SSqdl2Y4c/j5L0QdGzWvG7qdq3d6/atWtnyQwwH+sSAADgLSjwARNQ4APf5XK5VFtbq+rqah06dEhOp9PjM7Rs2VJhYWEKCwtT27Zt2QmH72WHstCT5f0JH374oQYNGqRRj76g+NvuNT2/el+lnrmjpx4cP16zZ+eanm83Gzdu1MCBA3XbI39Qwqj7TM9/9/kn9d6fZ2r7tm2KjY01Pd9u7PDn8vD+3cq+OUYvvvii7rnnHo9fH+ZgXQIAALwVBT5gAgp84Bsul+vkC+Pq6mo1NDRYPdJJhmEoNDRUYWFhioiI8EgxCu9hh5LQivL+hJSxY/XmWys0edF2BYe0NT1/1Z+ma82L07VlyxZ1797d9Hw7GTZ8hErLv9KEhZsVYPLX8MiBvZpzezelPvCA5syZbWq2ndnhz+cLv75eXdu10sqVKzx+bZw/1iUAAMAXUOADJqDAh79rbGzUf/7zH+3bt0/Hjh2zepyzMgxDkZGR6tChg4KDg60eBxazQzloZXkvSbt27VLPXr0Uf2eqfjQhx/T8Y846PXtnb1076EotXrzI9Hy7WLFihRITEzV2zhL1ue4npucvmfUbbV31qr4oL1d4eLjp+XZm9Z/TDxc9r6WzHtDu3bvVoYP5zzWAuViXAAAAX2LNQZIAAJ9w/PhxVVZWavPmzdq1a5dXvEiWvtmR95///EefffaZysvLVVNTY/VIsIjVpaBkfXkvSTExMZqakaG1C/N0YFe56fmBwQ4lpuZoyZLFWr16ten5dtDU1KS09Cnqevk1uvTaW03P31/xuT5a/Lwyp03zu/Je+ubBti+99JI2/uMvemP6vWpqavLo9fvecIeMFi30+uuve/S6ODesSwAAgC9iBz5gAnbgw98cPXpUe/fuVVVVlZr5z4jttWnTRtHR0QoLC+MBc36C8v5UdXV16tGzlyJ6xSsl9w3T810ul/74y6sV4qrVpxs/8bmzn19++WWNGzdODxSuU2y/wabnvzLlDh3avl7bt33u1zt0rfxz+5eJtyis8aD+uW6tx66J5mFdAgAAfBk78AEAzXbs2DGVl5dry5YtOnDggM+8SJak2trak7+26upqq8eBm1Hef5fD4dDs3ByVFi9S+fo1pucbhqGRk/JUunmTCgsLTc+3ktPp1LTMR3XZDXe4pbzfWbJOpcWLlD1zhl+X95K1O/H7jRitf/1znXbu3Omxa+L7sS4BAAD+gAIfAHBWLpdLe/fu1WeffaZDhw5ZPY5bOZ1OlZWV6YsvvtDx48etHgduQHl/ZklJSYpPGKS3npmopsZG0/O79I3XFTeP1SPTMn2qkJo7d652767UiPEzTc92uVxaXpCufv0v15gxY0zP90ZWlfiXXvsTtQoK1l//+lePXA9nxroEAAD4Ewp8AMD3qq2t1b///W/t2rXL42cOW+ngwYP67LPPtG/fPp/a0efvKO+/n2EYKsjPU+W2Eq1fNt8t10hMzVZNba2ysma4Jd/TqqqqlDVjhuJH3a/2sT1Nz9/y3lJVlKzTnNm5ltyvdmVFiR/UJlS9h/xYRQsWuv1aODPWJaxLAADwN7wKAACcVmNjo7788ktt3bpVR48etXocSzQ2Nuqrr77S559/rrq6OqvHwQWivG+ehIQEJaekaOW8TDlrDpueHxbVWUPvnqr8/DyVlZWZnu9pM2dmq/5Yg26893HTsxsbGrRi7lQNGzZcw4cPNz3f21lR4vdPTNKmkk+1detWt18Lp2JdwroEAAB/RYEPAPiOqqoqlZaWav/+/VaPYgvf3u3X6IZjReB+lPfnZlZ2thqcNVpd6J5d8kNSJiskMlrp6VPcku8pFRUVKniuQNeMTVdoZAfT89cvfVH7dm5Tbm6O6dm+Ijk5WdnZ2dqwbL6+2LDG7dfrddWP1DqkrRYuZBe+J7EuORXrEgAA/AsFPgDgpKamJlVUVGjHjh1qaGiwehzb2bt3r7Zu3Sqn02n1KDgHlPfnLiYmRlMzMrR2YZ4O7Co3PT8w2KHE1BwtWbJYq1evNj3fUx597DE52kZoSPIk07Pr62q06vnfKjklRQMGDDA935d8snGj2nfppkuuuNbt12oVFKxLrx+logULOcbEA1iXfD/WJQAA+AcKfACAJOnYsWP6/PPPdeDAAatHsTWn06mtW7f61AM4fRnl/flLS0tTVFSUlhe4Z5d8/8TRius3WBMemuiVO0g3btyoBUVFuuG+JxXkCDE9/4NXntbRIweVNX266dm+5KuvvtLfXn9dg372kFoEBHjkmv1HjFZ52XZt3LjRI9fzV6xLmod1CQAAvo8CHwCgI0eO6N///jfnqTZTY2OjysrKtGfPHnZg2hjl/YVxOByanZuj0uJFKl+/xvR8wzA0clKeSjdvUmFhoen57pY+JUNRcb105a2/MD37yIG9+uCV2Zrw4ATFxsaanu9LnnvuOQU5QnTlrfd47JrdfnijQiPac4yOG7EuOTesSwAA8G0U+ADg5/bt26ft27fz1vTzsHv3bn3xxRdeuXvY11HemyMpKUnxCYP01jMT1eSG+7xL33hdcfNYPTIt06t2j65YsUKr3l2pEeNnKcANX9dVLzyp1kGBysycZnq2Lzly5Ij++Pzz+uHt97vlXRBnEtCypfrceJcWLHzVIw/O9TesS84f6xIAAHwTBT4A+KkT58p+9dVX7Na6AIcOHeL8WZuhvDePYRgqyM9T5bYSrV823y3XSEzNVk1trbKy3PPAXLM1NTUpLX2Kul5+jS699lbT8/dXfK6PFj+vzGnTFB4ebnq+L5k/f75qamo0+KepHr/25YlJ2l25S2vXrvX4tX0V6xJzsC4BAMD3UOADgB86fvw458qaiPNn7YPy3nwJCQlKTknRynmZctYcNj0/LKqzht49Vfn5eSorKzM932xFRUXavKlEN03IlWEYpue/M2+aOnXqrNRUz5fS3qSxsVHPPJunfsN+qnbRXTx+/Yv7XaXw6C5asIBjdMzAusRcrEsAAPAtFPgA4GdOvEjmXFlzNTY2qry8XIcOHbJ6FL9Fee8+s7Kz1eCs0epC9+ySH5IyWSGR0UpPd88Dc83idDo1LfNRXXbDHYrtN9j0/J0l61RavEjZM2coODjY9Hxf8uabb2pnxQ5dkzzJkuu3aNFClw0frddef13Hjx+3ZAZfwbrEPViXAADgOyjwAcCPnHiRXF9fb/UoPsnlcumLL77gxbIFKO/dKyYmRlMzMrR2YZ4O7Co3PT8w2KHE1BwtWbJYq1evNj3fLHPnztXu3ZUaMX6m6dkul0vLC9LVr//lGjNmjOn5vmbO08/okgFDFHPplZbN0D9xtKoO/EfFxcWWzeDtWJe4F+sSAAB8AwU+APgJXiR7Bi+WPY/y3jPS0tIUFRWl5QXu2SXfP3G04voN1oSHJtryAYxVVVXKmjFD8aPuV/vYnqbnb3lvqSpK1mnO7FxL7mFv8tFHH2nd2v+nq8Y8bOkcnXoNUFRsTy1cyDE654N1iWewLgEAwPvx6gAA/AAvkj2LF8ueQ3nvOQ6HQ7Nzc1RavEjl69eYnm8YhkZOylPp5k0qLCw0Pf9CzZyZrfpjDbrx3sdNz25saNCKuVM1bNhwDR8+3PR8X/PMs8/qophLdOlQ8x8ifC4Mw9BlI5K0aPFiHhh6jliXeBbrEgAAvJvhcrlczfnADRs2uHsWwGsNHDjQ6hGAM+JFsnUMw9All1yidu3aWT2KT6K89zyXy6VBg69S5cGjGv/yBrUICDD9Gq89Pk47P3pb5WXbFRYWZnr++aioqFDPXr107T2ZGvYr8wv8D9/4o5bMekAbNmzQgAEDTM/3JXV1dWrXrp3aRseq9zUjFd4xTuGduiqic1dFdOqqoDahHp1nf8XnevrO3nrjjTc0atQoj17bW7EusQ7rEgAAvBMFPmACCnzYFS+SrceLZfegvLfOhx9+qEGDBmnUoy8o/rZ7Tc+v3lepZ+7oqQfHj9fs2bmm55+PlLFj9fe339WkRdsV5AgxNbu+rkZP395dt948Qi+/9JKp2b5qzpw5WrWqWOU7dujLnRWq/9bu9zZhEYro3FXtOnZVeKeuCu8Up4hOXRXeuavCO8apVZD5Dweem3KFEvp2099ef930bF/DusR6rEsAAPA+FPiACSjwYUcNDQ3aunUrL5JtwDAMde/eXW3btrV6FJ9AeW+9lLFj9eZbKzR50XYFh5h/X6/603SteXG6tmzZou7du5uefy42btyogQMH6rZH/qCEUfeZnv/u80/qvT/P1PZt2xQbG2t6vq9zuVzau3evduzYoR07dqiiokI7duzQFzt26IsvdmjXV1+qoaHh5MeHXRSt8E5d1a7T/xT8nbqqXXQXBbRsdc4zvPfSbBU//7j27d3L3/Pfg3WJfbAuAQDAu1DgAyagwIfduFwubd++XUeOHLF6FPyfgIAA9e7dW8HB5u/+9CeU9/awa9cu9ezVS/F3pupHE3JMzz/mrNOzd/bWtYOu1OLFi0zPPxfDho9QaflXmrBwswJM/lofObBXc27vptQHHtCcObNNzcY3GhoatHv37pMF/4n/fbHjm6J/z+5KnXg5ZLRooYjoLmrXMe5kwR/xrZI/tH2n0/6dc+jrLzXrlli99NJLGjt2rKd/iV6BdYn9sC4BAMB7UOADJqDAh918+eWX2r9/v9Vj4H8EBwerd+/eCnDDueH+gPLeXp566ilNz5qhh1/fosiYbqbnf/r2Qr366BgVFxfr+uuvNz2/OVasWKHExESNnbNEfa77ien5S2b9RltXvaovyssVHh5uej7Orr6+Xl9++eUpu/d37Nih8i+++f8D//nvv6UtWwUqolOswqL/e+7+iV38S2ber/7dOuutt/5h4a/GvliX2BPrEgAAvAMFPmACCnzYyX/+8x/t3LnT6jFwBmFhYerWrZsMw7B6FK9CeW8/dXV16tGzlyJ6xSsl9w3T810ul/74y6sV4qrVpxs/8XjB1NTUpMsHXKGaFqG674X3Tf8zu7/icz37sz7KmTVLaWlppmbDPLW1tacU+yeK/vIvdqiiYocOV1ef/NiL2kdp79d7LPn7yc5Yl9gb6xIAAOzPv195AoCPqamp0Zdffmn1GPge1dXV2r17tzp37mz1KF6D8t6eHA6HZufmKDk5WeXr16jbldeZmm8YhkZOytPvfp6gwsJC/epXvzI1/2yKioq0eVOJHihc55Zi651509SpU2elpqaang3ztGnTRn369FGfPn1O+/MHDx48WfBffPHFlPf/g3WJ/bEuAQDA/tiBD5iAHfiwg2PHjunf//73KQ/rg3117dpVERERVo9he5T39uZyuTRo8FWqPHhU41/eoBZu2CX/2uPjtPOjt1Vetl1hYWGm55+O0+lUj569FN7jh0rO/Zvp+TtL1un3v7xaL7/8slJSUkzPB+yAdYl3YV0CAIB9sUUEAHxAU1OTysvLeZHsRXbu3Km6ujqrx7A1ynv7MwxDBfl5qtxWovXL5rvlGomp2aqprVVW1gy35J/O3LlztXt3pUaMn2l6tsvl0vKCdPXrf7nGjBljej5gB6xLvA/rEgAA7IsCHwB8AC+6vM+JcuP48eNWj2JLlPfeIyEhQckpKVo5L1POmsOm54dFddbQu6cqPz9PZWVlpuf/r6qqKmXNmKH4UferfWxP0/O3vLdUFSXrNGd2LsetwGexLvE+rEsAALAvXjUAgJfbt2+fqqqqrB4D5+HYsWPasWOHmnmand+gvPc+s7Kz1eCs0epC9+ySH5IyWSGR0UpPn+KW/G+bOTNb9ccadOO9j5ue3djQoBVzp2rYsOEaPny46fmAHbAu8V6sSwAAsCcKfADwYk6nU7t27bJ6DFyAI0eOaN++fVaPYRuU994pJiZGUzMytHZhng7sKjc9PzDYocTUHC1ZslirV682Pf+EiooKFTxXoGvGpis0soPp+euXvqh9O7cpNzfH9GzADliXeD/WJQAA2A8FPgB4KZfLxS4pH1FZWamjR49aPYblKO+9W1pamqKiorS8wD275PsnjlZcv8Ga8NBENTY2uuUajz72mBxtIzQkeZLp2fV1NVr1/G+VnJKiAQMGmJ4PWI11ie9gXQIAgL1Q4AOAl9qzZw/ny/oIl8uliooKvy49KO+9n8Ph0OzcHJUWL1L5+jWm5xuGoZGT8lS6eZMKCwtNz9+4caMWFBXphvueVJAjxPT8D155WkePHFTW9OmmZwN2wLrEd7AuAQDAXijwAcAL1dXV6euvv7Z6DJiorq5Oe/bssXoMS1De+46kpCTFJwzSW89MVJMbdsl36RuvK24eq0emZaq6utrU7PQpGYqK66Urb/2FqbmSdOTAXn3wymxNeHCCYmNjTc8HrMa6xPf487oEAAC7ocAHAC/Drijf9fXXX/vd7kXKe99iGIYK8vNUua1E65fNd8s1ElOzVVNbq6ws8x6Yu2LFCq16d6VGjJ+lADd8/Ve98KRaBwUqM3Oa6dmA1ViX+C5/XJcAAGBHFPgA4GW+/vprziX1US6XSzt37vSbEoTy3jclJCQoOSVFK+dlyllz2PT8sKjOGnr3VOXn56msrOyC85qampSWPkVdL79Gl157qwkTnmp/xef6aPHzypw2TeHh4abnA1ZjXeK7/G1dAgCAXVHgA4AXcTqdvJ3Zx9XV1Wnv3r1Wj+F2lPe+bVZ2thqcNVpdaN4u+W8bkjJZIZHRSk+/8AfmFhUVafOmEt00IVeGYZgw3anemTdNnTp1VmpqqunZgNVYl/g+f1mXAABgZxT4AOAl2AXlP3bv3q36+nqrx3AbynvfFxMTo6kZGVq7ME8HdpWbnh8Y7FBiao6WLFms1atXn3eO0+nUtMxHddkNdyi232ATJ/zGzpJ1Ki1epOyZMxQcHGx6PmAl1iX+w9fXJQAA2B0FPgB4iaqqKtXU1Fg9BjzA5XLpq6++snoMt6C89x9paWmKiorS8oIL3yV/Ov0TRyuu32BNeGiiGs/zgblz587V7t2VGjF+psnTffPneHlBuvr1v1xjxowxPR+wGusS/+HL6xIAALwBBT4AeIGmpiZVVlZaPQY8qLq6WkeOHLF6DFNR3vsXh8Oh2bk5Ki1epPL1a0zPNwxDIyflqXTzJhUWFp7z51dVVSlrxgzFj7pf7WN7mj7flveWqqJknebMzrXkXgfciXWJ//HFdQkAAN6CVxMA4AX27dun48ePWz0GPGzXrl0+czQB5b1/SkpKUnzCIL31zEQ1necu+e/TpW+8rrh5rB6Zlqnq6upz+tyZM7NVf6xBN977uOlzNTY0aMXcqRo2bLiGDx9uej5gNdYl/smX1iUAAHgTCnwAsLmGhgZ9/fXXVo8BC9TV1engwYNWj3HBKO/9l2EYKsjPU+W2Eq1fNt8t10hMzVZNba2yspr/wNyKigoVPFega8amKzSyg+kzrV/6ovbt3Kbc3BzTswGrsS7xX76yLgEAwNtQ4AOAze3Zs+e8z3eG96usrFRTU5PVY5w3ynskJCQoOSVFK+dlyllz2PT8sKjOGnr3VOXn56msrKxZn/PoY4/J0TZCQ5InmT5PfV2NVj3/WyWnpGjAgAGm5wNWY13i37x9XQIAgDeiwAcAG6uvr9f+/futHgMWOnbsmP7zn/9YPcZ5obzHCbOys9XgrNHqwubvkj8XQ1ImKyQyWunpZ39g7saNG7WgqEg33Pekghwhps/ywStP6+iRg8qaPt30bMBqrEvgzesSAAC8FQU+ANhYZWUlZ43CK3c7Ut7j22JiYjQ1I0NrF+bpwK5y0/MDgx1KTM3RkiWLtXr16u/92PQpGYqK66Urb/2F6XMcObBXH7wyWxMenKDY2FjT8wGrsS6B5J3rEgAAvBkFPgDYVG1tLeeMQpL3nTdMeY/TSUtLU1RUlJYXnH2X/Pnonzhacf0Ga8JDE89YLK1YsUKr3l2pEeNnKcAN98SqF55U66BAZWZOMz0bsBrrEpzgbesSAAC8HQU+ANhUZWWl1SPARvbu3atjx45ZPcZZUd7jTBwOh2bn5qi0eJHK168xPd8wDI2clKfSzZtUWFj4nZ9vampSWvoUdb38Gl167a2mX39/xef6aPHzypw2TeHh4abnA1ZjXYJv85Z1CQAAvoACHwBsqLq6WkeOHLF6DNiIy+XSnj17rB7je1He42ySkpIUnzBIbz0zUU1uOH6hS994XXHzWD0yLVPV1dWn/FxRUZE2byrRTRNyZRiG6dd+Z940derUWampqaZnA1ZjXYL/5Q3rEgAAfAUFPgDY0N69e60eATZ04MABHT9+3OoxTovyHs1hGIYK8vNUua1E65fNd8s1ElOzVVNbq6ys/z4w1+l0alrmo7rshjsU22+w6dfcWbJOpcWLlD1zhoKDg03PB6zGugSnY+d1CQAAvoQCHwBspq6ujl1uOC2Xy6V9+/ZZPcZ3UN7jXCQkJCg5JUUr52XKWXPY9PywqM4aevdU5efnqaysTJI0d+5c7d5dqRHjZ5p+PZfLpeUF6erX/3KNGTPG9HzAaqxLcCZ2XZcAAOBrKPABwGbY5Ybvs3//fjU1NVk9xkmU9zgfs7Kz1eCs0erCGWf/4PMwJGWyQiKjlZ4+RVVVVcqaMUPxo+5X+9iepl9ry3tLVVGyTnNm51py/wPuxroE38du6xIAAHwRrzIAwEaOHTumgwcPWj0GbKyxsVEHDhywegxJlPc4fzExMZqakaG1C/N0YFe56fmBwQ4lpuZoyZLFeuCBB1R/rEE33vu46ddpbGjQirlTNWzYcA0fPtz0fMBqrEtwNnZalwAA4Kso8AHARvbv3y+Xy2X1GLC5vXv3Wn6fUN7jQqWlpSkqKkrLC6a4Jb9/4mh17j1Ar//tb7pmbLpCIzuYfo31S1/Uvp3blJubY3o2YAesS9AcdliXAADgyyjwAcAmmpqatH//fqvHgBeor6/X4cPmnx3eXJT3MIPD4dDs3ByVFi9S+fo1pucbhqE24R3Uum2EhiRPMj2/vq5Gq57/rZJTUjRgwADT8wGrsS5Bc1m9LgEAwNdR4AOATRw4cECNjY1WjwEvYdWZxJT3MFNSUpLiEwbprWcmqsnkv/8qt27U9n+9o8TfzFCQI8TUbEn64JWndfTIQWVNn256NmAHrEtwLnhWAgAA7kOBDwA24HK5eOGDc3LkyBHV1dV59JqU9zCbYRgqyM9T5bYSrV8239Tst5/L0EWxvXTlrb8wNVeSjhzYqw9ema0JD05QbGys6fmA1ViX4FxZsS4BAMBfUOADgA1UV1ervr7efUoAjgABAABJREFU6jHgZTxZrlDew10SEhKUnJKilfMy5awx5wiGbf9aoe0frtRNqbMU4Ib7ZNULT6p1UKAyM6eZng3YAesSnA++6QMAgHtQ4AOADezbt8/qEeCFDh48qOPHj7v9OpT3cLdZ2dlqcNZodeGMC85qamrS8oIpirv8Gl167a0mTHeq/RWf66PFzytz2jSFh4ebng/YAesSnA9PrUsAAPA3FPgAYLH6+nodOXLE6jHghVwulw4cOODWa1DewxNiYmI0NSNDaxfm6cCu8gvK+nR5kfZsK9GPJuTKMAyTJvyvd+ZNU6dOnZWammp6NmAHrEtwvjyxLgEAwB9R4APwGg0NDUpOGac//OEPVo9iqqqqKqtHgBdz5/1DeQ9PSktLU1RUlJYXTDnvjOP1Tq34/aPqe8Mdiu032MTpvrGzZJ1Kixcpe+YMBQcHm54P2AHrElwI7h8AAMxHgQ/AazQ2NmpB0ct6ZNo0HTp0yOpxTMMLHVyIo0eP6ujRo6bn7t69W+PGjdMPrr2N8h4e4XA4NDs3R6XFi1S+fs15Zfzztbk6vL9SieNnmjucvtlZurwgXf36X64xY8aYng/YBesSXAh3rUsAAPBnFPgAvM6hgweVm5tr9RimqKurk9PptHoMeDl3lC2dOnXSoMFX6eih/ZT38JikpCTFJwzSW89MVFNj4zl9bl11lYoLZyh+1P1qH9vT9Nm2vLdUFSXrNGd2riV/JgBPYF0CM/BNIAAAzMWrDwBeJ6LzJXo2L0979uyxepQLxgscmKGqqkoul8v03LTJk/TFxg/01Wcfm579fSjv/ZdhGCrIz1PlthKtXzb/nD53zZ+z1dTYoBvvfdz0uRobGrRi7lQNGzZcw4cPNz0fsAvWJTDD/2fv7uNqvv//gT9O104liVyU1cYwjDaUNldj1djms7GNFGHGKGZ0pVzrQrWLJNmYXGbG5vpjVhIz5iIS1odki8mGiaicOJ3z+2Nf/dhCF6/T+31Oj/vt9rnNjXMe76d93qvX+9nrPF+6WpcQERHVV2zgE5He6Tn8YxibWmDu3HlSl1IrWq2WD8okxN27d1FSUiI8d9CgQXB++hkcWPe58OxHYfOe3Nzc4OPri7SkcKiKb1XpPYWX83FgfQJ6jwiCtV0z4TVlbl2OqxdyERsbIzybSC64LiFRdLUuISIiqq/YwCcivdPAuhF6j5qOr75ahnPnzkldTo3dvn0b9+7dk7oMMhC6aLoYGxvj4ykf4VT6Rtz883fh+f/E5j3dtyA6GmpVMTKSI6v0+rQlM6Fs2Bi9fKYKr6WstBjpS2fDx9cXL7zwgvB8IrnguoRE4g+DiIiIxGEDn4j00kvvBcDarjlmzJwpdSk1xgcbEklXH1cfPXo0LC0t8fOGROHZD2Lznh7k6OiI0JAQHPg6HtcvnX/sawvOZOHErhS8On4uzJVWwmvZv/ZT3Ll9AxHz5wvPJpITrktIJI7RISIiEocNfCLSS6YWDdBv3Bxs+OYbHD9+XOpyqk2j0eDmzZtSl0EGpLy8HEVFRcJzra2tMe6DcTi6+UuUlRYLzwfYvKfKBQYGwt7eHt8nBD/2dbsWhaCJUzt0GzRGeA23r1/B/rVxmDxpMpycnITnE8kF1yUkmq7WJURERPURG/hEpLe6vjEKzZzbITR0utSlVFtRURHKy8ulLoMMjK52T06ePAllpcXI3Fa9Q0Wrgs17ehSlUom42Bic3rMJ5zP3Vvqa3EOpOHc4Da8FLICxDu6d9GVz0cDcDOHhYcKzieSE6xLSBX6qg4iISAw28IlIbxmbmODVCZFIS0vFnj17pC6nWvhAQ7pw8+ZNnTRgWrVqhXfefRc/r4+HRmA+m/f0JN7e3nB164Gdn035172n0WjwfUIwnF16okOfQcKvfS3/LI5sXorwsDDY2toKzyeSE65LSBd0tS4hIiKqb9jAJyK91qnfYDzVsTtCQqfrzZxNrVaLW7duSV0GGSCtVoviYt2MuZn68cf469Kv+N/+7ULy2LynqlAoFEhYGI+C3Gxkbn/4EyAnvk/BH7nZGDA5FgqFQvi1f0gKQ8uWDggICBCeTSQnXJeQruhyXUJERFSfsIFPRHpNoVDAM2ABMo8ewebNm6Uup0qKi4uh0WikLoMMlK7mzbq6uuKll3viQMpntc5i856qw83NDT6+vkhLCoeq+O8m470yFVKXzECnfkPg1Nld+DUvZB/E6T2bEB0VCQsLC+H5RHLCdQnpEufgExER1R4b+ESk99p074e2PTwROj0MarVa6nKeiLvcSJd0eX8FTpuKX7P24/dfjtY4g817qokF0dFQq4qRkRwJAPh5QyJuXSuAl3+U8GtptVp8nxCEzl1cMHz4cOH5RHLDdQnpEu8vIiKi2mMDn4gMgldANM7lnsWqVaukLuWJ+CBDulRWVoaysjKdZA8aNAjOTz+DA+s+r9H72bynmnJ0dERoSAgOfB2PS/87hj3JkXAdPB5NndoKv1bOvq3Izz6IT+JiYWTEpTIZPq5LSJd0uS4hIiKqL/hUQkQGwaH9i+jsMRQzZ83GnTt3pC7nkdRqNUpLS6UugwycrpoxxsbG+HjKRzi5ewNu/vl7td7L5j3VVmBgIOzt7bE+3BuacjX6j50l/BrlajV2LgzCqx4e8PDwEJ5PJDdcl1Bd4A+JiIiIaocNfCIyGJ4T5uPq1StYvHix1KU8Eh9gqC7o8j4bPXo0rKys8POGxCq/h817EkGpVCI4KBDXfz+P3iOCYG3XTPg1Mrcux/Xf8+Deo4fwbCI54rqE6gLvMyIiotphA5+IDEaTp55Ft/+MRWRUFG7evCl1OZXiAwzVhdu3b0Or1eok29raGuM+GIejm79EWWnxE1/P5j2JdPRoJpSN7NDLZ6rw7LLSYqQtnY3GDs8gackXPHiR6gWuS6gu6HJdQkREVB+wgU9EBqX/2JkovaNCXFyc1KVUig/KVBfKy8tRUlKis/zJkyehrLQYmdtWPPZ1bN6TSFlZWVi3LgWeEyJgrrQSnr9/7ae4c+sGhkWsQ3FJCSIiIoVfg0huuC6huqDrdQkREZGhYwOfiAxKw6Yt8ZL3FHz2+ef4448/pC7nIXfu3MG9e/ekLoPqCV02ZVq1aoV33n0XP6+Ph6a8vNLXsHlPogUFh8DeuR26DRojPPv29Sv4cU0cXh42GU8974befqFYuDAeeXl5wq9FJBdcl1Bd4g+LiIiIao4NfCIyOH1GBsPY1ALz5s2XupSH8MGF6pKu77epH3+Mvy79ipwft/3rz9i8J9FSU1ORvjsNnv4LYKyD+yl92VwYm5qh7+gwAEAv32mwsmuOoKBg4dcikguuS6gu8X4jIiKqOTbwicjgNLBuhN6jpuOrr5bJavckH1yoLpWUlECtVuss39XVFS+93BMH133+0O+zeU+iaTQaBAYF42mXnujQZ5Dw/Gv5Z3Fk81K8MjoMyoa2AAAzCyW8AmKwZctmZGRkCL8mkRxwXUJ1SdfrEiIiIkPGBj4RGaSX3guAVeNmmDFzptSlAAC0Wi2Ki5984CeRSLqeNxs4bSp+zdqP3385CoDNe9KNlJQUnDqZjdcmx0KhUAjP37U4DA2bOsD9vYCHfr+L1zA4d3bH5I+moPwRo6KI9BXXJSQFzsEnIiKqGTbwicggmVo0QL9xc/DN+vU4fvy41OWgrKwMGo1G6jKoniktLdVp/qBBg+D89DM4sO5zNu9JJ1QqFcLCZ+D5fkPg1NldeP6F7IP4JWMTvCZGwtTc4qE/UygUeH1qPE6fOonk5GTh1yaSEtclJAVdr0uIiIgMFRv4RGSwur4xCs2c22H69DCpS+GOI5KEru87Y2NjfDzlI5zcvYHNe9KJxMREXL5cAE//KOHZWq0WOxcGoUVbF3R5bXilr2nVyRUvDhyB6WHhKCoqEl4DkVS4LiEp8L4jIiKqGTbwichgGZuY4NUJkUhN/UHyGcbccURSqIv7bvTo0Xixaze89957bN6TUIWFhYiIjITr4PFo6tRWeH7Ovq24cPIgBk6OhZHRo5fEXgHRKC4pQUREpPAaiKTCdQlJgfcdERFRzbCBT0QGrVO/wXiqY3cEh4RCq9VKVgcfWEgK9+7dw71793R6DWtraxw9fAjrUlLYvCehoqKiUXZXjf5jZwnPLlersWtRKJ7t4Ylne3g89rU29g7o7ReKhQvjZXUwOlFtcF1CUqiLdQkREZEhYgOfiAyaQqGAZ8ACZB49gs2bN0tSg1ar5YMySYYfVyd9lJ+fj4RFCeg5IgjWds2E52duXY6/LuZiwKSYKr2+l+80WNk1R1BQsPBaiOoa1yUkJa5LiIiIqo8NfCIyeG2690PbHp6YHhYOtVpd59dXqVQ8KI4kwyYN6aMZM2dC2bAxevlMFZ5dVlqMtKWz4TLAFy3buVTpPWYWSngFxGDLls2Sj2Qjqi2uS0hKXJcQERFVHxv4RFQveAVEI/fsGaxatarOr80HFZIS7z/SN1lZWViXkoJ+4+bCXGklPH//2k9x59YNeE6YX633dfEaBufO7pj80RSUl5cLr4uorvD7AkmJ9x8REVH1sYFPRPWCQ/sX0dljKGbNnoM7d+7U6bX5UWGSEu8/0jdBwSGwd26HboPGCM++ff0KflwTh5eHTYZtC6dqvVehUOD1qfE4feokkpOThddGVFf4fYGkxPuPiIio+tjAJ6J6w3PCfFy58icWL15cp9flTiOSklqtxt27d6Uug6hKUlNTkb47DZ7+C2Csg0OR05fNhbGpGfqODqvR+1t1csWLA0dgelg4ioqKBFdHVDe4LiEpcV1CRERUfeKfjIiozp05cwb9+r+K27dvSV2KTmm1WgCAiZlFjd7f5Kln0e0/YxEZFYWxY8eiUaNGAqurHA+KIzkoLS2FmZmZ1GUQPZZGo0FgUDCedumJDn0GCc+/ln8WRzYvxWsBC6BsaFvjHK+AaHw25DtEREQiLi5WYIVEusd1CckB1yVERETVwwY+kQHIzMzEH5cLMGBSDIyMDfs/a3OrhujYb3CN39//g1nI+u8qxMXFITIyUmBllVOpVBU/eCCSSmlpaZ38wIqoNlJSUnDqZDYmJB+EQqEQnr9rcRgaNnWA+3sBtcqxsXdAb79QLFw4H+PHj0ObNm0EVUike1yXkBxwXUJERFQ9ht3pI6pnXvb+CCZm5lKXIWsNm7TAS95T8NnnnyMgIAAtWrTQ6fVUKpVO84mqgvchyZ1KpUJY+Aw8328InDq7C8+/kH0Qv2RswtB5a2BqXrNPcT2ol+80ZG5ZhqCgYGzevElAhUR1g98PSA54HxIREVUPZ+ATUb3TZ2QwjE0tMG/efJ1fizM+SQ54H5LcJSYm4vLlAnj6RwnP1mq12LkwCC3auqDLa8OFZJpZKOEVEIMtWzYjIyNDSCbpBnebP4zfD0gOeB8SERFVDxv4RFTvNLBuhN6jpuOrr5YhLy9Pp9cqKyvTaT5RVfA+JDkrLCxERGQkXAePR1OntsLzc/ZtxYWTBzFwciyMjMQtfbt4DYNzZ3dM/mgKysvLheWSGGq1Gi/36oM33ngTt24Z9hlB1cHvByQHvA+JiIiqhw18IqqXXnovAFaNm2HGzJk6vQ4fUEgO1Go1G4wkW1FR0Si7q0b/sbOEZ5er1fhvfCAcO3THsz08hGYrFAq8PjUep0+dRHJystBsqr3y8nIc/OlH7Nz5X7i69dD5D+z1BdclJAdclxAREVUPG/hEVC+ZWjRAv3Fz8M369Th+/LjOrsMHZZIL3oskR/n5+UhYlICeI4JgbddMeH7m1uUovPQryoqLoNFBs6hVJ1e8OHAEpoeFo6ioSHg+1V6/92egsFQNVzc37NmzR+pyJMfvBSQXvBeJiIiqjg18Iqq3ur4xCs2c22H69DCd5Gu1Ws74JNnggzLJ0YyZM6Fs2Bi9fKYKzy4rLUb60tl4bcBruHYxF5nbVwi/BgB4BUSjuKQEERGROsmn2mnq1A4TVh5G02e7wtPTE0lJSVKXJBmuS0hOuC4hIiKqOjbwiajeMjYxwasTIpGa+oNODiG8d+8eD88j2WDThuQmKysL61JS0G/cXJgrrYTn71/7Ke7cvoEvliyBj68v0pLCoSoWPwvdxt4Bvf1CsXBhPMe0yJSyoS38Fu5Ej/cC4O/vjw8/nIB79+5JXVad47qE5ITrEiIioqpjA5+I6rVO/QbjqY7dERwSKvyhljuLSE54P5LcBAWHwN65HboNGiM8+/b1K9i/Ng6TJ02Gk5MTFkRHQ60qRkaybnbJ9/KdBiu75ggKCtZJPtWesYkJ3pwWj8EzlmF58nJ4eHji+vXrUpdVp/h9gOSE9yMREVHVsYFPRPWaQqGAZ8ACZB49gs2bNwvN5oMJyQnvR5KT1NRUpO9Og6f/AhibmAjPT182Fw3MzRAe/veINEdHR4SGhODA1/G4fum88OuZWSjhFRCDLVs26+QTXSSO61tj8X5SOo6dPI1u3V3xyy+/SF1SneH3AZIT3o9ERERVxwY+EdV7bbr3Q9senpgeFg61Wi0slw8mJCe8H0kuNBoNAoOC8bRLT3ToM0h4/rX8sziyeSnCw8Jga2tb8fuBgYGwt7fH9wm62SXfxWsYnDu7Y/JHU1CugwNzSZynX+iFiauOoszEEj3c3bFjxw6pS6oT/D5AcsL7kYiIqOrYwCciwt+HEOaePYNVq1YJy+SDCcnJ3bt3OfuYZCElJQWnTmbjtcmxUCgUwvN/SApDy5YOCAgIeOj3lUol4mJjcHrPJpzP3Cv8ugqFAq9PjcfpUyeRnJwsPJ/EatzSGR8uPwinF/th0KBBiImJMfivkVyXkJxwXUJERFR1bOATEQFwaP8iOnsMxazZc3Dnzh0hmTyci+REq9XWy0MbSV5UKhXCwmfg+X5D4NTZXXj+heyDOL1nE6KjImFhYfGvP/f29oarWw/s/GwKNDrYJd+qkyteHDgC08PCUVRUJDyfxDJXWsEnbhP6jg5DaGgoRowcCZVKJXVZOsN1CckJ1yVERERVxwY+EdH/8ZwYgStX/sTixYuF5Ikcx0MkAu9JklpiYiIuXy6Ap3+U8GytVotdi4LRuYsLhg8fXulrFAoFEhbGoyA3G5nbVwivAfj7E13FJSWIiNDNgbkklpGREbwmRsA78mts2PgtevXugz/++EPqsnSC3wNIbnhPEhERVQ0b+ERE/6dJqzbo9p+xiIyKws2bN2udxxnIJDe8J0lKhYWFiIiMhOvg8Wjq1FZ4fs6+rfjtxAF8EhcLI6NHL3Hd3Nzg4+uLtKRwqIpvCa/Dxt4Bvf1CsXBhPPLy8oTnk2508RqG8cv2I+/CJXTt1h2ZmZlSlyQcvweQ3PCeJCIiqho28ImIHtD/g1kovaNCXFxcrbO4q4jkhg/KJKWoqGiU3VWj/9hZwrPL1WqkJobCw8MTHh4eT3z9guhoqFXFyEjWzS75Xr7TYGXXHEFBujkwl3TDsUM3TFydCdPGDujZqxe++eYbqUsSiusSkhuuS4iIiKrGROoCiIjkpGGTFnjJewo+j49HQEAAWrRoUaMcPpCQHIlo3uzduxfHjx8XUA3VJ4WFhYhfGI9X3p8Ja7tmwvMzty7H1Qu5eH7w6/jss8+q9J7evXohbe2nUBgbw9KmifCanF7ojS1bUjBx4kS0adNGeD49Xk2/3jVs0gIffLkPmyI+wLBhw3Dq1GnMmzf3sZ/q0Adcl5Ac8YdKREREVcMGPhHRP/QZGYyj332BefPmY8mSpBpl8EGZ5Ki29+Xdu3fh6ekJKIxgbGomqCqqD+7dVcHCqhF6+UwVnl1WWowfksJhZGKCpC+XVeu9xmbmOLg+AQodNWdNLSyxfMVqGBnrd/NXXzVq5gCHDt2q/T5Tcwu8N281mrXuhKio6fjll1+wZs1qWFlZ6aDKusF1CckR70siIqKqYQOfiOgfGlg3Qu9R0/FVUhimTZtao52T3FFEclTb+1Kr1eLevXsYOm8NXhjoK6gqMnQFZ7KQOKIrPCdGwlwpvgG6f+2nKCu9jcBN52Dbwkl4PtVPCoUCfUeFwP6ZDtgwYzjcX3oZ27dthbOzs9Sl1QjXJSRHvC+JiIiqhtuBiIgq8dJ7AbBq3AwzZs6s0fu5o4jkiPclSWHXohA0cWqHboPGCM++ff0KflwTh5eHTWbznnSiQ+83MWHFIfx5oxjdunfH/v37pS6pRvj1n+SI9yUREVHVsIFPRFQJU4sG6DduDr5Zvx5ZWVnVfj8fSEiOeF9SXcs9lIpzh9PwWsACGJuI/+Bn+rK5MDY1Q9/RYcKzie5r1rojJqw8jEbOndC/f3989dVXUpdUbfz6T3LE+5KIiKhq2MAnInqErm+MQrOn2yM0dHq138uPBJMc8b6kuqTRaPB9QjCcXXqiQ59BwvOv5Z/Fkc1L8croMCgb2grPJ3qQZaMmGJOYihcHvY8PPvgAH300Ra++pupTrVR/8L4kIiKqGjbwiYgewdjEBB4TIpGa+gMyMjKq9V7uKCI54n1JdenE9yn4IzcbAybHQqFQCM/ftTgMDZs6wP29AOHZRJUxNjHF29OX4D8hi5G4OBEDBgzEjRs3pC6rSvj1n+SI9yUREVHVsIFPRPQYHV95G091ckVwSCi0Wm2V38cdRSRHfFCmunKvTIXUJTPQqd8QOHV2F55/IfsgfsnYBK+JkTA1txCeT/Q47u9OxJjEVPx8JBOubj1w9uxZqUt6Iq5LSI64LiEiIqoaNvCJiB5DoVDAK2ABMo8ewZYtW6r8Pj6QSGvOnDno1q0b5syZI3UpssIGDtWVnzck4ta1Anj5RwnP1mq12JkQjBZtXdDlteHC84mqok33fpi4+iiKy43h6uaGH374QeqSHovrkrqzfft2dOvWDW+++abUpcge1yVERERVI/40MSIiA9O62yto28MTodPD8Oabb8KkCgcxVme3fl348ssvsWzZsn/9vqmpKWxsbNCmTRu8+uqreOONN6r09yP9JLf7kgxTaVEh9iRHwnXweDR1ais8P2ffVlzIPoD3E1NhZMS9KCQdO8fW+DD5EL6ZMRwDBw7Ep59+io8++kgnI6NqS45f/7VaLdLT07Fr1y6cOXMGN27cgJGRERo3bowmTZqgY8eOeOGFF9C9e3dYWVlJXS7pgBzvSyIiIjniUw8RURV4BUQj9+wZrF69ukqvl/MDiZ2dXcX/jI2N8ddff+HQoUOIiIjAmDFjcOvWLalLrLUmTZrAyckJTZo0kboUWZHzfUmGY+/KaGjK1eg/dpbw7HK1GrsWheLZHp54toeH8Hyi6rKwaogRn25FT59p+Pjjj/H++2NRVlYmdVn/Irev/7dv38b48eMRGhqKvXv34s8//4RarYaZmRn+/PNPZGdnY926dQgKCqr2OUSkP+R2XxIREckVt1kSEVWBQ/sX0dljKGbOmg1vb280aNDgsa+X8wPJPz/m/+eff2L58uXYvHkzcnJyEBcXh/nz50tUnRgBAQEICODBlkR1rfByPg6sT8ArY8JhbddMeH7m1uX462IuvKPWC88mqikjY2MM/CgWzVp3wprID3Dm7Fls2bwJ9vb2UpdWQW7rklmzZuH48eMwNjaGt7c3Bg8eDEdHRxgZGUGtVuO3337DwYMHZT+aiIiIiKgucAc+EVEVeU6MwJUrfyIpKUnqUoRq3rw5wsPD4erqCgDYvXs3SktLJa6KdEFuDRwyPGlLZkLZsDF6+UwVnl1WWoy0pbPhMsAXLdu5CM8nqq2ub4zEuKX78MvZPHTt1h3Z2dlSlyRLFy9exP79+wEAEyZMwJQpU/DUU09VjMQyMTHBs88+Cz8/P6xbtw4eHvy0jaHiuoSIiKhquAOfiKiKmrRqg27/GYvIqCiMHTsWNjY2j3ytPj6Q9OjRA0eOHMG9e/dw8eJFtG/f/qE/Lysrw+bNm7Fnzx6cP38eJSUlsLGxQadOnTBkyBC89NJLj80/ffo0vvvuO2RlZeGvv/6CsbEx7O3t0alTJ3h5eaFHjx6Vvm/v3r3Yvn07fvnlF9y8eRMNGjRAmzZt4OXlhbfeeqvSmf1z5szBjh078MYbb1QcZFtYWIgBAwagvLwcn376Kfr06fPIWpcsWYLly5fD0dGx0sOLz5w5g2+++QbHjx/HX3/9BSMjIzg6OqJXr14YPnw4GjVq9K/33D+H4MUXX8TSpUuRnp6OTZs2ITc3Fzdv3sTYsWMxfvz4x/47rC19vC9JfxScycKJXSl4a/oXMFeKn1e9f+2nuHPrBjwn6PcnhMiwPfV8D0xcdRRrg96C+0svIWXtWrz99ttSlyWrr/+5ubkVv37c9+L7LCwsKv39S5cuYd26dThy5AiuXLkCjUaDFi1awN3dHT4+PmjevPm/3rN9+3bMnTsXLVq0wPbt2/G///0Pq1atQlZWFm7dugV7e3v06dMHY8eORcOGDR9Z06lTp7By5UqcOHECKpUKzZo1Q//+/TF69Ogq/BsAiouL8c033+DHH3/ExYsXoVKp0LhxY3Tp0gXe3t54/vnn//Wey5cvY9CgQQCAbdu2QaPRYNWqVTh8+DCuXbuGJk2aYPv27VW6vlzI6b4kIiKSMzbwiYiqof8Hs3Bi52rExsYiMjJS6nKEevAhSqPRPPRnFy9exJQpU3Dx4kUAgEKhgKWlJa5fv459+/Zh3759eOeddxAaGvqv3PLycnz++edYv/7/j7xo0KABysvL8dtvv+G3335DRkYG9u7d+9D7SktLER4eXrFLDwAsLS1RXFyMrKwsZGVlYefOnYiPj3/sQ/Z9jRs3hru7O3766Sfs3LnzkU0DrVaLXbt2AQAGDhz4rz//8ssv8dVXX1X8+7KwsIBarca5c+dw7tw5bNu2DfHx8f/6AciDPv/8c6SkpEChUMDa2poHcZJB2LUoBE2c2qHboDHCs29fv4If18Th5WGTYdvCSXg+kUiNmrfCuGX78e3c0Rg8eDA2bdokiya+HF25cgVPP/10td+3efNmxMTEQK1WAwDMzMygUCiQn5+P/Px8bNu2DTExMY/cHAAAu3btwpw5c6BWq2FlZYXy8nIUFBRg3bp1OHToEFauXAmlUvmv923duhWRkZEVayUrKyv88ccfWLFiBTIyMp74//Xp06cxbdo0XL9+HQBgbGwMCwsLXLlyBampqUhLS8PEiRMf+8OAkydPIioqCqWlpbCwsKh0MwMREREZDn6nJyKqJkPdLXTo0CEAfzfnW7ZsWfH7t2/fRkBAAC5fvozu3btj3Lhx6NixI8zMzFBcXIytW7fiyy+/xLfffgsnJyd4e3s/lLt48eKK5v2gQYPg5+cHJ6e/G3CFhYU4efJkpTNuZ82ahf3796NVq1YYP348evXqBUtLS5SVleHQoUP47LPPcPLkScybNw+ffPJJlf6Or7/+On766Sfs378ft2/fhrW19b9ek52djYKCAgD/buCvW7cOy5Ytg6WlJUaPHo033ngDTZo0QXl5OXJzc5GQkICjR49i2rRp2LhxY6UP/mfOnMHx48cxcuRIjBgxAra2trh7927FgzyRPso9lIpzh9Mw4pMtMNZBIyl92VwYm5qh7+gw4dlEumKo64Xa6tChAxQKBbRaLeLj4xETE1OxLqiKvXv3IjIyEiYmJhg1ahSGDBlSsdv+woUL+OKLL7B7926EhITgm2++qXQn/o0bNzBv3jy88cYbGDt2LJo3bw6VSoVt27bhs88+w6+//orVq1fjww8/fOh9Z86cQVRUFDQaDbp27Yrp06fD2dkZarUa6enpWLBgAb766qtH1n758mVMmjQJt2/frtix36ZNG5iYmKCwsBAbNmzAihUrsHjxYjz99NPo27dvpTlRUVF45plnEBwcjA4dOlT83YmIiMgwccsfEVE1pC+bB0tlAwQHB0tdijB//vknIiMjcfToUQBAr169HhoBk5ycXNG8X7RoEV544QWYmZkB+HvXmY+PD+bOnQsAWL58ecVuOODvh8m1a9cCAEaOHIlZs2Y99JDeuHFj9O3bF9HR0Q/V9NNPP2Hv3r2ws7PDl19+iddeew2WlpYAAHNzc/Tp0wdLly5FgwYNsHfvXpw9e7ZKf9fevXvDysoKd+/eRVpaWqWv+e9//wsAcHFxgaOjY8Xv37x5E0lJSVAoFIiLi8OoUaPQpEkTAH/vnnvuueewaNEiPPfcc7hy5Uqlo3eAvz9Z4OPjg8mTJ8PW1hbA3zsHW7RoUaW/A5HcaDQafJ8QDGeXnujQZ5Dw/Gv5Z3Fk81K8MjoMyoa2wvOJRLv55+9YOrYnzv+8k7vvK9GyZUu89dZbAIC8vDy888478PHxQUxMDLZu3Yq8vLxH/vDj3r17iI2NBQBMnz4dAQEBaNGiBRQKBRQKBZydnbFgwQL07t0bJSUlSElJqTRHpVLB09MTM2bMqGjwW1hY4L333sPQoUMBoNLNBUlJSSgvL8dTTz2FhQsXwtnZGcDfc/u9vLwQFRWF27dvP/LvvnDhQty+fRsDBw5ETEwM2rdvX7F7vnHjxvjwww8xefJkAMDSpUsfmWNjY4OkpKSK5j2Aav0QhIiIiPQLG/hERFX018VzOLplGcLDwh47/x74exe7XHl5eVX8r2fPnnjjjTewefNmAICzs/NDY3C0Wi22bdsGAPDx8XnkR7T79u0LS0tL3Lx5E2fOnKn4/R07dkCj0cDGxqZa893vN78HDhwIe3v7Sl/TrFkzdOvWDQDw888/VynX3Nwcr776KgBg586d//rzu3fvYvfu3RXXftD3338PlUqF5557ruLA33+6/wAP/P9PNPyTkZER/Pz8qlSvaHK+L0l/nfg+BX/kZmPA5Fid3GO7FoehYVMHuL8XIDybSLSLpw4hya87UHIdBw8ckE3zXm5f/0NCQjB27Fg0aNAAWq0WZ8+excaNGzF//nwMGzYMXl5e+Oyzz/716bQDBw7g6tWrsLOzq5gHX5nXX38dwOPXB++//36lv39/xN7vv/8OlUpV8fu3b9+u+N4+cuTISmfzu7u7o3PnzpXmFhUVISMjAwAwatSoJ9aem5v7yE/nvffee5V+yk/fyO2+JCIikiuO0CEiqqLUJTPRvHkL+Pv7S11KrTzqYfD1119HWFgYzM3NK37v119/RVFREQBg7ty5j53VfufOHQDAH3/8gU6dOgH4e0YrALi5uT2U+yQnTpwA8PeM28oa7fcVFxcD+PtTBFX1+uuvY8uWLRWjchwcHCr+7P5oHTMzM3h4eFRa0/nz5yua9JW5/7D/xx9/VPrnjo6OaNy4cZXrFYkPyiTavTIVUpfMQKd+Q+DU2V14/oXsg/glYxOGzlsDU/PKD7IkkotjO1Zjc+QHcHV1xeZN3z3yB9D09w+8P/zwQ/j6+uLHH3/E8ePHkZOTg99++w337t1DYWEh1q1bV3HWzf11RXZ2NgDg1q1beO211x6Zf+/ePQCP/l5sY2ODVq1aVfpnTZs2rfj1rVu3Khr1Z86cqZh7f38DQWW6detWsf550KlTpyreP2HChEe+/0F//PEH7Ozs/vX7Xbp0qdL75Y7rEiIioqphA5+IqAoKzhzHybRvsHz58kp3XP2TnB9IMjMzAfy9u/7+IbSJiYn473//i9atW2PkyJEVr7127VrFr2/cuFGl/Ad3q93/YUF1xsOo1WrcvHkTwN8N+vtN+qpe80lcXFzg4OCAgoICfP/99xg7dmzFn93/YUHv3r3/NR///r+LsrIylJWV1bgmqZr3RLrw84ZE3LpWAC//ykdS1YZWq8XOhGC0aOuCLq8NF55PJIqmvBy7EkPx45pPMGbM+1iyJKli1JxcyHVdYmVlhYEDB1Z86q2srAwnTpzA+vXrsX//fty8eRMhISHYtGkTzM3NK74X37t3r0pnxzzq+/Xjdq8bGxtX/PrBsYCFhYUVv37cD2ce9WcPrqmqeu4N1xJEREQEsIFPRFQlPyROR9t27R9qbj+OXB+UH6RQKNCkSRMMGTIETk5OmDBhQsUM9+7duwNAxU4x4O9ZsJXtAqvqtaqqvLy84tdRUVHw9PSs0TUfV8uAAQPw1VdfYefOnRUN/Js3b+LAgQMA/v/H1x90/9/FkCFDMH369Bpf/3GfYtA1fbgvSX+UFhViT3IkXAePR1OntsLzc/ZtxYXsA3g/MVXS/26IHkdVfAvrw72R+/MuxMfHY/LkybL8WivHmipjbm4ONzc3uLm5Yc6cOdixYweuXLmCn3/+GX379q1YI7z00ktISEiQuNrquV+7ubl5xXqjpgzla6K+3JdERERSM4zv/EREOpR3dA9yD6ViQXTUI2fA/5O+PZB069YNAwcOhFarRWxsbMVD5oMN+7y8vGrn3j/k9fLly1V+j7m5OaysrGp8zaq436C/ePEiTp06BQBIS0uDWq2Gra0t3N3/PQrk/r8LXdVUF/TtviR527syGppyNfqPnSU8u1ytxq5FoXi2hyee7eHx5DcQSeCv3/PwxZgeuHz6AHbu3ImPPvpItl9n5VrX4zx4fkB+fj6A/7+ukOJ78YO73q9evfrI1z240/5B92svKyvD77//LrY4PaWP9yUREZEU2MAnInoMrVaL1MXT0d3VDW+99VaV3/fgx6/1xQcffABjY2P89ttv2LFjBwCgdevWsLS0BACkpqZWO/P+QW6HDx+u0tiZ++7Pdt29e/dDnwIQpVWrVhW13R+bc/+fXl5elf6g5n5Np0+ffuRMXbmr6g+giJ6k8HI+DqxPQO8RQbC2ayY8P3Prcvx1MRcDJsUIzyYSIe/oHizxc4WVcTmOHD782LNR5EAf1yUPjrm5P5Lo/vfiq1evVpxNU1fat29fsfP9/jjCyhw9erTS3+/cuXNFw/qHH34QX6Ae4rqEiIioatjAJyJ6jF8yNuPi6SOIjVlQrV1C+vhA4ujoWHFw6/Lly6FWq2FiYoJBgwYBAHbs2PHEh+X7B97e9+abb8LY2BhFRUX48ssvq1zL/V13Fy9exOrVqx/72jt37lQcVlcd9+ftpqam4tdff63YiV/Z+Jz7rzc3N0d5eTliYmIeGvXzTxqNBrdv3652Tbqmjw0ckqe0JTOhbNgYvXymCs8uKy1G2tLZcBngi5btXITnE9XWzxuTkBzgiZfcuuPI4UNo166d1CU9kZzWJQUFBbhw4cITX3d/MwHwd/McAHr16lWxk/2TTz554hk4/1yX1Ia1tTV69OgBAFi7dm2lGxMOHz5c6QG2wN87+Pv06QMAWLNmzRP/HYisXa64LiEiIqoaNvCJiB6hXK1GWlIYPD290Ldv32q9V18fSEaNGgWFQoHLly9jy5YtAICxY8fC0dER5eXlmDRpEtauXfvQgbbFxcU4ePAgZs+ejQ8++OChvFatWmHEiBEAgNWrV2P+/Pm4ePFixZ/fuHEDqampCAwMfOh9ffv2xSuvvAIASExMRHR09EMPuvfu3cPp06eRkJCAN95446GD5arK09MTpqamKCoqwpw5cwAATz/9NJ577rlKX9+kSRNMmjQJAPDTTz/B398fJ06cqGjka7Va5OfnY+3atRg6dCj2799f7Zp0TV/vS5KXgjNZOLErBa+OnwtzpZXw/P1rP8WdWzfgOWG+8Gyi2ihX38Pm6AnYGuOPSQGTsHPnf2Frayt1WVUip6//v/76K95991189NFH2LFjx0Nj9tRqNc6cOYO5c+ciJSUFANCxY0e4uLgA+HvMXmhoKBQKBc6cOYMxY8bg559/fugH+QUFBfjuu+8wcuRIbNy4UWjtH374IYyNjZGfn48pU6ZUjPZRq9VIS0vD9OnTYW1t/cj3T5kyBTY2NigpKcHYsWOxdetWFBcXV/z5zZs3sWfPHgQFBSE8PFxo7XIkp/uSiIhIzuSzFYOISGaO7ViJK/lnsWDT19V+r5x2ulVHmzZt0Lt3b+zbtw8rVqzAoEGDYGNjg8WLFyMoKAi5ubmIj49HfHw8rK2todFoUFJSUvH+Vq1a/StzwoQJKCkpwcaNG7F161Zs3boVSqUSGo2mYufc/Zn3D5o/fz7mzZuH1NRUfPfdd/juu+/QoEEDmJqaori4+KHROjWZodqwYUP07NkTGRkZyMnJAfDo3ff3DRs2DHfv3sXixYuRmZmJsWPHwtTUFEqlEiUlJVCr1bWqSdf09b4kedm1KARNnNqh26AxwrNvX7+CH9fE4eVhk2Hbwkl4PlFNldz8C+tC3sHFkwfx1Vdf4f3335e6pGqR09d/ExMTaDQaHDhwoOIw1/vfS2/dugWtVlvx2vbt2+OTTz556NDWvn37Yt68eYiMjERubi4mTZoEY2NjWFlZ4c6dO7h7927Fa+/veBelQ4cOCAkJQXR0NI4ePYp33nkHVlZWuHv3Lu7evQtnZ2e8/fbb+Pzzzyt9v6OjIxYvXozg4GBcvnwZ8+fPR0REBKytraFWq1FaWlrxWldXV6G1y5Gc7ksiIiI543dMIqJK3FPdwZ6lczB02DC88MIL1X6/Pu8oGjNmDPbt24crV65g06ZNGDZsGBwcHLB69Wr88MMP2L17N/73v//h5s2bMDY2hoODA9q2bYtevXqhd+/e/8ozNjZGSEgIvLy88N133yErKwuFhYUwNzdHy5Yt8fzzz1c6O9jCwgJRUVEYPHgwtm3bhuzsbPz1118oLS2Fra0tnnnmGbi7u+OVV16Bvb19jf6ur7/+OjIyMgAARkZGGDBgwBPfM3LkSLzyyivYuHEjjh49isuXL6O4uBiWlpZwdHREt27d0LdvXzz//PM1qkmX9Pm+JHnIPZSKc4fTMOKTLTDWQeMlfdlcGJuaoe/oMOHZRDV15fwvWD31TaCsGHv27EHPnj2lLqna5PT1393dHZs3b8aBAwdw4sQJnD9/HlevXsXt27dhYWGBpk2bol27dnjllVfw6quvPtS8v2/AgAHo3r07Nm7ciJ9//hm///47iouL0aBBAzg7O8PFxQV9+/bFiy++KLz+wYMHo02bNlixYgVOnjwJlUqF5s2bo3///hg1ahT27Nnz2Pe3b98eGzZswLZt27B3716cO3cOt27dgqmpKZ566il06NABvXv3xssvvyy8drmR031JREQkZwrtg1scHuPYsWO6roVIb3Xt2lXS669duxYjRoxAxEEVTMzMJa3FUOxbHYe0pDD873//Q5s2bar9/tLSUvzvf//TQWVENde8eXM4ODjU+P1lZWWwsLDA0Hlr8MJAX4GVkT7QaDRY5PsizJXWGL/sR+GfMrmWfxafD+2I1wIWoPeIwCe/gagO5Py4HRtmDEeb1s9gx/ZtcHLSz0+GcF1CclTbdQkREVF9wR34RET/cOf2Tfy4Mhpjx35Qo+Y9wI8EkzzV9r5UKBQwNTXFpoix2BozUVBVpC/K793DvbsqTEg+qJMRUbsSQ6FQGCF92Vzs+Wqe8HzRtFotNBoNjIyMAR1MzNJqtdBqNDA2MgJkOJJLH1hYNcSYxbth79y+2u/VarXYtyoGPywOw3/+8xbWrFld6bg3fcF1CckR70siIqKq4XdMIqJ/2Lc6Fhp1GWbNmlnjDH4kmOSotvelmZkZUlNTcfz4cUEVkb64d+8eFsTEwrHnQDh1dheefyH7IH7ZuwXDhw+X/FNtVXX37l1ERy+A0v4pdH1jlPB8LbT4+ZtFMNfexbRpUysdI0KPplarERISgoKczGo38O+VqbApYiyyvk/BjBkzMXfuHL3/9891CckR70siIqKqYQOfiOgBt/76Awe/jkfg1I/RokWLGufwgYTkSMROt759+6Jv3761L4b0yieffIJbt4rg5R8tPFur1WLXomB07uKCNWvW6FWj9KmnnoKPjw9atn8Rrbv1FZ7/tEsvLB7lBhsbG3zwwQfC8w1ZWVkZQkJCqv2+W9cuY23Q27iSdxLr16/H0KFDdVBd3eO6hOSIO/CJiIiqRn+ekIiI6kD6snmwVDZAcHBwrbP4UEJywwYO1URhYSEiIiPhOng8mjq1FZ6fs28rfjtxAJ/ExepV8x4AvL294erWAzs/mwJNebnw/FadXPHiwBGYHhaOoqIi4fn0sEs5mUjy6457hQU48NNPBtO8v4/rEpIbrkuIiIiqRr+ekoiIdOivi+dwdMsyhE2fDhsbm1rn8aGE5Ib3JNVEVFQ0yu6q0X/sLOHZ5Wo1UhND4eHhCQ8PD+H5uqZQKJCwMB4FudnI3L5CJ9fwCohGcUkJIiIidZJPf8v+YT2+/KAXnnVuhWOZR/VmlFN18HsAyQ3vSSIioqphA5+I6P+kLpmJ5s1bwN/fX0ged7qR3PCepOrKz89HwqIE9BwRBGu7ZsLzM7cux9ULuYiNjRGeXVfc3Nzg4+uLtKRwqIpvCc+3sXdAb79QLFwYj7y8POH59Z1Go8EPi8Pxdbg3hr73Ln7ct7dWI/TkjN8DSG54TxIREVUNG/hERAAKzhzHybRvMG/uHDRo0EBIppmZmZAcIhEUCgVMTU2lLoP0zIyZM6Fs2Bi9fKYKzy4rLUb60tnw8fWFi4uL8Py6tCA6GmpVMTKSdbNLvpfvNFjZNUdQUO3Hu9H/V1ZyGylBg7F3ZTRiYmKwetUqWFhYSF2WznBdQnLCdQkREVHVsYFPRATgh8TpaNuuPfz8/IRlmpubC8siqi0zMzMoFAqpyyA9kpWVhXUpKeg3bi7MlVbC8/ev/RR3bt9AxPz5wrPrmqOjI0JDQnDg63hcv3ReeL6ZhRJeATHYsmUzMjIyhOfXR4WX8/Hl2Jdx4fgebNu2DcHBwQb/NZLrEpITrkuIiIiqjg18Iqr38o7uQe6hVERHRQr9KC8flElOeD9SdQUFh8DeuR26DRojPPv29SvYvzYOkydNhpOTk/B8KQQGBsLe3h7fJ+hml3wXr2Fw7uyOyR9NQbkODsytT349/iOS/LrDTF2CQz//jDfeeEPqkuoEvw+QnPB+JCIiqjo28ImoXtNqtUhNDEW37q54++23hWbzwYTkhPcjVUdqairSd6fB038BjHUwozh92Vw0MDdDeHiY8GypKJVKxMXG4PSeTTifuVd4vkKhwOtT43H61EkkJycLz68vjmxehuUT+6Nr507IPHoEHTt2lLqkOsPvAyQnvB+JiIiqjg18IqrXTu/ZhIu/HEVszALhH+PlgwnJCe9HqiqNRoPAoGA87dITHfoMEp5/Lf8sjmxeivCwMNja2grPl5K3tzdc3Xpg52dToNHBLvlWnVzx4sARmB4WjqKiIuH5hqxcrca2Tz7Cpshx+GDsB0hLS4WdnZ3UZdUpfh8gOeH9SEREVHVs4BNRvVWuVmP3knB4enrhlVdeEZ5vamrK2Z4kGzy8kKoqJSUFp05m47XJsTr5GvZDUhhatnRAQECA8GypKRQKJCyMR0FuNjK3r9DJNbwColFcUoKICN0cmGuISm/dwKqPBuLwxsVISkrCkiVJ9fLwTK5LSE64LiEiIqo6NvCJqN46tmMlruSfxYIF0TrJVygUfDgh2eBON6oKlUqFsPAZeL7fEDh1dheefyH7IE7v2YToqEhYWFgIz5cDNzc3+Pj6Ii0pHKriW8Lzbewd0NsvFAsXxiMvL094vqG5mn8GS0a54dq5Y0hLS8OECROkLkkyXJeQnHBdQkREVHVs4BNRvXRPdQd7ls7B0GHD8MILL+jsOnw4IbngvUhVkZiYiMuXC+DpHyU8W6vVYteiYHTu4oLhw4cLz5eTBdHRUKuKkZGsm13yvXynwcquOYKCdHNgriHJSI5EY6UJjh45opNP2+kbfi8gueC9SEREVHVs4BNRvXRwQyKKC68gYv58nV6HDyckByYmJjA2Npa6DJK5wsJCRERGwnXweDR1ais8P2ffVvx24gA+iYuFkZFhL0EdHR0RGhKCA1/H4/ql88LzzSyU8AqIwZYtm5GRkSE83xAYGxvj5V59MHDg6zhy+BBat24tdUmywHUJyQHXJURERNVj2E9PRESVuHP7Jn5cGY2xYz9AmzZtdHotPiiTHPA+pKqIiopG2V01+o+dJTy7XK1GamIoPDw84eHhITxfjgIDA2Fvb4/vE3SzS76L1zA4d3bH5I+moFwHB+bqOxMTE/z0417s2LEdDRs2lLoc2eD3A5ID3odERETVwwY+EdU7+1bHQqMuw6xZM3V+Lc6aJTngfUhPkp+fj4RFCeg5IgjWds2E52duXY6rF3IRGxsjPFuulEol4mJjcHrPJpzP3Cs8X6FQ4PWp8Th96iSSk5OF5xsKHtr6MH4/IDngfUhERFQ9JlIXQETiHPh6IYyMDfs/a3Orhug2aEyNxy/c+usPHPw6HoFTP0aLFi0EV/dvhnpII+kX3of0JDNmzoSyYWP08pkqPLustBjpS2fDx9cXLi4uwvPlzNvbGwsTFmHnZ1Pgv+YYjASPjGjVyRUvDhyB6WHheO+992BjYyM0nwwPvx+QHPA+JCIiqh7D7vQR1RPdunVDi5YO2L8yQupSdEqr1aKkuBgNrBrh+VffqVFG+rJ5UDawQFBQkODqKmdhYQGFQgGtVlsn1yOqjFKplLoEkrGsrCysS0nBW9O/gLnSSnj+/rWf4s7tGzo/c0SOFAoFEhbGo0ePHsjcvgKub40Vfg2vgGh8NuQ7REREIi4uVng+GRauS0gOuC4hIiKqHjbwiQxA+/btcbngktRl6FxZWRksLCygvquq0fv/ungOR7csQ8yCBWjUqJHY4h5BoVBAqVSipKSkTq5HVBk+KNPjBAWHwN65HboNGiM8+/b1K9i/Ng6TJ02Gk5OT8Hx94ObmBh9fX2xLCkfnV9+DhZXYeew29g7o7ReKhQvnY/z4cTo/24X0G9clJAdclxAREVUPZ+ATUb2RumQmmjdvAX9//zq9Lh9SSEomJiacNUuPlJqaivTdafD0XwBjE/H7OtKXzUUDczOEh4cJz9YnC6KjoVYVIyM5Uif5vXynwcquOYKCdHNgLhkWrktISlyXEBERVR8b+ERUL1z63zGcTPsG8+bOQYMGDer02paWlnV6PaIH8f6jR9FoNAgMCsbTLj3Roc8g4fnX8s/iyOalCA8Lg62trfB8feLo6IjQkBAc+Doe1y+dF55vZqGEV0AMtmzZjIyMDOH5ZFj4fYGkxPuPiIio+tjAJ6J6IXVxGNq2aw8/P786vzZ3upGUeP/Ro6SkpODUyWy8NjkWCoVCeP4PSWFo2dIBAQEBwrP1UWBgIOzt7fF9gm52yXfxGgbnzu74aMrHKC8v18k1yDDw+wJJifcfERFR9bGBT0QGL+/oHuQeSkV0VCRMdDAi4kksLCxgZMQvtyQNPihTZVQqFcLCZ+D5fkPg1NldeP6F7IM4vWcToqMiYWFhITxfHymVSsTFxuD0nk04n7lXeL5CocDrU+Nx6mQ2kpOTheeT4eC6hKTEdQkREVH1ceVGRAZNq9UiNTEU3bq74u2335akhvsHxhFJQZcfVddoNPDxHYkPPhiH0tJSnV2HxEtMTMTlywXw9I8Snq3VarFrUTA6d3HB8OHDhefrM29vb7i69cDOz6ZAo4Nd8q06ueLFgSMwPSwcRUVFwvPJMHBdQlLiCB0iIqLqYwOfiAza6T2bcPGXo4iNWaCTERFVxQdlkoKpqSlMTU11ln/gwAGsS1mDFStXoFt3V+Tk5OjsWiROYWEhIiIj4Tp4PJo6tRWen7NvK347cQCfxMVyl+8/KBQKJCyMR0FuNjK3r9DJNbwColFcUoKICN0cmEuGgesSkoKu1yVERESGik9VRGSwytVq7F4SDk9PL7zyyiuS1sIHZZKCru+7DRs2olEzB0xam4UbKi26de+O1atX6/SaVHtRUdEou6tG/7GzhGeXq9VITQyFh4cnPDw8hOcbAjc3N/j4+iItKRyq4lvC823sHdDbLxQLF8YjLy9PeD4ZBq5LSAq874iIiGqGDXwiMljHdqzElfyzWLAgWupS+HFhkoSux+ds/PZbdHhlCJq36YSJq46gQ//34Ofnh9Gjx3Ckjkzl5+cjYVECeo4IgrVdM+H5mVuX4+qFXMTGxgjPNiQLoqOhVhUjI1k3u+R7+U6DlV1zBAXp5sBc0n9cl5AUeN8RERHVDBv4RGSQ7qnuYM/SORg6bBheeOEFqcuBubk5R0lQndPlTrcDBw7gyp9/oPOr7wEAzBpY4t3ZK/DO7BVYt349R+rI1IyZM6Fs2Bi9fKYKzy4rLUb60tnw8fWFi4uL8HxD4ujoiNCQEBz4Oh7XL50Xnm9moYRXQAy2bNmMjIwM4fmk/7guISlwBz4REVHNcNVGRAbp4IZEFBdeQcT8+VKXAuDvucdWVlZSl0H1jC53ut0fn/NUZ/eHfr/bm6Pgv+ooR+rIUFZWFtalpKDfuLkwV4r/erR/7adQFd+UzddduQsMDIS9vT2+T9DNLvkuXsPg3NkdH035GOU6ODCX9BvXJSQF7sAnIiKqGTbwicjg3Ll9Ez+ujMbYsR+gTZs2UpdToWHDhlKXQPWIpaUlTExMdJL94PicynZwNmvdkSN1ZCgoOAT2zu3QbdAY4dm3r1/B/rVxmBQwCU5OTsLzDZFSqURcbAxO79mE85l7hecrFAq8PjUep05mIzk5WXg+6T+uS6gu6XJdQkREZOjYwCcig7NvdSw06jLMmjVT6lIewgdlqku6vN/+OT6nMhypIy+pqalI350GT/8FMNZBAyV92Vw0MDdDeHiY8GxD5u3tDVe3Htj52RRodLBLvlUnV7w4cASmh4WjqKhIeD7pN65LqC7xfiMiIqo5NvCJyKDcunYZB7+Ox8dTpqBFixZSl/OQBg0awNTUVOoyqJ7Q5YPyo8bnVIYjdaSn0WgQGBSMp116okOfQcLzr+WfxZHNSxEeFgZbW1vh+YZMoVAgYWE8CnKzkbl9hU6u4RUQjeKSEkRE6ObAXNJfXJdQXWIDn4iIqObYwCcig5L+1XwoG1ggKChI6lIqxYcXqgvGxsY6mzP7pPE5leFIHWmlpKTg1MlsvDY5FgqFQnj+D0lhaNnSAQEBAcKz6wM3Nzf4+PoiLSkcquJbwvNt7B3Q2y8UCxfGIy8vT3g+6TeuS6gu6HJdQkREVB+wgU9EBuOvi+dwdMsyhIeFoVGjRlKXUyk+KFNdsLa21kmjFqja+JzKcKSONFQqFcLCZ+D5fkPgVIVPTFTXheyDOL1nE6KjImFhYSE8v75YEB0NtaoYGcm62SXfy3carOyaIyhINwfmkv7iuoTqgi7XJURERPUBG/hEZDBSl8xE8+Yt4O/vL3Upj8QHZaoLchmfUxmO1KlbiYmJuHy5AJ7+UcKztVotdi0KRucuLhg+fLjw/PrE0dERoSEhOPB1PK5fOi8838xCCa+AGGzZshkZGRnC80l/cV1CdYH3GRERUe2wgU9EBuHS/47hZNo3mDd3Dho0aCB1OY9kYmICpVIpdRlk4HT1oFyT8TmV4UidulFYWIiIyEi4Dh6Ppk5thefn7NuK304cwCdxsbW6H+hvgYGBsLe3x/cJutkl38VrGJw7u+OjKR+jXAcH5pJ+4rqE6gIb+ERERLVjInUBRLXVtWtXqUsgGUhdHIa27drDz89P6lKeqGHDhmxWks6Ym5vD3NxcJ9n3x+e8Xc3xOZW5P1Ln6Rf7YF3MRBw+cgTfbtyADh06CKiUACAqKhpld9XoP3aW8OxytRqpiaHw8PCEh4eH8Pz6SKlUIi42Bj4+PjifuRetu/UVmq9QKPD61HgsHuWG5ORkfPDBB0LzSX9xXUK6pMt1CRERUX3B7VJEpPfyju5B7qFUREdFwsRE/j+X5C4k0iU5j8+pDEfq6EZ+fj4SFiWg54ggWNs1E56fuXU5rl7IRWxsjPDs+szb2xuubj2w87Mp0Ohgl3yrTq54ceAITA8LR1FRkfB80k9cl5Au8f4iIiKqPTbwiUivabVapCaGolt3V7z99ttSl1MlVlZWHDdBOmNjY6OTXI1Gg2+/+w4d+70j/P7lSB3xZsycCWXDxujlM1V4dllpMdKXzoaPry9cXFyE59dnCoUCCQvjUZCbjcztK3RyDa+AaBSXlCAiQjcH5pL+4bqEdElX6xIiIqL6hCs1ItJrp/dswsVfjiI2ZgEUCoXU5VSJQqHgbiTSCYVCASsrK51kHzx4EH/+cRnP939XJ/n3R+q8M3sF1q1fj27dXZGTk6OTaxm6rKwsrEtJQb9xc2GuFH8/7F/7KVTFNxExf77wbALc3Nzg4+uLtKRwqIpvCc+3sXdAb79QLFwYj7y8POH5pH+4LiFd0eW6hIiIqD5hA5+I9Fa5Wo3dS8Lh4eGJV155RepyqqVx48ZSl0AGqFGjRjA2NtZJ9jffbBA+PqcyHKlTe0HBIbB3bodug8YIz759/Qr2r43DpIBJcHJyEp5Pf1sQHQ21qhgZybrZJd/Ldxqs7JojKEg3B+aS/uG6hHRBl+sSIiKi+oQNfCLSW8d2rMSV/LNYsCBa6lKqzcbGhg80JJyuGjC6HJ9TGY7UqbnU1FSk706Dp/8CGOvgTJD0ZXPRwNwM4eFhwrPp/3N0dERoSAgOfB2P65fOC883s1DCKyAGW7ZsRkZGhvB80j9cl5Au8AdDREREYrCBT0R66Z7qDvYsnYP3hg7Fiy++KHU51WZkZIRGjRpJXQYZEGNjY53NmdX1+JzKcKRO9Wk0GgQGBeNpl57o0GeQ8Pxr+WdxZPNShIeFwdbWVng+PSwwMBD29vb4PkE3u+S7eA2Dc2d3fDTlY5Tr4MBc0i9cl5BoulyXEBER1Tds4BORXjq4IRHFhVcQGREhdSk1xl1JJFLjxo11dg5EXY3PqQxH6lRdSkoKTp3MxmuTY3VyL/yQFIaWLR0QEBAgPJv+TalUIi42Bqf3bML5zL3C8xUKBV6fGo9TJ7ORnJwsPJ/0D9clJJIu1yVERET1DRv4RKR37ty+iR9XRuP998eiTZs2UpdTY9bW1jA1NZW6DDIQhjI+pzIcqfNkKpUKYeEz8Hy/IXDSwQ9aLmQfxOk9mxAdFQkLCwvh+VQ5b29vuLr1wM7PpkCjg13yrTq54sWBIzA9LBxFRUXC80m/cF1CIvEHQkREROKwgU9EeuendZ+j/J4Ks2fPkrqUWlEoFHy4ISHMzMxgaWmpk2wpxudUhiN1Hi8xMRGXLxfA0z9KeLZWq8WuRcHo3MUFw4cPF55Pj6ZQKJCwMB4FudnI3L5CJ9fwCohGcUkJIiJ0c2Au6Q+uS0gUXa5LiIiI6iM28IlI7xQW/IqPp0xBixYtpC6l1vigTCIY6vicynCkzr8VFhYiIjISroPHo6lTW+H5Ofu24rcTB/BJXKxkn8Koz9zc3ODj64u0pHCoim8Jz7exd0Bvv1AsXBiPvLw84fmkX7guIRE4PoeIiEgsPoURkd5pZGuL4GDdHOpX15RKJcdRUK0Z8vicynCkzsOioqJRdleN/mPFfyqpXK1GamIoPDw84eHhITyfqmZBdDTUqmJkJOtml3wv32mwsmuOoCDD+N5KNcd1CYnAHwQRERGJJZ+ncSKiJzA2NoaP70hER0WhUaNGUpcjDB9yqDYaNGiABg0a6CS7oKAARUU3cf5wGq78Kq9xNRyp87f8/HwkLEpAzxFBsLZrJjw/c+tyXL2Qi9jYGOHZVHWOjo4IDQnBga/jcf3SeeH5ZhZKeAXEYMuWzcjIyBCeT/qF6xKqDV2uS4iIiOorNvCJSG+YmJhg7ZpV+PDDD6UuRSg+KFNt6PL+adWqFTKPHkUjcyDJrzuO7Vils2vVVH0fqTNj5kwoGzZGL5+pwrPLSouRvnQ2fHx94eLiIjyfqicwMBD29vb4PkE3u+S7eA2Dc2d3fDTlY5Tr4MBc0h9cl1Bt8P4hIiISjw18IiKJmZubw9raWuoySA8pFArY2dnp9BodOnRA5tEj8B46FBvnjMLGuaNx906JTq9ZXfV1pE5WVhbWpaSg37i5MFdaCc/fv/ZTqIpvImL+fOHZVH1KpRJxsTE4vWcTzmfuFZ6vUCjw+tR4nDqZjeTkZOH5pD+4LqGaqot1CRERUX3EBj4RkQzY29tLXQLpIVtbW5iamur8OpaWllixIhkrV65ETvoGJPm5cqSODAQFh8DeuR26DRojPPv29SvYvzYOkwImwcnJSXg+1Yy3tzdc3Xpg52dToNHBLvlWnVzx4sARmB4WjqKiIuH5pD+4LqGaqKt1CRERUX3DBj4RkQzY2NjA3Nxc6jJIzzRrJn7m+eP4+flxpI5MpKamIn13Gjz9F8DYxER4fvqyuWhgbobw8DDh2VRzCoUCCQvjUZCbjcztK3RyDa+AaBSXlCAiQjcH5pJ+4LqEaqKu1yVERET1BRv4REQyoFAo+NBD1WJtbQ2lUlnn1+VIHelpNBoEBgXjaZee6NBnkPD8a/lncWTzUoSHhcHW1lZ4PtWOm5sbfHx9kZYUDlXxLeH5NvYO6O0XioUL45GXlyc8n/QD1yVUXVKtS4iIiOoDNvCJiGTCzs4OxsbGUpdBekLKxgpH6kgrJSUFp05m47XJsVAoFMLzf0gKQ8uWDggICBCeTWIsiI6GWlWMjGTd7JLv5TsNVnbNERSkmwNzST9wXULVwR/4EBER6Q4b+EREMmFkZISmTZtKXQbpAXNzczRs2FDqMjhSRwIqlQph4TPwfL8hcOrsLjz/QvZBnN6zCdFRkbCwsBCeT2I4OjoiNCQEB76Ox/VL54Xnm1ko4RUQgy1bNiMjI0N4PukHrkuoquSyLiEiIjJUbOATEcmIvb29TnbUkmFp1qyZbO4TjtSpW4mJibh8uQCe/lHCs7VaLXYtCkbnLi4YPny48HwSKzAwEPb29vg+QTe75Lt4DYNzZ3d8NOVjlOvgwFzSD1yXUFXIaV1CRERkiNjAJyKSEVNTU86cpscyNjaGnZ2d1GU8hCN16kZhYSEiIiPhOng8mjq1FZ6fs28rfjtxAJ/ExcLIiEtEuVMqlYiLjcHpPZtwPnOv8HyFQoHXp8bj1MlsJCcnC88n/cB1CT2JHNclREREhoZPZ0REMsMZovQ4TZs2lW1zlSN1dCsqKhpld9XoP3aW8OxytRqpiaHw8PCEh4eH8HzSDW9vb7i69cDOz6ZAo4Nd8q06ueLFgSMwPSwcRUVFwvNJP3BdQo8j53UJERGRoeB3WiIimVEqlbC2tpa6DJIhhUIBe3t7qct4LI7U0Y38/HwkLEpAzxFBsLYT30zL3LocVy/kIjY2Rng26Y5CoUDCwngU5GYjc/sKnVzDKyAaxSUliIjQzYG5JH9cl9Cj6MO6hIiIyBCwgU9EJEPc7UaVsbOzg6mpqdRlPBFH6og3Y+ZMKBs2Ri+fqcKzy0qLkb50Nnx8feHi4iI8n3TLzc0NPr6+SEsKh6r4lvB8G3sH9PYLxcKF8cjLyxOeT/qB6xKqjL6sS4iIiPQdG/hERDJkY2PD3W70EIVCgRYtWkhdRrVwpI4YWVlZWJeSgn7j5sJcaSU8f//aT6EqvomI+fOFZ1PdWBAdDbWqGBnJutkl38t3GqzsmiMoSDcH5pL8cV1C/6SP6xIiIiJ9xQY+EZFMOTg4SF0CyUizZs1gZmYmdRnVxpE6tRcUHAJ753boNmiM8Ozb169g/9o4TAqYBCcnJ+H5VDccHR0RGhKCA1/H4/ql88LzzSyU8AqIwZYtm5GRkSE8n/QD1yX0IH1dlxAREekjNvCJiGTK0tIStra2UpdBMmBiYoLmzZtLXUaNcaROzaWmpiJ9dxo8/RfA2MREeH76srloYG6G8PAw4dlUtwIDA2Fvb4/vE3SzS76L1zA4d3bHR1M+RrkODswl+eO6hO7T93UJERGRvmEDn4hIxhwcHKBQKKQugyTWokULGBsbS11GrXGkTvVoNBoEBgXjaZee6NBnkPD8a/lncWTzUoSHhbEpZwCUSiXiYmNwes8mnM/cKzxfoVDg9anxOHUyG8nJycLzST9wXUKA4axLiIiI9AUb+EREMmZubo6mTZtKXQZJyNDuAY7UqbqUlBScOpmN1ybH6qRh9kNSGFq2dEBAQIDwbJKGt7c3XN16YOdnU6DRwS75Vp1c8eLAEZgeFo6ioiLh+SR/hvY9iaqP9wAREVHdYwOfiEjmuMupfmvZsqXB7XbkSJ0nU6lUCAufgef7DYFTZ3fh+ReyD+L0nk2IjoqEhYWF8HyShkKhQMLCeBTkZiNz+wqdXMMrIBrFJSWIiNDNgbkkf1yX1G+GuC4hIiKSOzbwiYhkjnNG6y+lUmnQo004UufREhMTcflyATz9o4Rna7Va7FoUjM5dXDB8+HDh+SQtNzc3+Pj6Ii0pHKriW8Lzbewd0NsvFAsXxiMvL094Pskf1yX1l6GvS4iIiOSKDXwiIj1gb28PU1NTqcugOubo6Gjwu9w4UuffCgsLEREZCdfB49HUqa3w/Jx9W/HbiQP4JC4WRkZcChqiBdHRUKuKkZGsm13yvXynwcquOYKCdHNgLskf1yX1U31YlxAREckRn9qIiPSAkZERHBwcpC6D6pCNjQ2sra2lLqNOcKTOw6KiolF2V43+Y2cJzy5Xq5GaGAoPD094eHgIzyd5cHR0RGhICA58HY/rl84LzzezUMIrIAZbtmxGRkaG8HySP65L6p/6tC4hIiKSGzbwiYj0ROPGjWFlZSV1GVQHFAoFWrVqJXUZdY4jdYD8/HwkLEpAzxFBsLZrJjQbADK3LsfVC7mIjY0Rnk3yEhgYCHt7e3yfoJtd8l28hsG5szs+mvIxynVwYC7JH9cl9Ud9XZcQERHJBRv4RER6QqFQwMnJiR9drgdatmwJc3NzqcuQRH0fqTNj5kwoGzZGL5+pQvIeVFZajPSls+Hj6wsXFxfh+SQvSqUScbExOL1nE85n7hWer1Ao8PrUeJw6mY3k5GTh+SR/XJfUH/V5XUJERCQHbOATEekRCwsLtGjRQuoySIeUSiWaNRO/81qf1NeROllZWViXkoJ+4+bCXCl+V+v+tZ9CVXwTEfPnC88mefL29oarWw/s/GwKNDrYJd+qkyteHDgC08PCUVRUJDyf5I/rEsPHdQkREZH02MAnItIzzZs3R4MGDaQug3SAuxkfVt9G6gQFh8DeuR26DRojsMK/3b5+BfvXxmFSwCQ4OTkJzyd5UigUSFgYj4LcbGRuX6GTa3gFRKO4pAQREbo5MJfkj+sSw8V1CRERkTywgU9EpGcUCgWcnZ35MGWAmjdvDqVSKXUZslJfRuqkpqYifXcaPP0XwNjERHiN6cvmooG5GcLDw4Rnk7y5ubnBx9cXaUnhUBXfEp5vY++A3n6hWLgwHnl5ecLzSf64LjFcXJcQERHJAxv4RER6SKlUonnz5lKXQQIplUqOIXgEQx+po9FoEBgUjKddeqJDn0HCa7uWfxZHNi9FeFgYbG1theeT/C2IjoZaVYyMZN3sku/lOw1Wds0RFKSbA3NJ/rguMTxclxAREckHG/hERHqqRYsW3BVlILh7sWoMdaROSkoKTp3MxmuTY3VyD/yQFIaWLR0QEBAgPJv0g6OjI0JDQnDg63hcv3ReeL6ZhRJeATHYsmUzMjIyhOeTfuC6xHBwXUJERCQvbOATEekphUKBp59+mg9XBsDBwYHzg6vI0EbqqFQqhIXPwPP9hsCps7vwWi5kH8TpPZsQHRUJCwsL4fmkPwIDA2Fvb4/vE3SzS76L1zA4d3bHR1M+RrkODswl+eO6xHBwXUJERCQvbOATEekxCwsLODo6Sl0G1YK1tTXs7e2lLkOvGNJIncTERFy+XABP/yjhNWi1WuxaFIzOXVwwfPhw4fmkX5RKJeJiY3B6zyacz9wrPF+hUOD1qfE4dTIbycnJwvNJP3Bdov+4LiEiIpIfNvCJiPScvb09GjduLHUZVANmZmbcrVgL+j5Sp7CwEBGRkXAdPB5NndoKv3bOvq347cQBfBIXCyMjLvkI8Pb2hqtbD+z8bAo0Otgl36qTK14cOALTw8JRVFQkPJ/0A9cl+ovrEiIiInni0xwRkQFwcnLi3Fk9Y2RkhNatW8PU1FTqUvSaPo/UiYqKRtldNfqPnSX8muVqNVITQ+Hh4QkPDw/h+aSfFAoFEhbGoyA3G5nbV+jkGl4B0SguKUFEhG4OzCX9wHWJ/uG6hIiISL7YwCciMgD3H7pMTEykLoWqiM0NcfRxpE7nLi5ISEhAzxFBsLZrJvx6mVuX4+qFXMTGxgjPJv3m5uYGH19fpCWFQ1V8S3i+jb0DevuFYuHCeOTl5QnPJ/3AdYn+4bqEiIhIvtjAJyIyEGZmZmjdujU/9qwHmjdvzvECOqBPI3UuX7kGM8uG6OUzVfg1ykqLkb50Nnx8feHi4iI8n/TfguhoqFXFyEjWzS75Xr7TYGXXHEFBujkwl/QD1yX6g+sSIiIieWMDn4jIgFhZWeGpp56Sugx6DBsbG7Rs2VLqMgyWPozUUd+7C1VxETwnRsJcaSU8f//aT6EqvomI+fOFZ5NhcHR0RGhICA58HY/rl84LzzezUMIrIAZbtmxGRkaG8HzSH1yXyB/XJURERPLHBj4RkYFp0qQJmjZtKnUZVAkLCwseDlcH5D5SZ9eiEDRxaodug8YIz759/Qr2r43DpIBJcHJyEp5PhiMwMBD29vb4PkE3u+S7eA2Dc2d3fDTlY5Tr4MBc0h9cl8gX1yVERET6gQ18IiID1KpVK1hbW0tdBj3A2NgYrVu3hrGxsdSl1BtyHKmTeygV5w6n4bWABTDWwWzo9GVz0cDcDOHhYcKzybAolUrExcbg9J5NOJ+5V3i+QqHA61PjcepkNpKTk4Xnk37hukR+uC4hIiLSHwqtVqutyguPHTum61pIz3Tt2lXqEojoMdRqNc6cOYOysjKpS6n3FAoF2rRpg4YNG0pdSr1UUlKCgIBJWLlyBbq+OQr/CU6EWQPLOq9Do9Fgke+LMFdaY/yyH4XveLyWfxafD+2ImAULEBgYKDSbDJNWq0UP95dQcOMO/Nccg5EOGnkbZo3EhSO7cD7vHGxsbITlrly1GqtWrkS5RgOtVgvNA//85681Wi20//fPyv78wfdrH/PnFb/WaoDK3q/RwNTMDJlHj6J9+/bC/q6GgusS+eC6hIiISL+wgU81xgY+kfzdu3cPZ8+e5cOyhBQKBZ555hk0atRI6lLqvVWrVmHCxImwae4M7wUb0eyZDnV6/eP/XYMNs0diQvJBOHV2F56/NngIbp7LxLncs7CwsBCeT4bp8OHD6NGjBwbPWAbXt8YKzy+6WoDPhrTFJH9/xMXFCsvt2as3jp04ifa93oACCiiMjACFAgqFERRGRlA84tcVr1Eo/u/PHv41HvH793+N/7uWwsio4rr3f/9SzlFk7VyDn376CS+//LKwv6sh4bpEelyXEBER6R828KnG2MAn0g98WJYOH5LlJycnB0PeeRe/5edjUEgSur7hVyfXvVemwqdD2sGxQ3f4xn4rPP9C9kEsef9lrFmzBr6+vsLzybD5jhiBbTtTMW3TOVhYid+Rm/7VfOxdPh85OTlo06aNkMwlS5YgYNIkTN9ZAGu7ZkIya2vVx2/C9NYlnMg6zpnij8F1iXS4LiEiItJPnIFPRGTgTE1N0a5dO5ibm0tdSr3Ch2R56tChAzKPHoH30KHYOGcUNs4djbt3SnR+3Z83JOLWtQJ4+UcJz9Zqtdi1KBidu7hg+PDhwvPJ8C2IjoZaVYyM5Eid5PfynQYru+YIChJ3YO7QoUNhbGyME7vWCcusjcKC33Dmp/9iUoA/m/dPwHWJNLguISIi0l9s4BMR1QN8WK5bfEiWN0tLS6xYkYyVK1ciJ30DkvxcceXXHJ1dr7SoEHuSI+E6eDyaOrUVnp+zbyt+O3EAn8TFwsiISzuqPkdHR4SGhODA1/G4fum88HwzCyW8AmKwZctmZGRkCMls3LgxBg0ahBP/lf5wagA4/N0XaGhjwx+iVRHXJXWL6xIiIiL9xqc8IqJ6gg/LdYMPyfrDz88PmUePopE5kOTXHcd26KYRuHdlNDTlavQfO0t4drlajdTEUHh4eMLDw0N4PtUfgYGBsLe3x/cJ4nbJP6iL1zA4d3bHR1M+Rnl5uZDMUX5+KMjNxuXcbCF5NXVPdQfHti3H6FGjoVQqJa1Fn3BdUje4LiEiItJ/bOATEdUjfFjWLT4k6x9dj9QpvJyPA+sT0HtEkE7mdGduXY6rF3IRGxsjPJvqF6VSibjYGJzeswnnM/cKz1coFHh9ajxOncxGcnKykEwvLy/YNWmK4zr64VtVZad9g+Kb1zFx4gRJ69BHXJfoFtclREREhoENfCKieub+wzJ3CYplbGyM1q1b8yFZD+lypE7akplQNmyMXj5TheQ9qKy0GOlLZ8PH1xcuLi7C86n+8fb2hqtbD+z8bAo0gnbJP6hVJ1e8OHAEpoeFo6ioqNZ5pqam8PXxwckfUlCuviegwpo5vHExvLxew7PPPitZDfqM6xLd4LqEiIjIcLCBT0RUD91/WLazs5O6FINgYWGB9u3bw8bGRupSqBZEj9QpOJOFE7tS8Or4uTBXWgmq8v/bv/ZTlN4qRMT8+cKzqX5SKBRIWBiPgtxsZG5foZNreAVEo7ikBBERYg7MHTXKD7euX8W5Q6lC8qrr99NH8HtOJgIC/CW5vqHgukQsrkuIiIgMCxv4RET1lJGREZydndGqVSsoFAqpy9FbjRo1Qvv27WFhYSF1KSSAyJE6uxaFoIlTO3QbNEZwlcDt61ewb3Us1PfuYc6cuSgtLRV+Daqf3Nzc4OPri7SkcKiKbwnPt7F3QG+/UCxcGI+8vLxa57m4uKDT8511dobFk/y8cTGecnLGgAEDJLm+IeG6RAyuS4iIiAwPG/hERPWcvb09nn32WZiYmEhdit5p2bIlnnnmGRgbG0tdCgkkYqRO7qFUnDuchtcCFsBYB/9tpS+bCxMzcwwKTsS69evRrbsrcnLEjP0hWhAdDbWqGBnJYnbJ/1Mv32mwsmuOoCAxB+aOHuWH/+3bitJbN4TkVVXxjWs4lfYN/CdO4PcBgbguqTmuS4iIiAwTG/hERARra2s899xznD9bRcbGxmjTpg1atGjBXYIGrKYjdTQaDb5PCIazS0906DNIeF3X8s/iyOaleGV0GF56zx/+q47ihkqLbt27Y/Xq1cKvR/WPo6MjQkNCcODreFy/dF54vpmFEl4BMdiyZTMyMjJqnTd8+HBoNeU4mfqNgOqqLnPrcigUwJgx4j9lU99xXVI9XJcQEREZNjbwiYgIAGBmZsb5s1XAubL1S01G6pz4PgV/5GZjwORYnTRSdi0OQ8OmDnB/LwAA0Kx1R0xcdQQd+r8HPz8/jB49hiN1qNYCAwNhb2+P7xPE7JL/py5ew+Dc2R0fTfkY5bU8MLd58+Z4uWdPZP237sboaMrLcWTTF/AeNgxNmjSps+vWJ1yXVA3XJURERIaPDXwiIqpwf/7s008/DVNTU6nLkZ1mzZpxrmw9VJ2ROvfKVEhdMgOd+g2BU2d34bVcyD6IXzI2wWtiJEzN//99aNbAEu/OXoF3Zq/gSB0SQqlUIi42Bqf3bML5zL3C8xUKBV6fGo9TJ7ORnJxc4xytVovZs+fgx3370NixNbRarcAqH+3MT/9F4eULCAgIqJPr1Vdclzwe1yVERET1g0JbxVXusWPHdF0L6ZmuXbtKXQIR6VB5eTkKCgpw7do1qUuRnKWlJZ566il+lJ+Qk5ODIe+8i9/y8zEoJAld3/B76M9/XPMJdiWG4uMNOWjq1FbotbVaLb4Y2wt375Rg0tpjMDKqfB/GlfO/4Ovp76Hoz3x8sWQJRo4cKbQOqj+0Wi16uL+Eght34L/mGIx0MFd7w6yRuHBkF87nnav2DuLS0lKMGj0aGzdsgNfESPQdPb3OxockB3ihYflNHD1yuE6uR1yXPIjrEiIiovqFO/CJiKhSxsbGeOqpp9C+fXs0aNBA6nIkcf/fQbt27fiQTAAeP1KntKgQe5Ij4Tp4vPDmPQDk7NuKC9kHMHBy7COb9wBH6pA4CoUCCQvjUZCbjcztK3RyDa+AaBSXlCAionoH5hYUFKBX7z7Ytn0HfOM24ZUxYXXWvL92IRe5h1IxKcC/Tq5Hf+O6hOsSIiKi+oo78KnGuAOfqP7QarW4evUqLl++DI1GI3U5dcLW1hatWrXiR/bpkVatWoUJEyfCprkzvBdsxLHtK3Douy8QtDkP1nbNhF6rXK1G/NBOaNTCCe8n/lDl92VuX4ltMRPR+pln8O3GDejQoYPQuqh+8B0xAtt2pmLapnOwsGooPD/9q/nYu3w+cnJy0KZNmye+PjMzE28O+g/KNEbw/XQbHNq/ILymx9n+6RT8Ly0FBb//ztElEuG6hIiIiOoT7sAnIqInUigUaNasGTp27IhGjRpJXY5OWVhY4Nlnn8UzzzzDh2R6LD8/P2QePYpG5sDiEV3x09cL0XtEkPDmPQBkbl2Ovy7mYsCkmGq9r9ubo+C/6ihuqLTo1r07Vq9eLbw2MnwLoqOhVhUjI7l6u+SrqpfvNFjZNUdQ0JMPzN2wYQN69e4N8yatMHHV0Tpv3t+9U4KsHSvxwfvvs3kvIa5LiIiIqD5hA5+IiKrMzMwMrVu3RocOHWBnZ1dn4wrqgpWVVcXfrWFD8TtMyTDdH6nj6OgACysb9PKZKvwaZaXFSFs6Gy4DfNGynUu138+ROlRbjo6OCA0JwYGv43H90nnh+WYWSngFxGDLls3IyMio9DX3D6sdOnQo2vd5G2O/2AvrJs2F1/IkWd+nQFVyGx9++GGdX5v+jesSIiIiqg84QodqjCN0iOjevXu4evUqrl27hvLycqnLqZFGjRqhefPmsLS0lLoU0lNZWVno2rUr3pr+BdwGjxOev3vpXOxdGY1p352FbQunWmVxpA7VVGlpKZ5t2w6N27nCN/Y74flarRZfvv8yrFGKrOPHYPzAgblSHlb7zxoX+bigaztnbNu2tc6vT0/GdQkREREZIu7AJyKiGjM1NYWDgwOef/55ODo6wszMTOqSqkShUKBp06bo2LEjWrduzYdkqpWg4BDYO7dDt0FjhGffvn4FP66Jw0tDJ9W6eQ9wpA7VnFKpRFxsDE7v2YTzmXuF5ysUCrw+NR6nTmYjOTm54velPKz2ny5kH8Dl3JMI4OG1ssV1CRERERki7sCnGuMOfCL6J61Wi6KiIty8eRNFRUVQq9VSl1RBoVDA2toaNjY2aNy4MUxMTKQuiQxAamoqvLy8MOKTLejY9z/C87csmIjs1PUI2nIeyoa2wnLv3inB1tgAHNu+EqNGjcbixYlQKpXC8skwabVa9HB/CQU37sB/zTEYPbBLXpQNs0biwpFdOJ93DufOnZP0sNp/Wjd9GEp+O47cs2dgZMR9UPqA6xIiIiIyBGzgU42xgU9Ej6PValFSUoKioiIUFRXhzp07dV6DiYkJbGxsYGNjg4YNGz40koGotjQaDVxeeBHFRtYYt+xH4buCr+WfxedDO+K1gAXoPSJQaPZ9HKlD1XX48GH06NEDg2csg+tbY4XnF10twGdD2sKjf3/sTt+NZm06wzduiyTz7h90668/EPPGU/gkLg5TpkyRtBaqGa5LiIiISF+xgU81xgY+EVVHWVkZioqKcOvWLahUKty9exdV/BZUZaampjA3N4eVlRVsbGxgaWlpUAfakbysWbMGI0eOxITkg3Dq7C4+P2gICv6XiWnfnYWpuYXw/PuunP8FX09/D0V/5uOLJUswcuRInV2LDIPviBHYtjMV0zadg4WV2MM1tVotln3YD78e2wuX14ZjyMzlOr3/q2r3snk4sCYGlwsK0KhRI6nLIQG4LiEiIiJ9wQY+1Rgb+ERUG1qtFmVlZVCpVA/9s6ysDBqNBhqNBlqtFlqtFgqF4qH/mZmZwcLCAubm5rCwsKj4NXeyUV1RqVR4tm072D7bHT6x3wrPv5B9EEvefxlD563BCwN9hef/E0fqUHVcunQJbdu1g+s7ARgwOUZY7l1VKb6dOxon06Q9rPafytX3EDfIGe/+5w0sXfql1OWQjnBdQkRERHLFQXtERCQJhUJR8ZBLpG8SExNx+XIBhsanCc/WarXYmRCMFm1d0OW14cLzK2PWwBLvzl6Bp1/sg3UxE3H4yBGO1KFHcnR0RGhICOZHRMJ18DjYObaudWbR1QKsCXwLV37NgW/cJnR65W0BlYqRs3crbl69zMNrDRzXJURERCRXPH2JiIiIqBoKCwsRERkJ18Hj0dSprfD8nH1bcSH7AAZOjq3zgzK7vTkK/quO4oZKi27du2P16tV1en3SH4GBgbC3t8f3CcG1zrqUk4nFfq64ff1PfPjVT7Jq3gPAoY2JeOnlnujcubPUpRARERFRPcQGPhEREVE1REVFo+yuGv3HzhKeXa5WY9eiUDzbwxPP9vAQnl8VzVp3xMRVR9Ch/3vw8/PD6NFjUFpaKkktJF9KpRJxsTE4vWcTzmfurXHOybQN+PKD3rBp1goBq47Cof0L4ooU4M+80zh/bB8mcfc9EREREUmEDXwiIiKiKsrPz0fCogT0HBEEa7tmwvMzty7HXxdzMWCSuLniNXF/pM47s1dg3fr16NbdFTk5OZLWRPLj7e0NV7ce2PnZFGjKy6v1Xq1Wi7Qv52Dd9KHo+MrbGPflXlg3aa6jSmvu0LdJsG/WHIMHD5a6FCIiIiKqp9jAJyIiIqqiGTNnQtmwMXr5TBWeXVZajLSls+EywBct27kIz68JjtShx1EoFEhYGI+C3Gxkbl9R5ffdVZXi67BhSF82F14TIzF0/lqYmstv7riq+BZO7FyDD8ePg5mZmdTlEBEREVE9xQY+ERERURVkZWVhXUoK+o2bC3OllfD8/Ws/RenN63hl9HTh2bXBkTr0OG5ubvDx9UVaUjhUxbee+PqiqwVYOq4P/rd/B3zjNuGVMWFQKBR1UGn1Hf/vatwru4Nx48ZJXQoRERER1WNs4BMRERFVQVBwCOyd26HboDHCs29fv4If18TCSAGsC3kHV36V17gajtShx1kQHQ21qhgZyZGPfZ3cD6t9kFarxeFvF+Ott9+Gg4OD1OUQERERUT3GBj4RERHRE6SmpiJ9dxo8/RfA2MREeH76srlQWpjjxx9/RCNzIMmvO47tWCX8OrXFkTpUGUdHR4SGhODA1/G4ful8pa+R+2G1/3T+6B5c+e0MAvx5eC0RERERSYsNfCIiIqLH0Gg0CAwKxtMuPdGhzyDh+dfyz+LI5qWYER4Od3d3ZB49Au+hQ7FxzihsnDsad++UCL9mbXCkDlUmMDAQ9vb2+D4h+KHf15fDav/p0MbFeK5DR/Tp00fqUoiIiIionmMDn4iIiOgxUlJScOpkNl6bHKuTWd0/JIWhZUsH+P/fTl9LS0usWJGMlStXIid9A5L8XDlSh2RPqVQiLjYGp/dswvnMvQD057Daf7r55+/I2bcVkwL8ZTufn4iIiIjqDzbwiYiIiB5BpVIhLHwGnu83BE6d3YXnX8g+iNN7NiE6KhIWFg83Nv38/JB59ChH6pDe8Pb2hqtbD+z8bApu/HFRbw6r/afDm76E0tISvr6+UpdCRERERMQGPhEREdGjJCYm4vLlAnj6RwnP1mq12LUoGJ27uGD48OGVvqZDhw4cqUN6Q6FQIGFhPApys5Hg4yK7w2pPZ2xG3pH0x75GfbcMx7Yug99IP1hbW9dRZUREREREj8YGPhEREVElCgsLEREZCdfB49HUqa3w/Jx9W/HbiQP4JC4WRkaPXpJxpA7pkwsXLsDExBSNHVrL5rBaTXk5di4MwtqgwVj50UCU3rrxyNeeSv8Wt65fhb//xDqskIiIiIjo0djAJyIiIqpEVFQ0yu6q0X/sLOHZ5Wo1UhND4eHhCQ8Pjyq9hyN1SM60Wi1mz56DoUOHotOr7+LDr/bL4rBaVfEtrAl8Cz+lfIaZM2dCqynHydRvHvn6wxsXo+8r/fDcc8/VYZVERERERI/GBj4RERHRP+Tn5yNhUQJ6jgiCtV0z4fmZW5fj6oVcxMbGVOt9HKlDclRaWoqhw4Zh3jx5HVZbeDkfX459GZeyf8SOHTswb948eHp6Ieu/lf/wq+BMFvJP/ozJkwLquFIiIiIiokdjA5+IiIjoH2bMnAllw8bo5TNVeHZZaTHSl86Gj68vXFxcqv1+jtQhOSkoKECv3n2wbbu8DqvNP/ETkvy6w7y8FId+/hkDBgwAAIwa5YcLpw7hWv7Zf73n542L4eDYCm+++WZdl0tERERE9Ehs4BMRERE9ICsrC+tSUtBv3FyYK62E5+9f+ylUxTcRMX9+rXI4UoeklpmZiW7dXfHbpT8xbpl8DqvN3LYCX03ohxee74ijRw6jQ4cOFX82aNAgNLSxwfH/PnwvlhYV4uQP6zDhw/EwMTGp65KJiIiIiB6JDXwiIiKiBwQFh8DeuR26DRojPPv29SvYvzYOkwImwcnJqdZ5HKlDUtmwYQN69e4N8yatMFFmh9V+O28M/Eb6YXdaKpo0afLQaywsLDD0vaE48f0aaDSait/P3L4C2nI1xo4dW9dlExERERE9Fhv4RERERP8nNTUV6bvT4Om/AMY62IWbvmwuGpibITw8TFgmR+pQXXrwsNr2fd7G2C/2yu6w2s8//xzLli2FmZlZpa8dNcoPN/78Hb8e2wsA0Gg0OPrdErzz7rto1kz8mRdERERERLXBBj4RERER/m7iBQYF42mXnujQZ5Dw/Gv5Z3Fk81LMCA+Hra2t8HyO1CFdk/NhtYtHueFi1j7s2LEDU6ZMeewcfnd3d7Ru8yyO/99/I+d+/gHXfj+PSQE8vJaIiIiI5IcNfCIiIiIAKSkpOHUyG69NjtXJIZw/JIWhZUsH+Pv7C8++jyN1SFfkfFht4sjuuHH5N7i5ulYcVvs4CoUCo/xG4peM71BWWoxDGxeji8sL6NGjRx1UTERERERUPWzgExERUb2nUqkQFj4Dz/cbAqfO7sLzL2QfxOk9mxAdFQkLC93uWOZIHRJNzofVLvuwH5o90xFvTluIPXvSkZGRUaX3+vr6oqy0BD+u+QRnDuzEpAB/WfxAgoiIiIjon9jAJyIionovMTERly8XwNM/Sni2VqvFrkXB6NzFBcOHDxee/ygcqUMiyP2w2hff8MP7i1PhOngcnDu746MpH6O8vPyJGc7Ozujdpy/2fDUPNo0awdvbuw4qJyIiIiKqPjbwiYiIqF4rLCxERGQkXAePR1OntsLzc/ZtxW8nDuCTuFgYGdXt0osjdaim5HxY7erAt7A/5TO8MfVzDA5fChNTMygUCrw+NR6nTmYjOTm5SlmjR/lBq9Vi9KjRUCqVOq6ciIiIiKhmFFqtVluVFx47dkzXtZCe6dq1q9QlEBER1VpgYBAWL/kC0zbnwdqumdDscrUaCcM6ofOzTkhN/UFodnWtWrUKEyZOhE1zZ3gv2Ihmz3SQtJ7KZG5fiW0xE9H6mWfw7cYN6NBBfjXWB6WlpRg1ejQ2btgAr4mR6Dt6uizGyxRezseqj9/EzT8vYnjUerR7+d/z7jfMGokLR3bhfN452NjYPDavtLQUe/ZkwNW1O+zt7XVVNhERERFRrVS5gU9ERERkaPLz89G2XTv0GR2OVz+YJTz/8HdfYsuCCTh+/DhcXFyE51dXTk4OhrzzLn7Lz8egkCR0fcNP6pL+5cr5X/D19PdQ9Gc+vliyBCNHjpS6pHqloKAAg/7zFn7JycG789bKZt59/omfsCbwbZhbNoTf59sf+QOooqsF+GxIW0zy90dcXGwdV0lEREREJB5H6BAREVG9NWPmTCgbNkYvn6nCs8tKi5G+dDZ8fH1l0bwHOFKHHk/uh9XaP9MR/qsOP/bTIzb2DujtF4qFC+ORl5dXh1USEREREekGG/hERERUL2VlZWFdSgr6jZsLc6WV8Pz9az+FqvgmIubPF55dG5aWllixIhkrV65ETvoGJPm54sqvOVKX9RCzBpZ4d/YKvDN7BdatX49u3V2RkyOvGg2NvhxWa9moyRPf18t3GqzsmiMoKLgOqiQiIiIi0i028ImIiKheCgoOgb1zO3QbNEZ49u3rV7B/bRwmBUyCk5OT8HwR/Pz8kHn0KBqZA0l+3XFsxyqpS/qXbm+Ogv+qo7ih0qJb9+5YvXq11CUZHH07rLYqzCyU8AqIwZYtm5GRkaHjSomIiIiIdIsz8ImIiKjeSU1NhZeXF0Z8sgUd+/5HeP6WBRNxJn09fj1/Hra2tsLzRSopKUFAwCSsXLkCXd8chf8EJ8KsgaXUZT3k7p0SbI0NwLHtKzFq1GgsXpwIpVIpdVl6T58Pq30SrVaLL99/GdYoRdbxYzA2NtZBpUREREREuscGPhEREdUrGo0GLi+8iGIja4xb9qPwhuW1/LP4fGhHxMbEYNq0aUKzdWnVqlWYMHEibJo7w3vBxsfOGZdK5vaV2BYzEa2feQbfbtyADh3kV6O+0PfDaqvi99NHsHiUG5YuXYoPPvhAYJVERERERHWHI3SIiIioXklJScGpk9l4bXKsTnYb/5AUhpYtHeDv7y88W5c4Uqf+MITDaquiVSdXvDhwBKaHhaOoqKhK71GpVMjMzER5eXmtrk1EREREJAob+ERERFRvqFQqhIXPwPP9hsCps7vw/AvZB3F6zyZER0XCwsJCeL6udejQAZlHj8B76FBsnDMKG+eOxt07JVKX9ZBmrTti4qoj6ND/Pfj5+WH06DEoLS2Vuiy9YUiH1VaFV0A0iktKEBERWaXXBwRMQvfu3bFx40Yh1yciIiIiqi028ImIiKjeSExMxOXLBfD0jxKerdVqsWtRMDp3ccHw4cOF59cVS0tLrFiRjJUrVyInfQOS/Fxx5dccqct6iFkDS7w7ewXemb0C69avR7fursjJkVeNciPrw2qnDsKPaz+Fl390tQ6rrQobewf09gvFwoXxyMvLe+xrk5OTsXz5V1AYGeH8+fPCaiAiIiIiqg028ImIiKheKCwsRERkJFwHj0dTp7bC83P2bcVvJw7gk7hYGBnp/xKLI3UMR2lpKYYOG4Z58+bCa2Ikhs5fC1Nz6T8hUng5H1+OfRmXsvfDzMwMqts3dDLWqpfvNFjZNUdQUPAjX3P8+HFMmDgRrm9/gKc6dGMDn4iIiIhkQ/+fLomIiIiqICoqGmV31eg/dpbw7HK1GqmJofDw8ISHh4fwfKlwpI7+KygoQK/efbBt+w74xm3CK2PCdNIkr678Ez8hya87zMtLcfjwIYSHheHA1/G4fkl849zMQgmvgBhs2bIZGRkZ//rzwsJCvD14COyf6YQ3AxNg69gGuecev1ufiIiIiKiusIFPREREBi8/Px8JixLQc0QQrO2aCc/P3LocVy/kIjY2Rni21DhSR3/J+bDaryb0wwvPd8TRI4fRoUMHBAYGwt7eHt8nPHqXfG108RoG587u+GjKxw8dUKvRaODrOwLXb96CT+x3MDW3gF2rNk8ct0NEREREVFfYwCciIiKDN2PmTCgbNkYvn6nCs8tKi5G+dDZ8fH3h4uIiPF8uOFJHv8j9sFq/kX7YnZaKJk3+PqxWqVQiLjYGp/dswvnMvcKvrVAo8PrUeJw6mY3k5OSK358/PwK7dn2PoRHrYNvCCQDQ2KE1rvz5B0pK5PVpEyIiIiKqn9jAJyIiIoOWlZWFdSkp6DduLsyVVsLz96/9FKrim4iYP194ttxwpI78yfmw2jWBb+GnlM/w+eefY9mypTAze/iwWm9vb7i69cDOz6ZA88AueVFadXLFiwNHYHpYOIqKirBr1y7MnTsH/cfNQVt3r4rXNWnVBgDw66+/Cq+BiIiIiKi62MAnIiIigxYUHAJ753boNmiM8Ozb169g/9o4TAqYBCcnJ+H5csSROvIl/8Nqf8SOHTswZcqUSufwKxQKJCyMR0FuNjK3r9BJLV4B0SguKUFAQACG+/ig3UsD0O/9GQ+9prFjawDgGB0iIiIikgU28ImIiMhgpaamIn13Gjz9F8DYxER4fvqyuWhgbobw8DDh2XLHkTryog+H1R76+WcMGDDgsa93c3ODj68v0pLCoSq+JbweG3sH9PYLxdq1a2HUwAbvzVsDI6OHH4msGtvDQmmF8+fFH6hLRERERFRdbOATERGRQdJoNAgMCsbTLj3Roc8g4fnX8s/iyOalmBEeDltbW+H5+oAjdeRBnw6rrYoF0dFQq4qRkRypk7p6+U7Dm4ELMeLTbVDaNP7XnysUCjThQbZEREREJBNs4BMREZFBSklJwamT2XhtcqxOdiL/kBSGli0d4O/vLzxbn3CkjrT07bDaqnB0dERoSAgOfB2P65fE74I3s1Di5WGT0bxNp0e+ppFDa+TlcQc+EREREUmPDXwiIiIyOCqVCmHhM/B8vyFw6uwuPP9C9kGc3rMJ0VGRsLCQfsa4HHCkTt3S58NqqyIwMBD29vb4PiFYB1U+mV2rNjjHHfhEREREJANs4BMREZHBSUxMxOXLBfD0jxKerdVqsWtRMDp3ccHw4cOF5+szjtSpG/p+WG1VKJVKxMXG4PSeTTifuVdsoVVg16oNLv1+EXfv3q3zaxMRERERPUih1Wq1UhdBREREJEphYSGead0az3kMx1shi4Xn/7J3C9YEvo3U1FR4eHgIzzcUq1atwoSJE2HT3BneCzai2TNVm39elzK3r8S2mIlo/cwz+HbjhirPaJdSQUEBBv3nLfySk4N3562Vzbz7/BM/YW3Q22jSqCH+u2O7kH+XWq0WPdxfQsGNO/BfcwxGxsYCKq2a85kZWPZhP5w9exZt27ats+sSEREREf0Td+ATERGRQYmKikbZXTX6j50lPLtcrUZqYig8PDzZvH8CjtQRz9AOq30ShUKBhIXxKMjNRub2FUIyq8rOsQ0A8CBbIiIiIpIcG/hERERkMPLz85GwKAE9RwTB2q6Z8PzMrctx9UIuYmNjhGcbIo7UEccQD6utCjc3N/j4+iItKRyq4ltCsx+nob0DTM3M2cAnIiIiIsmxgU9EREQGY8bMmVA2bIxePlOFZ5eVFiN96Wz4+PrCxcVFeL6hsrS0xIoVyVi5ciVy0jcgyc8VV37Nkbqsh5g1sMS7s1fgndkrsG79enTr7oqcHHnUaOiH1VbFguhoqFXFyEiO1El+ZYyMjGDn+AzOnz9fZ9ckIiIiIqoMG/hERERkELKysrAuJQX9xs2FudJKeP7+tZ9CVXwTEfPnC8+uDzhSp/rqw2G1VeHo6IjQkBAc+Doe1y/VXUPd1qENzp3jDnwiIiIikhYPsa2BY8eOSV0CyUzXrl2lLoGIqN571cMTp8//jslfn4KxiYnQ7NvXr+CTt1sjYMIEfPJJnNDs+qakpAQBAZOwcuUKdH1zFP4TnAizBpZSl/WQu3dKsDU2AMe2r8SoUaOxeHEilEplndZQnw6rrYrS0lI827YdGrdzhW/sd3Vyze2ffowrx77HubNn6uR6RERERESV4Q58IiIi0nupqalI350GT/8Fwpv3AJC+bC4amJshPDxMeHZ9w5E6T1bfDqutCqVSibjYGJzeswnnM/fWyTWbtGqDC7/9ivLy8jq5HhERERFRZdjAJyIiIr2m0WgQGBSMp116okOfQcLzr+WfxZHNSzEjPBy2trbC8+srjtSpXH09rLYqvL294erWAzs/mwJNHTTV7Vq1wb179/D777/r/FpERERERI/CBj4RERHptZSUFJw6mY3XJsfqZA73D0lhaNnSAf7+/sKz67sOHTog8+gReA8dio1zRmHj3NG4e6dE6rIe0qx1R0xcdQQd+r8HPz8/jB49BqWlpcKvw8Nqn0yhUCBhYTwKcrORuX2Fzq/X2LE1APAgWyIiIiKSFBv4REREpLdUKhXCwmfg+X5D4NTZXXj+heyDOL1nE6KjImFhIf3hoYaII3V4WG11uLm5wcfXF2lJ4VAV39LptWxbOMHI2Bh5eTzIloiIiIikwwY+ERER6a3ExERcvlwAT/8o4dlarRa7FgWjcxcXDB8+XHg+Pay+jtQpKChAr959sG37DvjGbcIrY8Ikb5IDfx9Wm+TXHeblpTj0888YMGCA1CVVWBAdDbWqGBnJkTq9jrGJKexaOrOBT0RERESSYgOfiIiI9FJhYSEiIiPhOng8mjq1FZ6fs28rfjtxAJ/ExcLIiEumulDfRurwsNqacXR0RGhICA58HY/rl3Q73sbWoTXyOEKHiIiIiCTEp1EiIiLSS1FR0Si7q0b/sbOEZ5er1UhNDIWHhyc8PDyE59Oj1ZeROjystnYCAwNhb2+P7xOCdXqdxq3aIPccd+ATERERkXTYwCciIiK9k5+fj4RFCeg5IgjWds2E52duXY6rF3IRGxsjPJuqxlBH6vCwWjGUSiViFkTj9J5NOJ+5V2fXsXNsg99+PQ+tVquzaxARERERPQ4b+ERERKR3ZsycCWXDxujlM1V4dllpMdKXzoaPry9cXFyE51PVGdpIHR5WK9a+fT/C1NwCFlY2OruGnWNr3CktxZ9//qmzaxARERERPQ4b+ERERKRXsrKysC4lBf3GzYW50kp4/v61n0JVfBMR8+cLz6bqM5SROjysVqzk5GR89dUy/CckSafjh+xatQEAHmRLRERERJJhA5+IiIj0SlBwCOyd26HboDHCs29fv4L9a+MwKWASnJychOdTzenzSB0eVivW8ePHMWHiRLi+/QG6DRqt02s1bvk0AOA8D7IlIiIiIomwgU9ERER6IzU1Fem70+DpvwDGJibC89OXzUUDczOEh4cJz6ba08eROn37vsLDagUqLCzE24OHoNkznfBmYILOr2dq0QC2zR25A5+IiIiIJMMGPhEREekFjUaDwKBgPO3SEx36DBKefy3/LI5sXooZ4eGwtbUVnk9i6MtInXdmJeO5Pv/Bvn170a73WzysVgCNRgNf3xG4fvMWhsd+V2fnBzR2bMMGPhERERFJhg18IiIi0gspKSk4dTIbr02O1cns8B+SwtCypQP8/f2FZ5N4ch6pc1dViq/DhuF/+7bCa2IkhkWk8LBaASIiIrBr1/cYGrEOti3qbsSVrUNrnMvjCB0iIiIikgYb+ERERCR7KpUKYeEz8Hy/IXDq7C48/0L2QZzeswnRUZGwsJC+0UpVI8eROkVXC7B0XB/8bz8PqxVp165dmDNnDl4dPxdt3b3q9NpNWrXB+fPcgU9ERERE0mADn4iIiGQvMTERly8XwNM/Sni2VqvFrkXB6NzFBcOHDxeeT7olp5E6l3IysdjPFbev/4kPv+JhtaLk5+fDe/hwtH95IF4ZE17n17dzbIOimzdRWFhY59cmIiIiImIDn4iIiGStsLAQEZGRcB08Hk2d2grPz9m3Fb+dOIBP4mJhZMSlkb6SeqTOybQN+PKD3rBp1goBPKxWqEmTP0LJHRXeDl8qyX+jjR1bAwDn4BMRERGRJPiUSkRERLIWFRWNsrtq9B87S3h2uVqN1MRQeHh4wsPDQ3g+1S0pRupotVqkfTkH66YPRcdX3sa4L3lYrWieHh5AuRpLRvfAiV3roNVq6/T6dmzgExEREZGE2MAnIiIi2crPz0fCogT0HBEEa7tmwvMzty7H1Qu5iI2NEZ5N0qjLkTr3D6tNXzYXXhMjMXT+Wh5WqwOTJgXgl19+QZ8e3bB+hg++GOOOC9kH6+z6FlYN0dDOHufP8yBbIiIiIqp7bOATERGRbM2YORPKho3Ry2eq8Oyy0mKkL50NH19fuLi4CM8nael6pA4Pq61bzz77LDZv3oS9e/fC1uQelrz/MtZNH4rCgt/q5PqNHVpzBz4RERERSYINfCIiIpKlrKwsrEtJQb9xc2GutBKev3/tp1AV30TE/PnCs0kedDVSh4fVSqdPnz44lnkUK1euxNXTP+Gzd9rj+4QQqIqLdHpdW8c2yD3HBj4RERER1T028ImIiEiWgoJDYO/cDt0GjRGeffv6FexfG4dJAZPg5OQkPJ/kQ/RIHR5WKz0jIyP4+fkh71wuZoSH4ci3ifh08LM49O0XKFerdXJNO8fWHKFDRERERJJgA5+IiIhkJzU1Fem70+DpvwDGJibC89OXzUUDczOEh4cJzyZ5qu1IHR5WKz+WlpaYPXs2zuXm4u03B2LLgglYNLwLzh7cJfxadq3a4NrVK7h9+7bwbCIiIiKix2EDn4iIiGRFo9EgMCgYT7v0RIc+g4TnX8s/iyObl2JGeDhsbW2F55N81XSkDg+rlTcHBwesWrkSmZmZeNaxKVZMHoAVkwfgyvlfhF3DrlUbAMCvv/4qLJOIiIiIqCrEb2kjIiIiqoWUlBScOpmNCckHddKM/CEpDC1bOsDf3194Nsnf/ZE6ffv2wYSJE1HwyxF4L9iIZs9UPi++6GoB1gS+hSu/5sA3bpNs5t3nn/gJa4PeRpNGDbH7558Nct59dXXt2hX79mZg69atmBYYhIXendH97XHwGD8XVo3ta5Vt59gaAJCXl4cuXbqIKJdkRqvVoqysDCqVquKfKpUKd+/eRXl5ObRabcX/gL9HOSkUCigUCpiZmcHc3BwWFhawsLCo+LWxsbHEfysiIiIyBGzgExERkWyoVCqEhc/A8/2GwKmzu/D8C9kHcXrPJqxZswYWFtLvoCbp+Pn5oXv37hjyzrtI8uuOQSFJ6PqG30OvuZSTidXT/gOFkRE+/OonWcy7B4DM7SuxJWoc3N1fwqbvvjXoeffVpVAo8NZbb2HgwIFYvHgx5s6bh+xdKeg7Ohwve39U409OKG3soLS2QV4eD7I1FGVlZSgqKsKtW7cqmvbVodFoKn6tVqtRWlr6r9eYmJjAwsICVlZWsLGxgaWlZb38lAwRERHVDkfoEBERkWwkJibi8uUCePpHCc/WarXYtSgYnbu4YPjw4cLzSf88bqSOrA+rnTu6XhxWWxtmZmb4+OOPcT4vDx+MGY3dX8xA/LvP4WTahood1NWhUChg16oND7LVY1qtFsXFxSgoKMAvv/yC06dP4/fff0dRUVG1m/dVpVarUVxcjD///BNnz57FyZMnkZ+fjxs3bqC8vFwn1yQiIiLDo9DWZAVbzx07dkzqEkhmunbtKnUJRER6r7CwEM+0bo3nPIbjrZDFwvN/2bsFawLfRmpqKjw8PITnk35btWoVJkyciIbNnPB011dw6NskuLw2HENmLpfFvHtV8S2snzEcZ37aiWbNmiE9fTc6duwodVl64+zZswgMDMKOHdvh3NkdA6d+jqc6uVUrY930oWh87xoyMvboqEoSTavVoqioCDdv3kRRURHUarXUJVVQKBSwtraGjY0NGjduDBMdHNhOREREhoE78ImIiEgWoqKiUXZXjf5jZwnPLlerkZoYCg8PTzbvqVJ+fn7Y/+OPKL1+GYe+TZLdYbVL3n8Zv2Xtx1v/r737Do+qzN8/fk8mvZOEFAKCwgqCIkpZcaUqYEWxoYAUZV2lKKuCCqIUQSmKBXAFXUGKCNKEZSUKFsAGiPQOoSSQQEJCCpm0+f3hl/xkaSknOWdm3q/r8lpMMs/c4FmSuc8zn+elqbIFRah5ixb69NNPzY7mMurXr69ly77UN998o2Bnjqb2vknzXumujOOHS71GZM162ssIHZdQVFSklJQUbdu2Tfv371daWpqlynvpj5sLp0+f1pEjR7RlyxYdOnRIeXl5ZscCAAAWRIEPAABMl5iYqPfef0+3PDZYIZExhq+/YenHSj20R+PHjzN8bbiHpKQkPfmPp+QoKFCPCYvU7vGhlphVnfj7Wk3p2VwFebnq98lPuunBp9Rv5q9qeOvD6tWrl/r0efyCs7dxYbfeeqt+3/SbPvroIyVvWq23HqivlVOGyZGTddnHRtaqp+Sko5SsFlZQUKCkpCRt3bpVR48eVX5+vtmRSsXpdOrkyZPavn279u/fr+zsbLMjAQAAC6HABwAApntl+HAFhkaoVffnDF/bkZutVdNeU/cePdSkSRPD14fr27Bhg5o1b6GDR4/rH9PX6tp2XcyOJOmPw2qnP9Ve0Vc1Uv+ZvyjmqoaSJN+AID302id68LVPNHfePDVr3kI7duwwOa3rsNvteuKJJ7Rv7x69OPgF/fjZ23rr/r/o1yUfqfgSc8kjataV0+nUwYMHqzAtSuPMmTNKTEzU1q1bdfz4cZeeL5+RkaHdu3dr165dysjIKNeZDQAAwL1Q4AMAAFNt2rRJc+fMUfsnR8ovMNjw9dfMeVt52Rl6ffRow9eG65s/f75atW4tv6ha6mfBw2pvvLuXnpiSoKDw8w+rbXZPb/WfuV6n8pxq1rw5I3XKKCQkRKNHj9bePXt09+23adHrf9fkHjdo7y/fXPDro2rVkyQOsrWQ/Px87d+/Xzt27FBaWppbld05OTklv7fMzEyz4wAAABNR4AMAAFMNHvKiouvUV7POjxu+dlZaitbMGq+BAwaqdu3ahq8P1+V0OvXaayPUtWtXNWjTRX3/9Z1ComLNjqW87NP69IX7tGbO27r7uUm6f9g0efv4XvTrY+o2YqROBdWqVUtzZs/WL7/8otrVQ/Vx/w6aOehupR7cec7XhUTFydc/QPuYg286p9OplJQUbd++XRkZGWbHqVR5eXnat2+fDhw4oIKCArPjAAAAE1DgAwAA0yQkJGjVN1+rY/83Zff2Nnz9VdNHKsDPV8OGDTV8bbiu3NxcdX3kEY0aNdKah9X+9oN6T1quW7oNKtUcfkbqGKNFixZat3aNFixYoNyjO/TOI9dp6fiBysk4KUmy2WyKqlmXAt9kOTk52rlzp44ePari4mKz41SZU6dOafv27UpNTXWrdxoAAIDLo8AHAACmKC4u1guDh+jKJreoYZvOhq9/InG3fl08Ta8MG6Zq1aoZvj5cU1JSklq1bqMvly239GG19f92R5nXYKROxdlsNj344IPatXOH3nzjDW376lO91aWefpj9lgrzHQqvWU/79jFCxwxFRUU6fPiwdu3apTNnzpgdxxRFRUU6cuSIdu/ezTttAADwIDYnt+/LbOPGjWZHgMU0bdrU7AgA4HJmzZqlnj176ul//6jajVsavv7sIQ8oY+8G7d2zW/7+5u+uhvk2bNigezrfK0exl3q89aUl5t1LfxxWu3jMk7qi8c3qMf6LC867L4v8MzlaOn6ANi6bod69+2jKlMkKDAw0KK1nOXHihF57bYSmTftQ1eJqK7BatOy5J3Vg316zo3mU9PR0HTlyRIWFhWZHsZSYmBjFxcXJbrebHQUAAFQiduADAIAql5eXp6HDXtF17R+olPL+0OYftW31Ir0xdgzlPSS59mG1ZcVIHeNUr15dU6dO0ZYtW9Ts2vo6vPVnHT18SEVFRWZH8wjFxcVKTEzUwYMHKe8vICUlRbt27VJeXp7ZUQAAQCWiwAcAAFVu8uTJSk5OUsf+Yw1f2+l06qv3h6jx9U3UrVs3w9eHa3GXw2rLg5E6xmnYsKH++98V+uqrr5SQkMCO5yqQn5+v3bt3Ky0tzewolpaXl6ddu3YpMzPT7CgAAKCSMEKnHBihg//FCB0AKL309HRdVbeurunQTfe9OMXw9bd/t0SzXuiihIQEdejQwfD14Tpyc3PVu08fLZg/X536jVHbPi9bYt59enKiZv7zHmUcP6xuY+eVa959WTBSB64mKytLBw4cYNd9GdWoUUOxsbGW+HsOAAAYx9vsAAAAwLOMHfuGHPmFurXvq4avXVRYqITJL6lDh46U9x4uKSlJne+9T9t37FCPCYt0bbsuZkeS9MdhtbNe6CK/oFD1++QnxVzVsNKf8+xInStvbKO54/rpl19/1RcL5qthw8p/bqCsUlNTdfToUbHPrOySk5OVm5urOnXq8C4RAADcCCN0AABAlUlMTNR777+nWx4brJDIGMPX37D0Y6Ue2qPx48cZvjZcx4YNG9SseQsdPHpcT05fa5nyfsOyGZr+VHtFX9VI/Wf+UiXl/Z8xUgdWdnbe/ZEjRyjvKyAjI4O5+AAAuBkKfAAAUGVeGT5cgaERatX9OcPXduRma9W019S9Rw81adLE8PXhGjzpsNryiKnbSP1m/qqGtz6sXr16qU+fx5Wbm2tKFuCsgoIC5t0biLn4AAC4Fwp8AABQJTZt2qS5c+ao/ZMj5RcYbPj6a+a8rbzsDL0+erTha8P6LH1Y7fP3VuphtWV1dqTOg699ornz5qlZ8xbasWOHqZnguc6W99xIMlZRUZH279+vjIwMs6MAAIAKosAHAABVYvCQFxVdp76adX7c8LWz0lK0ZtZ4DRwwULVr1zZ8fVhbbm6uuj7yiEaNGqlO/cao6+jZ8vHzNzuW0pMTNbXPTdrz01f626ODdEu3QZY6XJKROjDb2fLe4XCYHcUtOZ1OHThwgBIfAAAXR4EPAAAqXUJCglZ987U69n9Tdm9vw9dfNX2kAvx8NWzYUMPXhrUlJSWpVes2+nLZcvWYsEjtHh9qiZI88fe1mtqruQKceep8T2etnfO2Fozso/wzOWZHOwcjdWAWyvuqQYkPAIDro8AHAACVqri4WC8MHqIrm9yihm06G77+icTd+nXxNL0ybJiqVatm+PqwLisfVvvR0+11w3WNtGH9r1q8eJFmzJihHavma2qvFko5YK1xNYzUQVWjvK9alPgAALg2CnwAAFCp5syZo61bNuv2Z8ZXys7olVOHqkaNePXv39/wtWFdVj+stlfPXvrm6wRFRf1xWG2vXr20Yf16hftJU3s118blM01Oez5G6qAqUN6bgxIfAADXRYEPAAAqTV5enoYOe0XXtX9AtRu3NHz9Q5t/1LbVi/TG2DHy9zd/5jkqn5UPq531wn1aO+dtTZo0SdOnT5Ov77mH1TZs2FAb1v+qR7t21YIRvRmpA49DeW8uSnwAAFyTzel0Os0O4Wo2btxodgRYTNOmTc2OAACWNHHiRL340kv65/wdql77akPXdjqdmvb3Vgpx5mjTbxvl5cW+BHeXm5ur3n36aMH8+erUb4za9nnZEvPu05MTNeu5e5Sdelifz5unO+6447KPmTlzpp7u109hsXX06JsLFHNVwypIWjYbls3Ql+P6qe5VV+mLBfPVsKH1MsJ1FBYWateuXZT3FmCz2VSvXj2FhoaaHQUAAJSCSxX4FOewKgp8ADhfenq6rqpbV9d06Kb7Xpxi+Prbv1uiWS90UUJCgjp06GD4+rCWpKQkdb73Pm3fsUMPjZptmXn3ib+v1ezBXRQVHqr/LF9WppJ7x44deuDBh3QwMVGdX5yqpnf3qsSk5ZOyf7s+e/lhZR5P1L8++EA9e/Y0OxJckNPp1N69e5WVlWV2FPwfu92uBg0a8O41AABcAFvVAABApRg79g058gt1a99XDV+7qLBQCZNfUocOHSnvPYArHFa7/tdfyrxDnZE68BRHjhyhvLeYoqIi7d+/X0VFRWZHAQAAl0GBDwAADJeYmKj33n9Ptzw2WCGRMYavv2Hpx0o9tEfjx48zfG1Yi6sdVltWQUFB+uSTf2vGjBnasWq+pvZqoZQDOwxOXDG+AUF66LVP9OBrn2juvHlq1ryFduywVkZY18mTJ3XixAmzY+AC8vLydPDgQbnQm/IBAPBIFPgAAMBwrwwfrsDQCLXq/pzhaztys7Vq2mvq3qOHmjRpYvj6sAZXPqy2PHr16qUN69cr3E+a2qu5Ni6faUBaYzW7p7f6z1yvU3lONWveXJ9++qnZkWBx2dnZOnz4sNkxcAmZmZlKTk42OwYAALgECnwAAGCoTZs2ae6cOWr/5Ej5BQYbvv6aOW8rLztDr48ebfjasIbc3Fx1feQRjRo1Up36jVHX0bPl42f+nOb05ER92PdvOrr5By1fvlyDBg0y9BBdRurAneTn52v//v3s7nYBx48fV3p6utkxAADARVDgAwAAQw0e8qKi69RXs86PG752VlqK1swar4EDBqp27dqGrw/zJSUlqVXrNvpy2XL1mLBI7R4famhJXl6Jv6/V1F7N5VeUq59/+kl33HFHpTwPI3XgDoqLi7V//34VFhaaHQWldOjQIW7GAQBgURT4AADAMAkJCVr1zdfq2P9N2b29DV9/1fSRCvDz1bBhQw1fG+Zz18Nqy4OROnBllMGu5+xNl4KCArOjAACA/0GBDwAADFFcXKwXBg/RlU1uUcM2nQ1f/0Tibv26eJpeGTZM1apVM3x9mMvdD6stD0bqwBWlpqYyjsVF5efnc6gtAAAWRIEPAAAMMWfOHG3dslm3PzO+UkaerJw6VDVqxKt///6Grw3zeNphtWXFSB24kry8PB09etTsGKiArKwspaammh0DAAD8CQU+AACosLy8PA0d9oqua/+Aajduafj6hzb/qG2rF+mNsWPk72/+YaYwhqceVlsejNSB1TmdTnZvu4mkpCSdOXPG7BgAAOD/UOADAIAKmzx5spKTk9Sx/1jD13Y6nfrq/SFqfH0TdevWzfD1YQ5PP6y2PBipAys7duwY/63dhNPpVGJiIjdjAACwCAp8AABQIenp6Xp9zBi1uP8fql77asPX3/H9Uh38fZ0mThgvLy9+dHEHHFZbfozUgRXl5ubq+PHjZseAgXJzc3Xs2DGzYwAAAFHgAwCACho79g058gt1a99XDV+7qLBQCZNfUocOHdWhQwfD10fV47BaYzBSB1bBbm33dfz4cd5VAQCABVDgAwCAcktMTNR777+nWx4brJDIGMPX37D0Y6Ue2qPx48cZvjaqFofVGo+ROrCC48ePMy/dTTmdTh06dIibMwAAmMzmdKHvxhs3bjQ7AnBBTZs2NTsCAJiix2OPaflX3+i5RXvlFxhs6NqO3Gy91aWeOt/ZUbPYuevScnNz1btPHy2YP1+d+o1R2z4vW2LefXpyomY9d4+yUw/r83nzLDXvvqxmzpypp/v1U1hsHT365gLFXGW98T8bls3Ql+P6qe5VV+mLBfMtOaIIZZOXl6cdO3ZQ8Lq5+Ph4xcaaf8MVAABPxQ58AABQLps2bdLcOXPU/smRhpf3krRmztvKy87Q66NHG742qg6H1VYNRuqgqrE723MkJyfL4XCYHQMAAI9FgQ8AAMpl8JAXFV2nvpp1ftzwtbPSUrRm1ngNHDBQtWvXNnx9VA0Oq61ajNRBVUpPT1d2drbZMVAFnE6njhw5YnYMAAA8FgU+AAAos4SEBK365mt17P+m7N7ehq+/avpIBfj5atiwoYavjarBYbXmCAoK0ief/FszZszQjlXzNbVXC6Uc2GF2rHP4BgTpodc+0YOvfaK58+apWfMW2rHDWhlxacXFxUpKSjI7BqpQZmamsrKyzI4BAIBHosAHAABlUlxcrBcGD9GVTW5RwzadDV//ROJu/bp4ml4ZNkzVqlUzfH1ULg6rtQZG6qAypaamqqCgwOwYqGJHjx5lZBIAACagwAcAAGUyZ84cbd2yWbc/M75SZpmvnDpUNWrEq3///oavjcqVm5urro88olGjRqpTvzHqOnq2fPz8zY6l9OREfdj3bzq6+QctX75cgwYNssQc/srGSB1UhsLCQh0/ftzsGDBBbm6uTp06ZXYMAAA8DgU+AAAotby8PA0d9oqua/+Aajduafj6hzb/qG2rF+mNsWPk729+8YvS47Baa2KkDox27NgxFRUVmR0DJklKSlJxcbHZMQAA8CgU+AAAoNQmT56s5OQkdew/1vC1nU6nvnp/iBpf30TdunUzfH1UHg6rtT5G6sAIDodDJ06cMDsGTJSfn6+TJ0+aHQMAAI9CgQ8AAEolPT1dr48Zoxb3/0PVa19t+Po7vl+qg7+v08QJ4+XlxY8oroLDal0HI3VQUUlJScxAB+/CAACgivHqGAAAlMrYsW/IkV+oW/u+avjaRYWFSpj8kjp06KgOHToYvj6Mx2G1romROiivnJwc5p9DEucgAABQ1SjwAQDAZSUmJuq999/TLY8NVkhkjOHrb1j6sVIP7dH48eMMXxvG47Ba18dIHZRVUlKS2RFgISkpKcrPzzc7BgAAHoECHwAAXNYrw4crMDRCrbo/Z/jajtxsrZr2mrr36KEmTZoYvj6MxWG17sOTR+oUFxfrry1vVp8+jxuQ0v1lZmYqKyvL7BiwEKfTqWPHjpkdAwAAj0CBDwAALmnTpk2aO2eO2j85Un6BwYavv2bO28rLztDro0cbvjaMxWG17sdTR+r8+OOP+vXnnzRz5gzt3bvXoKTuKyUlxewIsKC0tDQVFBSYHQMAALdHgQ8AAC5p8JAXFV2nvpp1Nn6nalZaitbMGq+BAwaqdu3ahq8P43BYrXvztJE68+fPV3h0DYVERGvixLcMTOl+cnNz2X2PC3I6nUpNTTU7BgAAbo8CHwAAXFRCQoJWffO1OvZ/U3Zvb8PXXzV9pAL8fDVs2FDD14YxOKzWc3jKSJ3i4mIt+GKhGrZ/UC27PqMZM2eww/wS+LPBpZw4cULFxcVmxwAAwK1R4AMAgAsqLi7WC4OH6Momt6hhm86Gr38icbd+XTxNrwwbpmrVqhm+PiqOw2o9jyeM1Pnxxx91/FiyGt/2sP764NOy2b31/vvvV2Ji15Wfn69Tp06ZHQMWVlRUpLS0NLNjAADg1ijwAQDABc2ZM0dbt2zW7c+Mr5RydOXUoapRI179+/c3fG1UHIfVejZ3Hqkzf/58hcfE64rGLRUYWk3N73tSU6ZOVXZ2diUndj0nTpyQ0+k0OwYsLiUlhesEAIBKRIEPAADOk5eXp6HDXtF17R9Q7cYtDV//0OYftW31Ir0xdoz8/c3f0Y1zcVgtJPccqVNcXKz5C75Qw3YPyMvrj5dCt3QbpKysLH388cdVFdslFBcX68SJE2bHgAtwOBw6ffq02TEAAHBbFPgAAOA8kydPVnJykjr2H2v42k6nU1+9P0SNr2+ibt26Gb4+KobDavFn7jZSZ926dUo5fkyNb3u45GPhsVeoccdHNfGtt1VQUFBVsS0vLS1NRUVFZseAi+CsBAAAKg8FPgAAOEd6erpeHzNGLe7/h6rXvtrw9Xd8v1QHf1+niRPGl+yAhfk4rBaX4i4jdRYsWFAyPufPWj/2go4eOaz58+dXVVxLczqdFLIok6ysrDIfKA0AAEqHV80AAOAcY8e+IUd+oW7t+6rhaxcVFiph8kvq0KGjOnToYPj6KB8Oq0VpuPpInQuNzzkr7i+NVf/m2/XmuPHM8paUmZkph8Nhdgy4GG76AABQOSjwAQBAicTERL33/nu65bHBComMMXz9DUs/VuqhPRo/fpzha6N8OKwWZeHKI3UuND7nz1r3HKJtW7coISGhihNbT2pqqtkR4IJOnTrFGCoAACoBBT4AACjxyvDhCgyNUKvuzxm+tiM3W6umvabuPXqoSZMmhq+PsuOwWpSXK47UGTx48AXH55x1VdO2qtWwmd4cN76Kk1qLw+FQVlaW2THggpxOp9LS0syOAQCA26HABwAAkqRNmzZp7pw5av/kSPkFBhu+/po5bysvO0Ovjx5t+NooOw6rRUW52kidX375RY3aP3TRszdsNpta9Ryi775drY0bN1ZxUutIT083OwJcGNcPAADGszldaMijJ/8gDWtr2rSp2REAoMJu69BR2/Yf0TOfbZXd29vQtbPSUjSxS10NePppTZw4wdC1UTZOp1MjRozUqFEj1eT2bnpg+MeWmHefl31anw/vrt3rVuitt97Ss88+a4lRPiidmTNn6ul+/RQWW0ePvrlAMVdZ710TJw7tUWStepc8PLu4qEhvP3C12v+tuT6fN68K01nH9u3blZeXZ3YMuLCGDRsqICDA7BgAALgNduADAAAlJCRo1Tdfq2P/Nw0v7yVp1fSRCvDz1bBhQw1fG6XHYbWoLK4wUqd67asvWd5Lkpfdrr91f15fLFigAwcOVFEy68jNzaW8R4WxCx8AAGNR4AMA4OGKi4v1wuAhurLJLWrYprPh659I3K1fF0/TK8OGqVq1aoavj9LhsFpUNlcYqVMaTe/prcCwCL311ttmR6lyFK8wQnp6ulzojf4AAFgeBT4AAB5uzpw52rpls25/ZnylFLorpw5VjRrx6t+/v+Fro3Q4rBZVJSgoSJ988m/NmDFDO1bN19ReLZRyYIfZscrE1z9QNz08UP/+5N86ceKE2XGqjNPppMCHIfLz85WT43o37wAAsCoKfAAAPFheXp6GDntF17V/QLUbtzR8/UObf9S21Yv0xtgx8vc3f1SLJ+KwWpjBFUbqXErLh/rLKZumTJlidpQqk5WVpYKCArNjwE1wMwgAAONQ4AMA4MEmT56s5OQkdew/1vC1nU6nvnp/iBpf30TdunUzfH1cmtPp1GuvjVDXrl3VoE0X9f3XdwqJijU7lvKyT2vWC/dp7Zy3NWnSJE2fPk2+vr5mx0IlcOWROkHhkWra+Qm99/5k5ebmmh2nSlC4wkiM0QEAwDgU+AAAeKj09HS9PmaMWtz/D1WvfbXh6+/4fqkO/r5OEyeMv+zBkTAWh9XCKlx5pM4t3Z9TZmaGPvnkE7OjVLri4mJlZGSYHQNupKioSJmZmWbHAADALfBqGgAADzV27Bty5Bfq1r6vGr52UWGhEia/pA4dOqpDhw6Gr4+L47BaWJErjtSJqFFHjW97WBMmvqXCwkKz41SqzMxMFRUVmR0DboZ3dQAAYAwKfAAAPFBiYqLee/893fLYYIVExhi+/oalHyv10B6NHz/O8LVxcRxWCytzxZE6rR4brEOJB7Vw4UKzo1QqilZUhoyMDG4MAQBgAAp8AAA80CvDhyswNEKtuj9n+NqO3GytmvaauvfooSZNmhi+Pi6Mw2rhClxtpE58gxv0l7/epjfHjXfbed5Op1OnT582OwbckNPpVHZ2ttkxAABweRT4AAB4mE2bNmnunDlq/+RI+QUGG77+mjlvKy87Q6+PHm342jgfh9XCFbnSSJ3Wjw3R75t+0+rVq82OUimys7NVXFxsdgy4KebgAwBQcRT4AAB4mMFDXlR0nfpq1vlxw9fOSkvRmlnjNXDAQNWuXdvw9XEuDquFK3OVkTr1/nqb4us30bhx482OUinYfY/KxPUFAEDFeZf2Czdu3FiZOQAAHsbpdMrhcCgvL6/kf/Py8pSfn6+ioiI5nc6SfyTJy8tLNptNNptNvr6+8vPzk7+/v/z9/Ut+bbfbTf5dWV9CQoJWffO1Hpu4RHbvUv8YUGqrpo9UgJ+vhg0bavjaOFdSUpI633uftu/YoR4TFllm3n3i72s1e3AXRYWH6puffmLePS7p7Eidtm3b6Ol+/ZS0/Vc9+uYCxVxlnevGZrOp1WNDNO+Vbtq8ebOuv/56syMZioIVlcnhcMjhcMjPz8/sKAAAuCybs5TDHCnwgYtr2rSp2REAy3M4HMrMzNTp06dLSnujeXt7y9/fX8HBwQoLC1NQUBC7fv+kuLhYTW64UdleIXpy+g+G/9mcSNytSV0bafy4cXr++ecNXRvn2rBhg+7pfK8cxV7q8daXlph3L/1xWO2SsU+qZcubtWjhF8y7R5ns2LFDDzz4kA4mJqrzi1PV9O5eZkcqUVRYqLfur6fb292iObNnmx3HMIWFhdq8ebPZMeDmrrjiClWvXt3sGAAAuCxG6AAAKsXZg8uSkpK0fft2bdu2TUeOHFFmZmallPfSH0VEdna2jh8/rt27d2vLli1KTEzUqVOnVFRUVCnP6UrmzJmjrVs26/ZnxlfKjY2VU4eqRo149e/f3/C18f9xWC3clZVH6ti9vfW3bs/r83nzdOjQIbPjGIbd96gKXGcAAFQMBT4AwDBOp1MZGRlKTEzUli1btHv3bh0/flx5eXmm5CksLFRaWpoOHDigzZs3a+/evUpNTVVhYaEpecyUl5enocNe0XXtH1Dtxi0NX//Q5h+1bfUivTF2jPz9zZ/B7o44rBae4OxInRkzZmjHqvma2quFUg7sMDuWJKn5vY/LPzhMb789yewohqFYRVXIyspSKd/4DwAALoACHwBQYUVFRUpJSdG2bdu0f/9+paWlWa4kdzqdOn36tI4cOaItW7bo0KFDpt1YMMPkyZOVnJykjv3HGr620+nUV+8PUePrm6hbt26Grw8Oq4Xn6dWrlzasX69wP2lqr+bauHym2ZHkGxCkvz7UX9M/mq60tDSz4xiCAh9VoaioSDk51ng3DQAArogCHwBQbgUFBUpKStLWrVt19OhR5efnmx2pVJxOp06ePKnt27dr//79ys7ONjtSpUpPT9frY8aoxf3/UPXaVxu+/o7vl+rg7+s0ccJ4eXnxo4XRkpKS1Kp1G325bLl6TFikdo8PtURJnvj7Wk3t1Vx+Rbn6+aefdMcdd5gdCW7GiiN1bn54gAqLivXBBx+YmsMIZ86cUUFBgdkx4CG4WQQAQPnxKhsAUGZnzpxRYmKitm7dquPHj7v0fPmMjAzt3r1bu3btUkZGhlu+xXvs2DfkyC/UrX1fNXztosJCJUx+SR06dFSHDh0MX9/TbdiwQc2at9DBo8f15PS1urZdF7MjSfrjsNqPnm6vG65rpPW//qKGDRuaHQluymojdYIjotX0nj565933dObMGdNyGIFCFVWJ6w0AgPKjwAcAlFp+fr7279+vHTt2KC0tza3K7pycnJLfW2ZmptlxDJOYmKj33n9Ptzw2WCGRMYavv2Hpx0o9tEfjx48zfG1Px2G1wP9npZE6t3R/TqfS0/Tpp5+alsEIFKqoSjk5OZYbrwgAgKugwAcAXJbT6VRKSoq2b9+ujIwMs+NUqry8PO3bt08HDhxwi9ECrwwfrsDQCLXq/pzhaztys7Vq2mvq3qOHmjRpYvj6norDaoELs8pInaha9dSo/QMaP2Giy74Dzel0uv34OFgPc/ABACgfCnwAwCXl5ORo586dOnr0qIqLi82OU2VOnTql7du3KzU11WXfabBp0ybNnTNH7Z8cKb/AYMPXXzPnbeVlZ+j10aMNX9tTcVgtcGlWGanT+rHBOrB/n5YsWVLlz20Eh8PhUd/TYQ25ublmRwAAwCVR4AMALqioqEiHDx/Wrl27XH7Ob3kVFRXpyJEj2r17t0u+6Bw85EVF16mvZp0fN3ztrLQUrZk1XgMHDFTt2rUNX98TcVgtUHpmj9Sp1ai56jZrqzfeHOeSN3nZCQ0zcN0BAFA+FPgAgPOkp6dr27ZtOnHihNlRLOHP70JwlXEJCQkJWvXN1+rY/03Zvb0NX3/V9JEK8PPVsGFDDV/bE3FYLVB2Zo/Uaf3YEG3csF4//PBDlT2nUVzxpjRcH9cdAADlQ4EPAChRXFysxMREHTx4kIPGLiAlJUW7du1SXl6e2VEuqbi4WC8MHqIrm9yihm06G77+icTd+nXxNL0ybJiqVatm+PqehsNqgfI7O1Jn5MiR2rhshpJ3/15lz331zbcrrt61GjdufJU9p1EoUmGGgoICtzhfCACAqkaBDwCQJOXn52v37t1KS0szO4ql5eXladeuXcrMzDQ7ykXNmTNHW7ds1u3PjK+UESwrpw5VjRrx6t+/v+FrexIOqwWMc/JkmsJj4nVF45ZV9pw2m02tHhui//53hbZt21Zlz1tRTqeTAh+mYYwOAABlR4EPAFBWVpZ27tzJC/pSKioq0r59+3Ts2DHLzT7Oy8vT0GGv6Lr2D6h2JRRZhzb/qG2rF+mNsWPk72/+4aquisNqAeMUFxdrwRdfqFH7B+XlVbUvb67v9IiqxdbShIkTq/R5KyIvL48DbGEaftYEAKDsKPABwMOlpqZq7969jMwph+TkZB04cMBSc/EnT56s5OQkdew/1vC1nU6nvnp/iBpf30TdunUzfH1PwWG1gLF+/PFHHT+WrOtufajKn9vu7aObH/2n5s6Zo6NHj1b585cHBSrMxPUHAEDZUeADgIc6O+/+yJEjlttF7koyMjIsNRf/q5UrFRhaTf7BYYavveP7pTr4+zpNnDC+yne5ugsOqwWMN3/+giofn/NnLe7rK9+AIE2a9I4pz19WjDCBmbj+AAAoO159A4AHKigoYN69gaw0F3/mjBkK9PXW3BcfUGFBvmHrFhUWKmHyS+rQoaM6dOhg2LqehMNqAeOZOT7nLL+gELV4sJ8+nPahMjIyTMlQFuyAhpkKCwuVn2/czycAAHgCCnwA8DBny3tewBurqKhI+/fvN728iY+P15LFi3R0x3p9OX6AYe+u2LD0Y6Ue2qPx48cZsp4n4bBaoPKYOT7nz/7W9Rk5HPn617/+ZWqOy+EAW1gB1yAAAGVDgQ8AHuRsee9wOMyO4pacTqcOHDhgeonfsmVLffivf+nXxdP18xcfVHg9R262Vk17Td179FCTJk0qHtCDcFgtULkWLDB3fM5ZIVGxuvHuXpr0zruWGal2IXl5eYzNg+ko8AEAKBsKfADwEJT3VcMqJX6fPn00cOAzWv7Ws9q/4bsKrbVmztvKy87Q66NHGxPOQ3BYLVC5iouLNX+BueNz/qxV9+d1IjVFs2fPNjvKRVn55gI8B9chAABlY/5PugCASkd5X7WsUuK//fZbat26jT576UGlJyeWa42stBStmTVeAwcMVO3atY0N6MY4rBaofGaPz3E6nXLkZiszNUkpB3Yo93S64q6+XuMnTFRxcbEpmS6H2eOwAq5DAADKxtvsAACAykV5b46zJf5VV12l8PBwUzJ4e3trwfzP1ax5C81+4V7946N18gsMLtMaq6aPVICfr4YNG1pJKd3P/Pnz1at3b8XUa6wnJiyxxLz74qIifTX5Jf0wa6KeeKKvpk6dwrx7uLyKjM9xOp3KP5OjvOzMUv/jyM6QIydTjuxMncnO1Jns0youKjpv7asbXKOioiJLvCvgf/GzAKyA6xAAgLKhwAcAN0Z5by4rlPiRkZFa9uVS3dSypb4Y0Vvdxi0o9RiXE4m79eviaRo/bpyqVatWyUldn9Pp1IgRIzVq1Eg1ub2bHhj+sSXm3edln9bnw7tr97oVmjRpkp599llLjPIBKuLs+Jxa1/1NR7b9ctnyPb+keM9Q3iXKd0my2WwKCQ1VaGiYQsPCFB4WprjwMIXXrKmwsEYKCws775/w8PCSX0dGRsrHx6eK/0RKh58HYAWFhYUqKiqS3W43OwoAAC7B5izlKUYbN26s7CyAy2ratKnZEYDzFBYWateuXbxYtwCbzaZ69eopNDTUtAxLlixRly5d1OGpUbq17/BSPWb2kAeUsXeD9u7ZLX9/84toK8vNzVXvPn20YP58deo3Rm37vGyJkjw9OVGznrtH2amH9fm8ecy7h9tIS0tT7Tp1lJOdfc7HL1S+h4f/8b//W7Rf7J/g4GBL7p43wrZt2/i5AJZwzTXXKDAw0OwYAAC4BHbgA4AbOrvzmxfp1nD2v0eDBg1MK8Lvu+8+jRw5Uq+99qpi6zVWo7b3XvLrD23+UdtWL9KsWbMo7y8jKSlJne+9T9t37FCPCYssM+8+8fe1mj24i6LCQ/XNTz8x7x5uJTIyUr/8/LMyMjI8pnyvKKfTyexxWIbD4aDABwCglNiBDxiAHfiwmsOHD+vEiRNmx8D/8Pf3V4MGDUx7y3hxcbEeeuhhrfhqpZ7690+KrXftBb/O6XRq2t9bKcSZo02/baQMu4QNGzbons73ylHspR5vfan4BjeYHUnSH4fVLhn7pFq2vFmLFn6hqKgosyMBMFl+fr62bt1qdgxAklSzZk3FxMSYHQMAAJfAK3IAcDMnT56kvLeovLw8HTx4UKW8d244Ly8vzZw5Q/XqXqXZL9yr3Mz0C37dju+X6uDv6zRxwnjK+0uYP3++WrVuLb+oWuo3c70lyvvioiKteHewvhjZR7169tI3XydQ3gOQxPx7WAvXIwAApcercgBwI9nZ2Tp8+LDZMXAJmZmZSk5ONu35g4OD9eXSJSo+k6nPhnZVUWHhOZ8vKixUwuSX1KFDR3Xo0MGklNbmdDr12msj1LVrVzVo00V9//WdQqJizY6lvOzTmvXCfVo7521NmjRJ06dPk6+vr9mxAFgEhSmshOsRAIDSo8AHADeRn5+v/fv3m7a7G6V3/PhxpadfePd7Vbjyyiu18IsvdGDDt1rx7gvnfG7D0o+VemiPxo8fZ1I6a8vNzVXXRx7RqFEj1anfGHUdPVs+fuafEZCenKgP+/5NRzf/oOXLl2vQoEGWOEQXgHVQmMJKuB4BACg9CnwAcAPFxcXav3+/Cv9nNzWs69ChQ8rNzTXt+du2bat3331X6z57Vxu+/ESS5MjN1qppr6l7jx5q0qSJadmsKikpSa1at9GXy5arx4RFavf4UEuU5Im/r9XUXs3lV5Srn3/6SXfccYfZkQBYEIUprCQ/P59NJwAAlJK32QEAABVndhmMsjt706VBgwby8fExJUO/fv30+++bNeONp1S9TgPt/eVr5WVn6PXRo03JY2V/Pqz2yelrLTHvXuKwWgCll5+fb3YEoITT6VRBQQGj3gAAKAV24AOAi0tNTTV1HAvKLz8/39RDbW02m6ZMmazmzZtrzpD7tWbWeA0cMFC1a9c2JY9VcVgtAHfAu/RgNVyTAACUDgU+ALiwvLw8HT161OwYqICsrCylpqaa9vy+vr5avGihgv28FeDnq2HDhpqWxWo4rBaAOykqKjI7AnAOrkkAAEqHEToA4KKcTqepu7dhnKSkJIWGhiogIMCU54+JidG2rVvk7e2tkJAQUzJYTW5urnr36aMF8+erU78xatvnZUvMu09PTtSs5+5RduphLV++nHn3AEqN3c6wGgp8AABKhwIfAFzUsWPHmHvvJpxOpxITE9WgQQPTSuJq1aqZ8rxWlJSUpM733qftO3aox4RFurZdF7MjSfrjsNrZg7soKjxU3/z0kxo2bGh2JAAugqIUVsRNJQAASocROgDggnJzc3X8+HGzY8BAubm5OnbsmNkxPN6GDRvUrHkLHTx6XE9OX2uZ8n7Dshn66On2uuG6Rlr/6y+U9wDKhAIfVsR1CQBA6VDgA4CLObtbm9E57uf48eO8q8JEHFYLwF2x0xlWxHUJAEDpUOADgIs5fvy4zpw5Y3YMVAKn06lDhw5xc6aKcVgtAHfHTmdYEdclAAClwwx8AHAheXl5jFlxc7m5uUpJSVFsrPkFsifgsFoAnoCiFFbEdQkAQOlQ4AOAi2B3tudITk5WtWrV5OfnZ3YUt8ZhtQA8BaNKYEVclwAAlA4jdADARaSnpys7O9vsGKgCTqdTR44cMTuGW+OwWgCehJ3OsCKuSwAASocCHwBcQHFxsZKSksyOgSqUmZmprKwss2O4JQ6rBeBp2OkMK6LABwCgdCjwAcAFpKamqqCgwOwYqGJHjx5lZJKBOKwWgKeiKIUVcWMJAIDSocAHAIsrLCzU8ePHzY4BE+Tm5urUqVNmx3ALubm56vrIIxo1aqQ69RujrqNny8fP3+xYSk9O1Id9/6ajm3/Q8uXLNWjQIEscogvAvXAzuOosW7ZMzZo10z333GN2FMvjugQAoHQ4xBYALO7YsWPsnPNgSUlJCg8Pl5cX99zLi8NqAXg6KxalTqdTq1at0ldffaVdu3bp1KlT8vLyUkREhKKiotSoUSPdcMMNat68uYKDg82Oi0pgxesSAAArosAHAAtzOBw6ceKE2TFgovz8fJ08eVLR0dFmR3FJGzZs0D2d75Wj2EtPTl9riXn30h+H1S4Z+6RatrxZixZ+wbx7AJXKakVpVlaWnn/+ef32228lH7Pb7QoODtbx48eVlJSkzZs3a+7cuXrttdfYzQ4AADwaBT4AWFhSUpLlXnSj6h07dkyRkZGy2+1mR3Ep8+fPV6/evRVTr7GemLDEEvPui4uK9NXkl/TDrIl64om+mjp1CvPuAXicV199Vb/99pvsdrseffRR3X///apZs6a8vLxUWFiogwcP6scff9TKlSvNjopKxM+4AACUDgU+AFhUTk4O888h6f+fgxAfH292FJfgdDo1YsRIjRo1Uk1u76YHhn9siXn3edmn9fnw7tq9boUmTZqkZ599lnn3AKqElYrSw4cPa82aNZKkp59+Wr179z7n897e3vrLX/6iv/zlL+rVq5fy8vJMSImqYKXrEgAAK6PABwCLSkpKMjsCLCQlJUXVq1dnt/Zl5ObmqnefPlowf7469Rujtn1etkRJnp6cqFnP3aPs1MNavny57rjjDrMjAYAp9uzZU/LrNm3aXPbr/f0vfAP26NGjmjt3rn799VelpKSouLhYcXFxatmypbp3767Y2PPfdbVs2TKNHDlScXFxWrZsmXbu3KmZM2dq06ZNOn36tKKjo9WmTRv17dtXoaGhF820detWzZgxQ7///rvy8vIUExOjW2+9VX369CnFn4CUnZ2tzz//XD/88IMOHz6svLw8RURE6Prrr9ejjz6q66677rzHJCcnq3PnzpKkL7/8UsXFxZo5c6Z++eUXnThxQlFRUVq2bFmpnh8AALgWCnwAsKDMzExlZWWZHQMW4nQ6dezYMdWuXdvsKJbFYbUA4FpSUlJ05ZVXlvlxixcv1rhx41RYWChJ8vX1lc1mU2JiohITE/Xll19q3Lhxuummmy66xldffaURI0aosLBQwcHBKioqUlJSkubOnauff/5ZM2bMUGBg4HmPW7p0qcaMGaPi4mJJUnBwsI4dO6ZPPvlE3377rbp0ufT3nm3btun5559XWlqapD9m//v7+yslJUUJCQn6+uuv1a9fv0veDNiyZYvGjh2r3Nxc+fv7y9ubl/UAALgzL7MDAADOl5KSYnYEWFBaWpoKCgrMjmFJGzZsULPmLXTw6HE9OX2tZcr7Dctm6KOn2+uG6xpp/a+/UN4D8HgNGzYseWfUO++8o0OHDpXp8d99953GjBkjSerdu7eWLVumdevWae3atfriiy902223KScnRy+++KKOHz9+wTVOnTqlUaNG6e6779by5cv13Xff6YcfftCQIUPk7e2tAwcO6NNPPz3vcbt27dLYsWNVXFyspk2b6osvvtB3332nNWvWaMyYMUpLS9NHH3100ezJyckaOHCg0tLSdOutt2r27Nlat26dvv/+eyUkJKhv377y8vLSlClT9N133110nbFjx+qqq67Sp59+qrVr12rNmjWaPHlymf4cAQCA66DABwCLyc3NZfc9LsjpdCo1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Xr166YorrpD0x8G469evV0JCgiZOnFjyuLZt26pdu3b69ttvNXnyZB07dkzdunVT7dq1JUkFBQXavXu3Vq9eraVLl2ru3LmKiYkp0++1Y8eOeuutt5SZmakRI0ZIkq688kpdc801F/z6qKgoDRw4UBMnTtTatWvVv39/PfXUU7ruuutkt9vldDp16NAhrV27VkuXLlWfPn105513lilTZTP6uqSohxVR1ANVIyIiQidPnizVYfNwbTabTbVq1TI7BgAAHosCHwAuYuPyGUpJ3K03F31W5se64g58SapXr55at26t77//Xp988ok6d+6ssLAwTZkyRYMHD9aePXv0zjvv6J133lFISEjJTvOzLvTi7umnn1ZOTo4WLFigpUuXaunSpQoMDFRxcbHy8vIkScHBwec9bvTo0Ro1apQSEhK0cOFCLVy4UAEBAfLx8VF2dvY5B+yW5+3coaGhuuWWW/Ttt99qx44dki6++/6sRx55RPn5+ZoyZYo2bNigvn37ysfHR4GBgcrJyVFhYWGFMlW2ilyX+fn5WrhwIUU9LIOiHjCXzWZT7dq1tWPHDjmdTrPjoBLVqFGj1GMQAQCA8VyzYQKASlaQd0arp41Q10ce0Q033FDmx7vqDnxJevzxx/X9998rJSVFixYt0iOPPKL4+Hh9+umnWrlypb755hvt3LlTGRkZstvtio+P19VXX61WrVqpdevW561nt9v14osvqlOnTlq4cKE2bdqk9PR0+fn5qUaNGrruuuvUqVOn8x7n7++vsWPH6v7779eXX36pzZs36+TJk8rNzVW1atV01VVXqWXLlmrXrp2io6PL9Xu966679O2330r6YxzAHXfccdnH9OzZU+3atdOCBQu0fv16JScnKzs7W0FBQapZs6aaNWumtm3b6rrrritXpspUkeuyqKhI3bp1U3C1KMVd3YSiHlWGoh6wLn9/f8XFxZXpsHq4lsDAwDK/yxEAABjL5izldomNGzdWdhbAZTVt2tTU5589e7Yee+wxvf5jnrx92R1jhO8/naCvpw7Vzp07Va9evTI/Pjc3Vzt37qyEZED5xcbGKj4+vtyPDwgM1G1Pj9Ut3QYZFwr4P2Up6q9t1EiNGjWiqAcswOl0aufOnTpz5ozZUWAwm82mBg0aKDAw0OwoAAB4NHbgA8D/OJOVoR9mvKG+ff9ervJect0ROnBvFb0uIyIilZuZZlAaeCp21APuxWazqU6dOtq1axejdNxMbGws5T0AABZAwwQA/+P7T8eruNChV18dXu41XHmEDtxXRa/LyEgKfJQeRT3gOQIDAxUbG6tjx46ZHQUGCQwMVFxcnNkxAACAKPAB4BynTx7Tj5+9oxee+2eFXrRQ4MOKKroDPyoqUmkU+PgfFPUAJCkuLk6ZmZnKzc01Owoq6Oy7Kmw2m9lRAACAKPAB4Byrpo9SUGCAhgwZUuG1vL29VVhYaEAqwBgVvbEUFRmpo4co8D0VRT2AS7HZbLryyiu1Y8cORum4uPj4eAUEBJgdAwAA/B8KfAD4PycP79X6JdM17s03FRYWVuH17HY7BT4sxYgROme27DUoDayKoh5Aefn7+6tmzZo6cuSI2VFQTiEhIYqOjjY7BgAA+BMKfAD4PwkfDFdsbJz69+9vyHre3t5yOByGrAUYoaIjdJiB714o6gFUhujoaOXk5Cg9Pd3sKCgjX19fXXnllYzOAQDAYijwAUBS0q7ftOXrz/XRRx8Z9pZhX19f5eTkGLIWUFE2m00+Pj4VWiMyMlI5FPguh6IeQFWrXbu28vLymIfvQry8vFS3bt0K/6wAAACMR4EPAJJWTn5ZV9dvoF69ehm2pp+fn2FrARXl6+tb4R11kZGRcpzJVYEjTz5+/gYlg1Eo6gFYxdkyeOfOnYwTdBG1a9dWYGCg2TEAAMAFUOAD8Hj71q/Wnp8TtHDhwgqPGPkzCnxYiRHXY2RkpCQpNzNNYdHxFV4P5UNRD8AV+Pr6qm7dutqzZw+H2lpcbGws3x8AALAwCnwAHs3pdCph8ktq1ryFunTpYujaFPiwEgp810NRD8DVBQcH64orrtChQ4fMjoKLCAsLU40aNcyOAQAALoECH4BH27Z6kQ5vX68Zq1cbfmAXBT6sxOgCH8ahqAfgzqKiopSbm6sTJ06YHQX/w9/fn0NrAQBwART4ADxWUWGhvvlgmDp27KR27doZvr6Pj49sNhtvG4cl+Pr6VniNkgI/gwK/PCjqAXiqWrVqKS8vT1lZWWZHwf+x2+2qW7eu7Ha72VEAAMBlUOAD8Fgbl89QSuJuvbnos0pZ32azydfXVw6Ho1LWB8rCiB344eHh8vLyYgf+ZVDUA8C5bDabrrrqKu3atYufiyzg7H8Pf38OpAcAwBVQ4APwSAV5Z7R62gh1feQR3XDDDZX2PH5+frxQhSUYUeB7eXkpLLwaBf7/oagHgNLz9vZW/fr1tXv3bn42MtHZ8j40NNTsKAAAoJQo8AF4pB/nT1Z2eopeHz26Up+HOfiwAm9vb8PeIh8RGakcDyvwKeoBwBg+Pj6U+CY6W96Hh4ebHQUAAJQBBT4Aj3MmK0M/zHhDffv+XfXq1avU56LAhxUYeR1GRUa67Q58inoAqHyU+OagvAcAwHVR4APwON9/Ol7FhQ69+urwSn8uIw4OBSrKyOswKipS+128wKeoBwBzUeJXLcp7AABcGwU+4EbWffauvOzu/X9rv+BQNev8uLy8vMr1+NMnj+nHz97RC8/9U3FxcQanOx+Hg8EKjLwOoyIjtePIXsPWq0wU9QBgXZT4VYPyHgAA1+feTR/gIZo1a6a4GvFaM+N1s6NUKqfTqZzsbAUEh+u62x4s1xqrpo9SYIC/Bg8ebHC6C/P395fNZpPT6ayS5wMuJDAw0LC1IiMjlZv5s2HrGYGiHgBcEyV+5aK8BwDAPVDgA26gQYMGSk46anaMSudwOOTv76/C/LxyPf7k4b1av2S6xr35ZpW9kLHZbAoMDFROTk6VPB9wIcYX+OaM0KGoBwD3c7bE37dvn3Jzc82O4zbsdruuvPJKhYWFmR0FAABUEAU+AI+R8MFwxcbGqX///lX6vBT4MJO3t7ehM/AjIyOVnZmu4uLico+yuhyKegDwLGdL/MOHDystzbXPWbECf39/1a1bl1GOAAC4CQp8AB7h6M6N2vL15/roo48UEBBQpc8dFBSkEydOVOlzAmcFBQUZul5kZKScxcXKy85UYGi1Cq1FUQ8AOMvLy0t16tRRYGCgjh49yvjBcgoPD1edOnVkt9vNjgIAAAxCgQ/AIyRMGaqr6zdQr169qvy5jRxfApSV0ddfZGSkJCk3M63UBT5FPQCgtKKjoxUQEKADBw6osLDQ7DgupUaNGoqNjZXNZjM7CgAAMBAFPgC3t2/9au35OUELFy6Ut3fV/7Xn7+8vLy8vFRcXV/lzA5VZ4KtWvXM+R1EPADBCSEiIrrnmGu3fv5+5+KXAvHsAANwbBT4At+Z0OpUw+SU1a95CXbp0MSXD2YNss7OzTXl+eLbKGKEjSUe2/aJTSQcp6gEAlcLX15e5+KXAvHsAANwfBT4At7Zt9SId3r5eM1avNvXtxBT4MIOPj498fHwMXTMyMlKBQUFaNvFZSRT1AIDKc3YufmhoqI4ePaqCggKzI1lKTEyM4uLimHcPAICbo8AH4LaKCgv1zQfD1LFjJ7Vr187ULMzBhxkq47rz9/fXpt9+U2pqKkU9AKBKREREKCwsTElJSTpx4oTZcUwXFBSkK664gp8vAQDwEBT4ANzWxuUzlJK4W28u+szsKIaPMQFKo7Kuu6uvvlpXX311pawNAMCF2O12XXHFFYqMjNShQ4d05swZsyNVObvdrvj4eEVFRXFQLQAAHoQCH4BbKsg7o9XTRqjrI4/ohhtuMDuO/Pz8OMgWVY6deQAAdxMUFKRrrrlGqampSk5O9pifrapVq6ZatWoZPhoPAABYHwU+ALf04/zJyk5P0eujR5sdRdIfB9kGBwfr9OnTZkeBB+GdHwAAd2Sz2RQTE6Nq1arpyJEjysjIMDtSpfH391etWrUUGhpqdhQAAGASCnwAbudMVoZ+mPGG+vb9u+rVq2d2nBKhoaEU+KgyQUFB8vbm2zwAwH35+vqqbt26OnPmjFJSUpSeni6n02l2LEMEBwcrJiZGYWFhjMsBAMDD8coegNv5/tPxKi506NVXh5sd5RzsnEJV4noDAHiKgIAA1alTR/Hx8UpNTdWJEydUVFRkdqxyCQ8PV2xsLO+iAwAAJSjwAbiV0yeS9eNn7+iF5/6puLg4s+OcIyAgQD4+PiooKDA7CjwABT4AwNP4+PgoPj5esbGxOnnypFJTU5Wfn292rMuy2WyKiopSdHS0/P39zY4DAAAshgIfgFtZ9dFoBQb4a/DgwWZHuaDQ0FClpaWZHQNuzm63s3MPAOCx7Ha7YmJiFB0drczMTGVkZCgzM1OFhYVmRyths9kUEhKisLAwRUREMPYOAABcFD8lAHAbJw/v1fol0zXuzTcVHh5udpwLosBHVQgJCWFeLgDA49lsNoWHhys8PFxOp1M5OTnKzMxUZmamzpw5U+V5vL29FRYWprCwMIWGhsput1d5BgAA4Hoo8AG4jYQPhis2Nk79+/c3O8pFMdYEVYHrDACAc9lsNgUHBys4OFjx8fFyOBzKzMzU6dOnlZeXp/z8fMMPwPXx8ZGfn5+Cg4MVFhamoKAgbrADAIAyo8AH4BaO7tyoLV9/ro8++kgBAQFmx7kob29vBQYGKjc31+wocGMU+AAAXJqfn5+io6MVHR0tSXI6nXI4HMrLyzvnfx0Oh4qLi1VcXCyn0ymn0ymbzXbOP76+vvL395efn5/8/f1Lfs0OewAAYAQKfLi8pk2bmh0BFpAwZaiurt9AvXr1MjvKZYWGhlLgo9L4+fnJz8/P7BgAALgUm81WUr4DAABYCQU+AJe3b/1q7fk5QQsXLnSJA8BCQ0N1/Phxs2PATbH7HgAAAAAA9+FldgAAqAin06mEyS+pWfMW6tKli9lxSiU4OFheXvz1i8oRFhZmdgQAAAAAAGAQ629VBYBL2LZ6kQ5vX68Zq1e7zKFgNptNoaGhysjIMDsK3MzZA/oAAAAAAIB7YAsoAJdVVFiobz4Ypg4dOqpdu3ZmxymTiIgIsyPADYWHh3NgHgAAAAAAboQd+ABc1sblM5SSuFtvLpxrdpQyCwsLk91uV1FRkdlR4Ea4MQQAAAAAgHthBz4Al1SQd0arp43Qw1276sYbbzQ7Tpl5eXkpPDzc7BhwI3a7nfn3AAAAAAC4GQp8AC7px/mTlZ2eojGvv252lHJjtzSMFBER4TLnQAAAAAAAgNKhwAfgcs5kZeiHGW/oiSf6ql69embHKbeQkBD5+PiYHQNughtCAAAAAAC4Hwp8AC5n7dxJKirI02uvvWp2lAqx2WyUrjCEr6+vgoKCzI4BAAAAAAAMRoEPwOWkJx3QPwcNUlxcnNlRKowCH0ZgfA4AAAAAAO6JAh+AywmvVk1DhgwxO4YhAgMD5e/vb3YMuDhuBAEAAAAA4J68zQ4AAKVlt9vVvUdP3fK3lgoPDzc7jmEiIiKUnJxsdgy4qICAAAUEBJgdAwAAAAAAVAIKfAAuw9vbW7NnzTQ7huEo8FER7L4HAAAAAMB9MUIHAEzm5+enkJAQs2PABdlsNkVGRpodAwAAAAAAVBIKfACwgOjoaLMjwAVVq1ZNPj4+ZscAAAAAAACVhAIfACwgLCxMfn5+ZseAi4mJiTE7AgAAAAAAqEQU+ABgATabjTIWZRISEqLAwECzYwAAAAAAgEpEgQ8AFhEZGSm73W52DLgIbvgAAAAAAOD+KPABwCK8vLxUvXp1s2PABfj5+Sk0NNTsGAAAAAAAoJJR4AOAhURHR8tms5kdAxYXExPDdQIAAAAAgAegwAcAC/Hx8VG1atXMjgELs9vtioyMNDsGAAAAAACoAhT4AGAxzDbHpVSvXl1eXnz7BgAAAADAE9AAAIDFBAYGKiQkxOwYsCCbzabo6GizYwAAAAAAgCpCgQ8AFsQufFxIZGSkfHx8zI4BAAAAAACqCAU+AFhQWFgYu/BxDpvNpri4OLNjAAAAAACAKkSBDwAWFR8fb3YEWEhMTIx8fX3NjgEAAAAAAKoQBT4AWFRQUJCqVatmdgxYgLe3t2JjY82OAQAAAAAAqhgFPgBYWHx8vGw2m9kxYLK4uDjZ7XazYwAAAAAAgCpGgQ8AFubn56fq1aubHQMm4hoAAAAAAMBzUeADgMWx+9qz1ahRg3dhAAAAAADgoSjwAcDimH/uuQIDAzkHAQAAAAAAD0aBDwAuIDo6Wj4+PmbHQBWrWbMmu+8BAAAAAPBgFPgA4AK8vLwUHx9vdgxUobCwMIWEhJgdAwAAAAAAmIgCHwBcREREhIKDg82OgSpgs9lUq1Yts2MAAAAAAACTUeADgIuw2WyqXbs2I1U8QI0aNeTn52d2DAAAAAAAYDIKfABwIf7+/oqLizM7BipRYGCgYmJizI4BAAAAAAAsgAIfAFxMbGysAgICzI6BSsC7LAAAAAAAwJ9R4AOAi7HZbKpTpw4lrxuKjY1VYGCg2TEAAAAAAIBFUOADgAsKDAxUbGys2TFgoMDAQMYjAQAAAACAc1DgA4CLiouLY7e2m+BdFQAAAAAA4EIo8AHARdlsNl155ZWUvm4gPj6ecw0AAAAAAMB5KPABwIX5+/urZs2aZsdABYSEhCg6OtrsGAAAAAAAwIIo8AHAxUVHRysiIsLsGCgHX19f3kUBAAAAAAAuigIfANxA7dq1mYfvYry8vFS3bl35+PiYHQUAAAAAAFgUBT4AuIGzZbC3t7fZUVBK3HQBAAAAAACXQ4EPAG7C19dXdevWZRyLC4iNjWXsEQAAAAAAuCwKfABwI8HBwbriiivMjoFLCAsLU40aNcyOAQAAAAAAXAAFPgC4maioKFWvXt3sGLgAf39/Dq0FAAAAAAClRoEPAG6oVq1aCgkJMTsG/sRut6tu3bqy2+1mRwEAAAAAAC6C0w5Rbk2bNjU7AoCLsNlsuuqqq7Rr1y45HA6z43i8s/89/P39zY4CAAAAAABcCDvwAcBNeXt7q379+vLz8zM7ikc7W96HhoaaHQUAAAAAALgYCnwAcGM+Pj6U+CY6W96Hh4ebHQUAAAAAALggCnwAcHOU+OagvAcAAAAAABVFgQ8AHoASv2pR3gMAAAAAACNQ4AOAh6DErxqU9wAAAAAAwCgU+ADgQSjxKxflPQAAAAAAMBIFPgB4mLMlfmBgoNlR3IrdblfdunUp7wEAAAAAgGEo8AHAA50t8SMjI82O4hb8/f3VoEEDhYWFmR0FAAAAAAC4EW+zAwAAzOHl5aU6deooMDBQR48eldPpNDuSSwoPD1edOnVkt9vNjgIAAAAAANwMBT4AeLjo6GgFBATowIEDKiwsNDuOS6lRo4ZiY2Nls9nMjgIAAAAAANwQI3QAAAoJCdE111zDXPxSstvtqlevnuLi4ijvAQAAAABApaHABwBIknx9fZmLXwrMuwcAAAAAAFWFEToAgBJn5+KHhobq6NGjKigoMDuSpcTExCguLo559wAAAAAAoEpQ4AMAzhMREaGwsDAlJSXpxIkTZscxXVBQkK644gpGDAEAAAAAgCpFgQ8AuCC73a4rrrhCkZGROnTokM6cOWN2pCpnt9sVHx+vqKgoZt0DAAAAAIAqR4EPALikoKAgXXPNNUpNTVVycrKKi4vNjlQlqlWrplq1asnHx8fsKAAAAAAAwENR4AMALstmsykmJkbVqlXTkSNHlJGRYXakSuPv769atWopNDTU7CgAAAAAAMDDUeADAErN19dXdevW1ZkzZ5SSkqL09HQ5nU6zYxkiODhYMTExCgsLY1wOAAAAAACwBAp8AECZBQQEqE6dOoqPj1dqaqpOnDihoqIis2OVS3h4uGJjYxUUFGR2FAAAAAAAgHNQ4AMAys3Hx0fx8fGKjY3VyZMnlZqaqvz8fLNjXZbNZlNUVJSio6Pl7+9vdhwAAAAAAIALosAHAFSY3W5XTEyMoqOjlZmZqYyMDGVmZqqwsNDsaCVsNptCQkIUFhamiIgIeXvzLRAAAAAAAFgb7QUAwDA2m03h4eEKDw+X0+lUTk6OMjMzlf4qaLAAAASpSURBVJmZqTNnzlR5Hm9vb4WFhSksLEyhoaGy2+1VngEAAAAAAKC8KPABAJXCZrMpODhYwcHBio+Pl8PhUGZmpk6fPq28vDzl5+cbfgCuj4+P/Pz8FBwcrLCwMAUFBXEgLQAAAAAAcFkU+ACAKuHn56fo6GhFR0dLkpxOpxwOh/Ly8s75X4fDoeLiYhUXF8vpdMrpdMpms53zj6+vr/z9/eXn5yd/f/+SX7PDHgAAAAAAuBMKfACAKWw2W0n5DgAAAAAAgPN5mR0AAAAAAAAAAACcjwIfAAAAAAAAAAALosAHAAAAAAAAAMCCKPABAAAAAAAAALAgCnwAAAAAAAAAACyIAh8AAAAAAAAAAAuiwAcAAAAAAAAAwIIo8AEAAAAAAAAAsCAKfAAAAAAAAAAALIgCHwAAAAAAAAAAC6LABwAAAAAAAADAgijwAQAAAAAAAACwIAp8AAAAAAAAAAAsiAIfAAAAAAAAAAALosAHAAAAAAAAAMCCKPABAAAAAAAAALAgCnwAAAAAAAAAACyIAh8AAAAAAAAAAAuiwAcAAAAAAAAAwIIo8AEAAAAAAAAAsCAKfAAAAAAAAAAALIgCHwAAAAAAAAAAC6LABwAAAAAAAADAgijwAQAAAAAAAACwIJvT6XSaHQIAAAAAAAAAAJyLHfgAAAAAAAAAAFgQBT4AAAAAAAAAABZEgQ8AAAAAAAAAgAVR4AMAAAAAAAAAYEEU+AAAAAAAAAAAWBAFPgAAAAAAAAAAFkSBDwAAAAAAAACABVHgAwAAAAAAAABgQRT4AAAAAAAAAABYEAU+AAAAAAAAAAAWRIEPAAAAAAAAAIAFUeADAAAAAAAAAGBBFPgAAAAAAAAAAFgQBT4AAAAAAAAAABZEgQ8AAAAAAAAAgAVR4AMAAAAAAAAAYEEU+AAAAAAAAAAAWBAFPgAAAAAAAAAAFkSBDwAAAAAAAACABVHgAwAAAAAAAABgQRT4AAAAAAAAAABYEAU+AAAAAAAAAAAWRIEPAAAAAAAAAIAFUeADAAAAAAAAAGBBFPgAAAAAAAAAAFgQBT4AAAAAAAAAABZEgQ8AAAAAAAAAgAVR4AMAAAAAAAAAYEEU+AAAAAAAAAAAWBAFPgAAAAAAAAAAFkSBDwAAAAAAAACABVHgAwAAAAAAAABgQRT4AAAAAAAAAABYEAU+AAAAAAAAAAAWRIEPAAAAAAAAAIAFUeADAAAAAAAAAGBBFPgAAAAAAAAAAFgQBT4AAAAAAAAAABZEgQ8AAAAAAAAAgAVR4AMAAAAAAAAAYEEU+AAAAAAAAAAAWBAFPgAAAAAAAAAAFkSBDwAAAAAAAACABVHgAwAAAAAAAABgQRT4AAAAAAAAAABYEAU+AAAAAAAAAAAWRIEPAAAAAAAAAIAFUeADAAAAAAAAAGBBFPgAAAAAAAAAAFgQBT4AAAAAAAAAABZEgQ8AAAAAAAAAgAVR4AMAAAAAAAAAYEEU+AAAAAAAAAAAWBAFPgAAAAAAAAAAFkSBDwAAAAAAAACABVHgAwAAAAAAAABgQRT4AAAAAAAAAABYEAU+AAAAAAAAAAAWRIEPAAAAAAAAAIAF/T8DNnwXBPbXFgAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%matplotlib inline\n", + "\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from matplotlib.patches import Circle, Rectangle, Polygon, Arrow, FancyArrow\n", + "\n", + "def directory_polygon(x,y,box_bg,arrow1):\n", + " return [ Rectangle((x, y), 0.8, 1.1, zorder=1, fc=box_bg),\n", + " Rectangle((x+0.2, y+0.2), 0.8, 1.1, zorder=2, fc=box_bg),\n", + " Rectangle((x+0.4, y+0.4), 0.8, 1.1, zorder=3, fc=box_bg),\n", + " FancyArrow(x+1.4,y+0.6, 0.35, 0, fc=arrow1, width=0.25, \n", + " head_width=0.5, head_length=0.2),\n", + " Circle((x+2.5, y+0.6), 0.5, fc=box_bg)\n", + " ]\n", + "\n", + "\n", + "def create_base(box_bg = '#CCCCCC',\n", + " arrow1 = '#88CCFF',\n", + " arrow2 = '#88FF88',\n", + " supervised=True):\n", + " \n", + " fig = plt.figure(figsize=(15, 15), facecolor='w')\n", + " ax = plt.axes((0, 0, 1, 1),\n", + " xticks=[], yticks=[], frameon=False)\n", + " ax.set_xlim(0, 9)\n", + " ax.set_ylim(0, 6)\n", + " \n", + " x=0\n", + " y=3.6\n", + " patches = []\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=0.2\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1)) \n", + " y=1.8\n", + " patches.extend(directory_polygon(x,y,box_bg,arrow1))\n", + " \n", + " len=1.8\n", + " patches.extend( \n", + " [ \n", + " FancyArrow(3.1, 4.3, len, -0.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 4.1, len, -1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 3.8, len, -2.1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(3.1, 2.6, len, 1.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 2.4, len, 0, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 2.2, len, -0.9, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " FancyArrow(3.0, 1.2, len, 2.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " FancyArrow(3.1, 0.9, len, 1, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ), \n", + " FancyArrow(3.1, 0.7, len, 0.2, fc=arrow1, width=0.11, head_width=0.22, head_length=0.2 ),\n", + " \n", + " Circle((5.8, 3.9), 0.5, fc=box_bg),\n", + " Circle((5.8, 2.4), 0.5, fc=box_bg),\n", + " Circle((5.8, 1.1), 0.5, fc=box_bg)\n", + " ])\n", + " for p in patches:\n", + " ax.add_patch(p)\n", + " \n", + " plt.text(2.2,0.75, 'Receiver', fontsize=18)\n", + " plt.text(2.2,2.35, 'Receiver', fontsize=18)\n", + " plt.text(2.2,4.15, 'Receiver', fontsize=18)\n", + " plt.text(5.5,1.05, 'Sender', fontsize=18)\n", + " plt.text(5.5,2.35, 'Sender', fontsize=18)\n", + " plt.text(5.5,3.85, 'Sender', fontsize=18)\n", + "create_base()\n", + "plt.text(3.0, 5, 'Sundew Component Design',fontsize=36)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a0ace820", + "metadata": {}, + "source": [ + "What difference did it make?\n", + "----------------------------------------\n", + "\n", + "\n", + "On a given linux server used to run both applications for benchmarking purposes:\n", + "\n", + "* The original (PDS) C-code was performing 10-20 product routings per second, \n", + "\n", + "* Sundew (the python replacement) was peaking at 150 messages per second with files,\n", + "\n", + "* Sundew was file system bound, and was also a message router, capable of routing over 400 messages/second.\n", + "\n", + "It feels safe to say it was approximately an order of magnitude improvement in performance.\n", + "\n", + "Note:: These tests were done in 2005, using, for example, hard disks for storage. Performance on current hardware is far superior. \n" + ] + }, + { + "cell_type": "markdown", + "id": "413563af", + "metadata": {}, + "source": [ + "Bonus\n", + "----------\n", + "\n", + "The original PDS application was surrounded by scripts and code to do many kinds of\n", + "name processing, and with the original built in C, this was done with shell scripts.\n", + "As a happy side-effect of implementing in Python, it became feasible to implement\n", + "a plugin architecture to customize processing in a much more efficient way.\n", + "\n", + "Instead of forking of processes, as required by additional functionality in PDS, we could define routines that would be called from the python code. These routines could be written in Python, and thus save enormous amounts of overhead from the fork/reap pattern.\n", + "\n", + "While plugin architectures can be implemented in C, they are much more daunting, and having operations people writing C is a high bar to customize processing.\n", + "\n", + "So a great deal of peripheral, or customized processing was accellerated by the use of Python as the implementation language.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "89650e5c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/_sources/Contribution/Philosophy/index.rst.txt b/_sources/Contribution/Philosophy/index.rst.txt new file mode 100644 index 000000000..84dabdac4 --- /dev/null +++ b/_sources/Contribution/Philosophy/index.rst.txt @@ -0,0 +1,11 @@ + +Sarracenia Design Philosophy +============================ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + How to run the examples + Amdahl applied + CAP Theorem diff --git a/_sources/Contribution/Release.rst.txt b/_sources/Contribution/Release.rst.txt new file mode 100644 index 000000000..e1def4d92 --- /dev/null +++ b/_sources/Contribution/Release.rst.txt @@ -0,0 +1,606 @@ +============================ + Releasing MetPX-Sarracenia +============================ + +:version: |release| +:date: |today| + + +Pre-Release Overview +-------------------- + +MetPX-Sarracenia is distributed in a few different ways, and each has it's own build process. +Packaged releases are always preferable to one off builds, because they are reproducible. + +To publish a pre-release one needs to: + +- starting with the development branch (for sr3) or v2_dev (for v2.) +- run QA process on all operating systems looking for regressions on older 3.6-based ones. + + - github runs flow tests for ubuntu 20.04 and 22.04, review those results. + - github runs unit tests (only work on newer python versions.), review those results. + - find ubuntu 18.04 server. build local package, run flow tests. + - find redhat 8 server. build package: *python3 setup.py bdist_rpm*, run flow tests + - find redhat 9 server, build package: *python3 -m build --no-isolation*. run flow tests + +- review debian/changelog and update it based on all merges to the branch since previous release. +- Set the pre-release tag. +- commit the above. + +- pypi.org + + - to ensure compatiblity with python3.6, update a python3.6 branch (for redhat 8 and/or ubuntu 18.) + - use the python3.6 branch to release to pypi (because upward compatibility works, but not downward.) + - upload the pre-release so that installation with pip succeeds. + +- launchpad.org: + + * ensure the two branches are ready on github. + * pre-release branch ready. + * pre-release_py36 branch ready. + * update git repository (Import now): https://code.launchpad.net/~ssc-hpc-chp-spc/metpx-sarracenia/+git/trunk + * do: **Import Now** + * run the recipe for old OS (18.04, 20.04) https://code.launchpad.net/~ssc-hpc-chp-spc/+recipe/metpx-sr3-pre-release-old + * do: **Request Build** (on Focal and Bionic ) + * run the recipe for new OS (22.04, 24.04) https://code.launchpad.net/~ssc-hpc-chp-spc/+recipe/metpx-sr3-pre-release + * do: **Request Build** (on Jammy and Noble at least) + +- build redhat packages. + + - find redhat 8 server. build package: python3 setup.py bdist_rpm + - find redhat 9 server, build package: python3 -m build --no-isolation + +- on github: Draft a release. + + - create release notes as prompted. + - copy the installation instructions from a previous release (for mostly ubuntu.) + - attach: + - wheel built on python3.6 on ubuntu 18 (the uploaded to pypi.org) + - windows binary. + - redhat 8 and 9 rpms labelled as such. + +- encourage testing of pre-release, wait some time for blockers, if any. + + +Stable Release Process +---------------------- + +A Stable version is just a pre-release version that has been +re-tagged as stable after some period of waiting for issues +to arise. Since all the testing was done for the pre-release, +the stable release does not require any explicit testing. + +* merge from pre-release to stable:: + + git checkout stable + git merge pre-release + # there will be conflicts here for debian/changelog and sarracenia/_version.py + # for changelog: + # - merge all the rcX changelogs into a single stable one. + # - ensure the version at the top is correct and tagged 'unstable' + # - edit the signature at the bottom for reflect who you are, and current date. + # for sarracenia/_version.py + # - fix it so it shows the correct stable version. + git tag -a v3.xx.yy -m "v3.xx.yy" + git push origin v3.xx.yy + +* merge from pre-release_py36 to stable_py36:: + + git checkout stable_py36 + git merge pre-release_py36 + # same editing required as above. + git tag -a o3.xx.yy -m "o3.xx.yy" + git push origin v3.xx.yy + +* go on Launchpad, + + * stable branch ready. + * stable_py36 branch ready. + * https://code.launchpad.net/~ssc-hpc-chp-spc/metpx-sarracenia/+git/trunk + * do: **Import Now** + * https://code.launchpad.net/~ssc-hpc-chp-spc/+recipe/metpx-sr3-old + * do: **Request Build** (on Focal and Bionic ) + * https://code.launchpad.net/~ssc-hpc-chp-spc/+recipe/metpx-sr3 + * do: **Request Build** (on Jammy and Noble at least) + +* go on ubuntu 18.04, build bdist_wheel:: + + git checkout stable_py36 + python3 setup.py bdist_wheel + +note that *pip3 install wheel* is needed, because the one from +ubuntu 18 is not compatible with the current pypi.org. + +* go on redhat 8, build rpm:: + + git checkout stable_py36 + python3 setup.py bdist_rpm + +* go on redhat 9, build rpm:: + + git checkout stable_py36 + rpmbuild --build-in-place -bb metpx-sr3.spec + + +* On github.com, create release. + + * copy/paste install procedure from a previous release, adjust + * attach wheel build on ubuntu 18. + * attach redhat 8 rpm + * attach redhat 9 rpm + * attach windows exe + + +Details +------- + + +Quality Assurance +~~~~~~~~~~~~~~~~~ + +The Quality Assurance (QA) process, occurs mainly on the development branch. +prior to accepting a release, and barring known exceptions, + +* QA tests automatically triggerred by pushes to the development branch should all pass. + (All related github actions.) + tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow are included in "flow.yml" + +* build an ubuntu 18.04 vm and run the flow tests there to ensure that it works. + (installation method: cloning from development on github.) + tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow + +* build a redhat 8 vm and run the flow test there to ensure that it works. + (installation method: cloning from development on github.) + tests: static, no_mirror, flakey_broker, restart_server, dynamic_flow + +* build a redhat 9 vm and run the flow test there to ensure that it works. + +* build a windows executable... test? + +For extensive discussion see: https://github.com/MetPX/sarracenia/issues/139 + +Once the above are done, the pre-release process can proceed. + + +Versioning Scheme +~~~~~~~~~~~~~~~~~ + +Each release will be versioned as ``.. `` + +It is difficult to reconcile debian and python versioning conventions. +We use rcX for pre-releases which work in both contexts. + +Where: + +- **Version** is the application version. Currently, 2 and 3 exist. +- **YY** is the last two digits of the year of the initial release in the series. +- **MM** is a TWO digit month number i.e. for April: 04. +- **segment** is what would be used within a series. + from pep0440: + X.YrcN # Release Candidate + X.Y # Final release + X.ypN #ack! patched release. + +Currently, 3.00 is still stabilizing, so the year/month convention is not being applied. +Releases are currently 3.00.iircj +where: + + * ii -- incremental number of pre-releases of 3.00 + + * j -- beta increment. + +The first alpha release of v2 from January 2016 would be versioned +as ``metpx-sarracenia-2.16.01a01``. A sample v3 is v3.00.52rc2. At some point 3.00 +will be complete & solid enough that the we will resume the year/month convention. + +Final versions have no suffix and are considered stable and supported. +Stable should receive bug-fixes if necessary from time to time. + +.. Note:: If you change default settings for exchanges / queues as + part of a new version, keep in mind that all components have to use + the same settings or the bind will fail, and they will not be able + to connect. If a new version declares different queue or exchange + settings, then the simplest means of upgrading (preserving data) is to + drain the queues prior to upgrading, for example by + setting, the access to the resource will not be granted by the server. + (??? perhaps there is a way to get access to a resource as is... no declare) + (??? should be investigated) + + Changing the default requires the removal and recreation of the resource. + This has a major impact on processes... + + +Set the Version +~~~~~~~~~~~~~~~ + +This is done to *start* development on a version. It should be done on development +after every release. + +* git checkout development +* Edit ``sarracenia/_version.py`` manually and set the version number. +* Edit CHANGES.rst to add a section for the version. +* run dch to start the changelog for the current version. + * change *unstable* to *UNRELEASED* (maybe done automatically by dch.) +* git commit -a +* git push + + +Git Branches for Pre-release +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Prior to releasing, ensure that all QA tests in the section above are passed. +When development for a version is complete. The following should occur: + +A tag should be created to identify the end of the cycle:: + + git checkout development + git tag -a v3.16.01rc1 -m "release 3.16.01rc1" + git push + git push origin v3.16.01rc1 + +Once the tag is in the development branch, promote it to stable:: + + git checkout pre-release + git merge development + git push + +Once stable is updated on github, the docker images will be automatically upgraded, but +we then need to update the various distribution methods: `PyPI`_, and `Launchpad`_ + +Once package generation is complete, one should `Set the Version`_ +in development to the next logical increment to ensure no further development +occurs that is identified as the released version. + + +Build Python3.6 Compatbile Branch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Canonical, the company behind Ubuntu, provides Launchpad as a means of enabling third parties to build +packages for their operating system releases. It turns out that the newer OS versions have dependencies +that are not available on the old ones. So the development branch is configured to build on newer +releases, but an a separate branch must be created when creating releases for ubuntu bionic (18.04) and +focal (20.04.) The same branch can be used to build on redhat 8 (another distro that uses python 3.6) + +Post python 3.7.?, the installatiion method changes from the obsolete setup.py to use pyproject.toml, +and the *hatch* python tools. Prior to that version, hatchling is not supported, so setup.py must be used. +However the presence of pyproject.toml fools the setup.py into thinking it can install it. To +get a correct installation one must: + +* remove pyproject.toml (because setup.py gets confused.) + +* remove "pybuild-plugin-prproject" dep from debuan + +in detail:: + + # on ubuntu 18.04 or redhat 8 (or some other release with python 3.6 ) + + git checkout pre-release + git branch -D pre-release_py36 + git branch stable_py36 + git checkout stable_py36 + vi debian/control + # remove pybuild-plugin-pyproject from the "Build-Depends" + git rm pyproject.toml + # remove the new-style installer to force use of setup.py + git commit -a -m "adjust for older os" + +There might be a "--force" required at some point. Perhaps something along the lines of:: + + git push origin stable_py36 --force + +Then proceed with Launchpad instructions. + + +PyPi +~~~~ + +Because python packages are upward compatible, but not downward, build them on ubuntu 18.04 +(oldest supported python & OS version.) in order for pip installs to work on the widest number +of systems. + +for local installation on a computer with a python 3.6 for testing and development:: + + python3 setup.py bdist_wheel + +or... on newer systems, using build instead:: + + python3 -m build --no-isolation + +Pypi does not distinguish between older and newer python releases. There is only one package +version for all supported versions. When uploading from a new OS, the versions in use on the +OS are inferred to be the minimum, and so installation on older operating systems may be blocked +by generated dependencies on overly modern versions. + +So when uploading to pypi, always do so from the oldest operating system where it needs to work. +upward compatibility is more likely than downward. + +Pypi Credentials go in ~/.pypirc. Sample Content:: + + [pypi] + username: SupercomputingGCCA + password: + +Assuming pypi upload credentials are in place, uploading a new release used to be a one liner:: + + python3 setup.py bdist_wheel upload + +on older systems, or on (python >= 3.7) newer ones:: + + twine upload dist/metpx_sarracenia-2.22.6-py3-none-any.whl dist/metpx_sarracenia-2.22.6.tar.gz + +Should always include source (the .tar.gz file) +Note that the CHANGES.rst file is in restructured text and is parsed by pypi.python.org +on upload. + +.. Note:: + + When uploading pre-release packages (alpha,beta, or RC) PYpi does not serve those to users by default. + For seamless upgrade, early testers need to do supply the ``--pre`` switch to pip:: + + pip3 install --upgrade --pre metpx-sarracenia + + On occasion you may wish to install a specific version:: + + pip3 install --upgrade metpx-sarracenia==2.16.03a9 + + command line use of setup.py is deprecated. Replaced by build and twine. + + +Launchpad +--------- + +Generalities about using Launchpad for MetPX-Sarracenia. + +Repositories & Recipes +~~~~~~~~~~~~~~~~~~~~~~ + +For Ubuntu operating systems, the launchpad.net site is the best way to provide packages that are fully integrated +( built against current patch levels of all dependencies (software components that Sarracenia relies +on to provide full functionality.)) Ideally, when running a server, a one should use one of the repositories, +and allow automated patching to upgrade them as needed. + +Before every build of any package, it is important to update the git repo mirror on launchpad. + +* https://code.launchpad.net/~ssc-hpc-chp-spc/metpx-sarracenia/+git/trunk +* do: **Import Now** + +Wait until this completes. + +Repositories: + +* Daily https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-daily (living on dev... ) + should, in principle, be always ok, but regressions happen, and not all testing is done prior to every + commit to dev branches. + Recipes: + + * metpx-sr3-daily -- automated daily build of sr3 packages happens from *development* branch. + * sarracenia-daily -- automated daily build of v2 packages happens from *v2_dev* branch + +* Pre-Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-pre-release (for newest features.) + from *development* branch. Developers manually trigger builds here when it seems appropriate (testing out + code that is ready for release.) + + * metpx-sr3-pre-release -- on demand build sr3 packages from pre-release branch. + * metpx-sr3-pre-release-old -- on demand build sr3 packages from *pre-release_py36* branch. + * metpx-sarracenia-pre-release -- on demand build sr3 packages from *v2_dev* branch. + +* Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx (for maximum stability) + from *v2_stable* branch. After testing in systems subscribed to pre-releases, Developers + merge from v2_dev branch into v2_stable one, and manually trigger a build. + + * metpx-sr3 -- on demand build sr3 packages from *stable* branch. + * metpx-sr3-old -- on demand build sr3 packages from *stable_py36* branch. + * sarracenia-release -- on deman build v2 packages from *v2_stable* branch. + +for more discussion see `Which Version is stable `_ + + + +Automated Build +~~~~~~~~~~~~~~~ + +* Ensure the code mirror is updated by checking the **Import details** by checking `this page for sarracenia `_ +* if the code is out of date, do **Import Now** , and wait a few minutes while it is updated. +* once the repository is upto date, proceed with the build request. +* Go to the `sarracenia release `_ recipe +* Go to the `sr3 release `_ recipe +* Click on the **Request build(s)** button to create a new release +* for Sarrac, follow the procedure `here `_ +* The built packages will be available in the `metpx ppa `_ + + +Daily Builds +~~~~~~~~~~~~ + +Daily builds are configured +using `this recipe for python `_ +and `this recipe for C `_ and +are run once per day when changes to the repository occur. These packages are stored in the `metpx-daily ppa `_. +One can also **Request build(s)** on demand if desired. + + +Manual Process +++++++++++++++ + +The process for manually publishing packages to Launchpad ( https://launchpad.net/~ssc-hpc-chp-spc ) +involves a more complex set of steps, and so the convenient script ``publish-to-launchpad.sh`` will +be the easiest way to do that. Currently the only supported releases are **trusty** and **xenial**. +So the command used is:: + + publish-to-launchpad.sh sarra-v2.15.12a1 trusty xenial + + +However, the steps below are a summary of what the script does: + +- for each distribution (precise, trusty, etc) update ``debian/changelog`` to reflect the distribution +- build the source package using:: + + debuild -S -uc -us + +- sign the ``.changes`` and ``.dsc`` files:: + + debsign -k <.changes file> + +- upload to launchpad:: + + dput ppa:ssc-hpc-chp-spc/metpx- <.changes file> + +**Note:** The GPG keys associated with the launchpad account must be configured +in order to do the last two steps. + + + +Backporting a Dependency +++++++++++++++++++++++++ + +Example:: + + backportpackage -k -s bionic -d xenial -u ppa:ssc-hpc-chp-spc/ubuntu/metpx-daily librabbitmq + + +Building an RPM ++++++++++++++++ + +This process is currently a bit clumsy, but it can provide usable RPM packages. +Example of creating a multipass image for fedora to build with:: + + fractal% multipass launch -m 8g --name fed34 https://mirror.csclub.uwaterloo.ca/fedora/linux/releases/34/Cloud/x86_64/images/Fedora-Cloud-Base-34-1.2.x86_64.raw.xz + Launched: fed34 + fractal% + +Based on https://www.redhat.com/sysadmin/create-rpm-package ... install build-dependencies:: + + sudo dnf install -y rpmdevtools rpmlint git + git clone -b development https://github.com/MetPX/sarracenia sr3 + cd sr3 + +One can build a very limited sort of rpm package on an rpm based distro by +The names of the package for file magic data (to determin file types) has different names on +ubuntu vs. redhat. The last three lines of **dependencies** in pyproject.toml are about +"python-magic", but on redhat/fedora >= 9, it needs to be "file-magic" instead:: + + # remove last three lines of dependencies in setup.py + + * on redhat <=8: vi setup.py ; python3 setup.py bdist_rpm + + # might work, but might need some removals also. + * on redhat >=9: vi pyproject.toml; python3 -m build + +"python-magic", but on redhat, it needs to be "file-magic" instead:: + + vi pyproject.toml + +using the normal (for Redhat) rpmbuild tool:: + + rpmbuild --build-in-place -bb metpx-sr3.spec + +When doing this on the redhat 8, edit the metpx-sr3.spec and potentially pyproject.toml +to remove the other dependencies because there are no OS packages for: paramiko, +watchdog, xattr, & magic. Eventually, one will have removed enough that the rpm file +will be built. + +One can check if the dependencies are there like so:: + + [ubuntu@fed39 sr3]$ rpm -qR /home/ubuntu/rpmbuild/RPMS/noarch/metpx-sr3-3.00.47-0.fc39.noarch.rpm + + /usr/bin/python3 + python(abi) = 3.12 + python3-appdirs + python3-humanfriendly + python3-humanize + python3-jsonpickle + python3-paramiko + python3-psutil + python3-xattr + python3.12dist(appdirs) + python3.12dist(humanfriendly) + python3.12dist(humanize) + python3.12dist(jsonpickle) + python3.12dist(paramiko) + python3.12dist(psutil) >= 5.3 + python3.12dist(watchdog) + python3.12dist(xattr) + rpmlib(CompressedFileNames) <= 3.0.4-1 + rpmlib(FileDigests) <= 4.6.0-1 + rpmlib(PartialHardlinkSets) <= 4.0.4-1 + rpmlib(PayloadFilesHavePrefix) <= 4.0-1 + rpmlib(PayloadIsZstd) <= 5.4.18-1 + + [ubuntu@fed39 sr3]$ + +You can see all of the prefixed python3 dependencies required, as well as the recommended binary accellerator packages +are listed. Then if you install with dnf install, it will pull them all in. Unfortunately, this method does not allow +the specification of version of the python dependencies which are stripped out. on Fedora 34, this is not a problem, +as all versions are new enough. Such a package should install well. + +After installation, one can supplement, installing missing dependencies using pip (or pip3.) +Can check how much sr3 is working using *sr3 features* and use pip to add more features +after the RPM is installed. + + +Building a Windows Installer +---------------------------- + +One can also build a Windows installer with that +`script `_. +It needs to be run from a Linux OS (preferably Ubuntu 18) in the root directory of Sarracenia's git. +find the python version in use:: + + fractal% python -V + Python 3.10.12 + fractal% + +So this is python 3.10. Only a single minor version will have the embedded package needed +by pynsist to build the executable, so look at:: + + https://www.python.org/downloads/windows/ + +Then go look on python.org, for the "right" version (for 3.10, it is 3.10.11 ) +Then, from the shell, run:: + + sudo apt install nsis + pip3 install pynsist wheel + ./generate-win-installer.sh 3.10.11 2>&1 > log.txt + +The final package will be generated into build/nsis directory. + + +github +------ + +* Click on Releases +* Click on tags, pick the tag for the new release vXX.yy.zzrcw +* Click on Pre-Release tag at the bottom if appropriate. +* Click on Generate Release notes... Review. +* copy/paste of Installation bit at the end from a previous release. +* Save as Draft. +* build packages locally or download from other sources. + drag and drop into the release. +* Publish. + +This will give us the ability to have old versions available. +launchpad.net doesn't seem to keep old versions around. + + +Troubleshooting +--------------- + + + +ubuntu 18 +--------- + +trying to upload from ubuntu 18 vm:: + + buntu@canny-tick:~/sr3$ twine upload dist/metpx_sr3-3.0.53rc2-py3-none-any.whl + /usr/lib/python3/dist-packages/requests/__init__.py:80: RequestsDependencyWarning: urllib3 (1.26.18) or chardet (3.0.4) doesn't match a supported version! + RequestsDependencyWarning) + Uploading distributions to https://upload.pypi.org/legacy/ + Uploading metpx_sr3-3.0.53rc2-py3-none-any.whl + 100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 408k/408k [00:00<00:00, 120kB/s] + HTTPError: 400 Client Error: '2.0' is not a valid metadata version. See https://packaging.python.org/specifications/core-metadata for more information. for url: https://upload.pypi.org/legacy/ + ubuntu@canny-tick:~/sr3$ + +I uploaded from redhat8 instead. used pip3 to install twine on redhat, and that was ok. This could be a result +of running the system provided python3-twine on ubuntu. + diff --git a/_sources/Contribution/deltas.rst.txt b/_sources/Contribution/deltas.rst.txt new file mode 100644 index 000000000..a5fc7aedf --- /dev/null +++ b/_sources/Contribution/deltas.rst.txt @@ -0,0 +1,222 @@ + +Status: Pre-Draft + + +============================================== + Discussion of File Modification Propagation +============================================== + +This was early thinking about how to deal with file updates. +The early versions of the protocol only concerned itself with entire files. +when the file sets are large enough, partial updates become very desirable. +Also, when the size of individual files is large enough, and when traversing +WAN links, one can obtain substantial practical advantage by sending sending +data using multiple streams, rather than just one. + +So in v02 there is a header 'parts' which indicates the partitioning method +used for a given transfer, and the conclusion about the format/protocol +is now documented in the sr_post(7) man page. This file contains early +discussions & notes. + +Algorithm used (regardless of tool): + - for each ´block´ (blocksize interesting) generate a signature. + - when a subscriber reads an notification message, it includes the signature. + - he compares the signatures on the file he already has, and updates it to match. + +the zsync algorithm is the right idea, can perhaps use it directly. + + +What If Each Notification is for a Block, not a File ? +------------------------------------------------------ + +gedanken experiment... per block messages, rather than entire files ? +what if the messages we send are all per block? + +Why is this really cool? + + - It does the gridftp thing, splitting out single file transfers + into parallel streams. + + - For large files, the ddsr's might have a whole bunch of part files, + instead of the complete ones, because the transfer is split over + multiple nodes, no problem, as long as later stages are subscribed + to all ddsr's. + + - intervening switches do not need to store the largest file + that can be transferred, only some number of the largest chunks. + eliminates the maximum file size problem. + + - This also deals with files that are written over time, without waiting + until they are complete before hitting send. + + - for the client to do multi-threaded send, they just start up + any number of sr_senders listening to their own input exchange. + sharing the subscription, just like sr_subscribe (dd_subscribe) does. + + - for large files, you can see progress reports sources receive + confirmation of each switching layer receiving each chunk. + +say we set a blocksize of 10MB, and we checksum that block, noting the offset, then +continue? + +so v01.post: +blocksz sz-inblocks remainder blocknum flags chksum base-url relative-path flow .... + +see logging.txt for a description of 'flow', user settable, with a default. + +flags says whether the chksum is for the name or the body. (if checksum is for name, +then cannot use blocking chksums.) ... says name checksums should only be used for smallish files. + +The blocksz establishes the multiplier for the sz and the blocknum. the remainder +is the last bit of the file after the last block boundary. + +So you calculating the checksum for each block you send off a message with the block, + +this way, for large files, the transfer can be split over a large number of nodes. +but then re-assembly is a bit of a puzzle. will each node of ddsr have only +fractional (aka sparse) large files? as long as the sr_sub is to both ddsr's, it should +get everything. what happens with sparse files? + +https://administratosphere.wordpress.com/2008/05/23/sparse-files-what-why-and-how/ + +that´s it's OK... +it would work, on linux, but it's a bit strange, and would like cause confusion in +practice. Besides, how do we know when we are done? + +--- Re-assembly --- +How about the following. when sr_subscribe(dd_subscribe) writes files, it writes to a file +suffixed .sr§1 when it receives a file, and there is a .sr§ it checks the size +of the file, if the current part is contiguous, it just appends (via lseek & write) +the data to the .sr§ file. If not, it creates a separate .sr§ + +.sr§ suffix +----------- + +but that means they advertise the parts... hmm... the names now mean something, +We use the Section Character instead of part. to avoid that, pick a name that +is more unusual that .part something like .sr§partnum (using utf-8 interesting +to see what url-encoding will do to that.) It is good to use UTF special +characters, because no-one else uses them, so unlikely to clash. + +what is someone advertises a .sr§ file? what does it mean? do we need to +detect it? + +Then it looks in the directory to see if a .sr§ exists, and appends +it if it does, and loops until all contiguous parts are appended (the corresponding +files deleted.) + +NOTE: Do not use append writing .sr§, but always lseek and write. This prevents +race conditions from causing havoc. If there are multiple sr_subscribe (dd_subscribes) writing +the file they will just both write the same data multiple times (worst case.) + +anyways when you run out of contiguous parts, you stop. + +if the last contiguous block received includes the end of the file, then +do the file completion logic. + +How to Select Chunksize +----------------------- + + - source choice? + - we impose our own on ddsr? + +a default to 10*1024*1024=10485760 bytes, with override possible. + +might want the validation part to impose a minimum chunksize +on posted files, in addition to the file path. + +we set a minimum, say 10MB, then if that is less than 5% of the file, +then use 5%, until we get to a maximum chunksize... say 500MB. +override with size 0 (no chunking one long send.) + +what is the overhead to send a block? + - there are no fixed width fields in the messages, are all variable length ASCII. + - estimate that an avereage notification is 100 bytes, + - log message is perhaps 120 bytes. + +for each block transferred. + notification goes in one direction, + at least one log message per scope hope goes through in the other direction. + + if we say two hops + client delivery (a third hop) + + so a block will have on the order of 100+4*120 = 580 bytes. + +It accepted that there is substantial protocol overhead on small files. + +However, one would hope the overhead would get more reasonable for bigger files, +but that is limited by the blocksize. +Aesthetically, need to choose a size that dwarfs the overhead. + + + +if we do blockwise cksums, path from v00 +---------------------------------------- + +compatibility... upgrading... +v00.notify alerts boil down to: + +v01.post could be: + + filesz 1 0 0 ... + - the blocksize is the length of the entire file, 1 block ithe sz + - no remainder. + - 0th block (the first one, zero origin counting) + +or we take the convention that a blocksize of zero means no blocking... +in which chase it would be: + + 0 1 filesz 0 ... + - store the sz as the remainder. + - disable blocking for that file. + +if there is validation on the blocking size, needs to be a way to deal with it. + + +Digression about ZSync +---------------------- + +zsync is available in repositories and zsync(1) is the existing download client. +zsyncmake(1) builds the signatures, with a programmable block size. + +It looks ike zsync is usable as is? + +downside: portability. + need zsync on Windows and mac for downloads, dependency a pain. + there is a Windows binary, made once in 2011... hmm... + have not seein it on Mac OS either... sigh... + +we send the signatures in the notification messages, rather than posting on the site. +If we set the blocksize high, then for files < 1 block, there is no signature. + +should sr_sarra post the signature to the site, for zsync compatibility? + +Do not want to be forking zsyncmake for every product... +even if we do not use zsync itself, might want to be compatible... so use +a third party format and have a comparable. 1st implementation would do +forking, 2nd version might replicate the algorithm internally. + +perhaps we have a threshold, if the file is less than a megabyte, we just send +the new one. The intent is not to replicate source trees, but large data sets. + + - for most cases (when writing a new file) we do not want extra overhead. + - target is large files that change, for small ones, transfer again, is not a big deal. + - want to minimize signature size (as will travel with notifications.) + - so set a block size to really large. + +Perhaps build the zsync client into sr_subscribe (dd_subscribe), but use zsync make on the server side ? +or when the file is big enough, forking a zsync is no big deal? but mac & win... + + +Server/Protocol Considerations +------------------------------ + +HTTP: + -- uses byte range feature of HTTP. + -- FIXME: find samples from other email. + + +in SFTP/python/paramiko... + -- there is readv( ... ) which allows to read subsets of a file. + -- the read command in SFTP PROTOCOL spec has offset as a standard argument of read + diff --git a/_sources/Contribution/index.rst.txt b/_sources/Contribution/index.rst.txt new file mode 100644 index 000000000..b9250ccf9 --- /dev/null +++ b/_sources/Contribution/index.rst.txt @@ -0,0 +1,22 @@ + +Contributing +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + AMQPprimer + Development + Documentation + Release + man_page_template.rst + mqtt_issues + v03 + on_part_assembly + Evolution + Original (2015) Basic Idea + Original (2015) Design + deltas + Philosophy + diff --git a/_sources/Contribution/man_page_template.rst.txt b/_sources/Contribution/man_page_template.rst.txt new file mode 100644 index 000000000..a735dab61 --- /dev/null +++ b/_sources/Contribution/man_page_template.rst.txt @@ -0,0 +1,109 @@ +========== + SR3_TITLE +========== + +-------- +sr_title +-------- + +:Manual section: 1 +:Date: |today| +:Version: |release| +:Manual group: MetPX-Sarracenia + +SYNOPSIS +======== + +**sr_title** foreground|start|stop|restart|reload|status configfile +**sr_title** cleanup|declare|setup configfile + +DESCRIPTION +=========== + +**sr_title** Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do +eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit +esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat +cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id +est laborum. `sr_subscribe(7) `_ +with some limitations. + + - doesn't download data, only circulates notification messages. + - runs as only a single instance (no multiple instances). + - does not support any plugins. + - does not support vip for high availability. + - different regular expression library: POSIX vs. python. + - does not support regex for the strip command (no non-greedy regex). + +It can therefore act as a drop-in replacement for: + + `sr_report(1) `_ - process report notification messages. + + `sr_shovel(8) `_ - process shovel notification messages. + + `sr_winnow(8) `_ - process winnow notification messages. + + +The **sr_cpump** command takes two arguments: an action start|stop|restart|reload|status... (self described) +followed by a configuration file. + +The **foreground** action is used when debugging a configuration, when the user wants to +run the program and its configfile interactively... The **foreground** instance +is not concerned by other actions. The user would stop using the **foreground** instance +by simply on linux or use other means to send SIGINT or SIGTERM to the process. + +The actions **cleanup**, **declare**, **setup** can be used to manage resources on +the rabbitmq server. The resources are either queues or exchanges. **declare** creates +the resources. **setup** creates and additionally does the bindings of queues. + +The actions **add**, **remove**, **edit**, **list**, **enable**, **disable** act +on configurations. + +CONFIGURATION +============= + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore +et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid +ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur?" + + + + +ENVIRONMENT VARIABLES +===================== + +If the SR_CONFIG_EXAMPLES variable is set, then the *add* directive can be used +to copy examples into the user's directory for use and/or customization. + +An entry in the ~/.config/sarra/default.conf (created via sr_subscribe edit default.conf ) +could be used to set the variable:: + + declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples + +The value should be available from the output of a list command from the python +implementation. + +SEE ALSO +======== + +`sr3_report(7) `_ - the format of report messages. + +`sr3_report(1) `_ - process report messages. + +`sr3_post(1) `_ - post notification messages of specific files. + +`sr3_post(7) `_ - the format of notification messages. + +`sr3_subscribe(1) `_ - the download client. + +`sr3_watch(1) `_ - the directory watching daemon. diff --git a/_sources/Contribution/mqtt_issues.rst.txt b/_sources/Contribution/mqtt_issues.rst.txt new file mode 100644 index 000000000..27080c898 --- /dev/null +++ b/_sources/Contribution/mqtt_issues.rst.txt @@ -0,0 +1,42 @@ + + +========================= +MQTT Implementation Notes +========================= + + + +v3 vs. v5 +--------- + +* version 3 has resends sent on a timed basis, every few seconds (perhaps as much as 20 seconds.) + If you ever have a backlog, these retransmits will be a storm of ever increasing traffic. + +* version 3 does not have shared subscriptions, can only use one process per subscription. + load balancing more difficult. + + +Shared Subscriptions +-------------------- + +* once you join a group, you are there until the session is dead, even if you disconnect, + it will pile 1/n messages in your queue. + + +Back Pressure +------------- + +1. paho client is async,  +2. best practice is to have very light-weight on_message handlers. +3. paho client acknowledges in the library around the time on_message is called. + +If you have an application that is falling behind... say it is slow at processing +but since the reception is async, all this means is you will get an ever bigger +queue of messages on the local host. Ideally, it would let the broker know that +things are going poorly and the broker would send less data there. + +Methods: + +1. v5: Receive Maximum ... limit the number of messages in flight between broker and client. + implemented. works. + diff --git a/_sources/Contribution/on_part_assembly.rst.txt b/_sources/Contribution/on_part_assembly.rst.txt new file mode 100644 index 000000000..9d083b483 --- /dev/null +++ b/_sources/Contribution/on_part_assembly.rst.txt @@ -0,0 +1,57 @@ +================== +File Re-assembling +================== + + +Components +---------- + +**sr_watch:** You can use sr3_watch to watch a directory for incoming partition files (.Part) from sr_subscribe or sr_sender, both have the ability to send a file in partitions. In the config file for sr3_watch the important parameters to include are: + + - path + - on_part /usr/lib/python3/dist-packages/sarra/plugins/part_file_assemble.py + - accept \*.Part + - accept_unmatch False # Makes it only acccept the pattern above + +**Part_File_Assemble (plugin):** This plugin is an on_part plugin which triggers the assembly code in **sr_file** + +**sr_file:** Contains the reassembly code... The algorithm is described below + + +Algorithm +--------- + +After being triggered by a downloaded part file: + + - if the target_file doesn't exist: + + - if the downloaded part file was the first partition (Part 0): + + - create a new empty target_file + + - find which partition number needs to be inserted next (i) + + - while i < total blocks: + + - file_insert_part() + + - inserts the part file into target file and computes checksum of the inserted portion + + - verify insertion by comparing checksums of partition file and inserted block in the file + - delete file if okay, otherwise retry + - trigger on_file + + +Testing +------- + +Create an sr3_watch config file according to the template above +Start the process by typing the following command: ```sr_watch foreground path/to/config_file.cfg``` + +Then create a subcriber config file and include ```inplace off``` so the file will be downloaded in parts +Start the subscriber by typring ```sr_subscribe foreground path/to/config_file.cfg``` + +Now, you must send post messages of the file for the subscriber +for example: ```./sr_post.py -pb amqp://tsource@localhost/ -pbu sftp://@localhost/ -p /home//test_file -px xs_tsource --blocksize 12M``` + + diff --git a/_sources/Contribution/v03.rst.txt b/_sources/Contribution/v03.rst.txt new file mode 100644 index 000000000..ff4ab0e62 --- /dev/null +++ b/_sources/Contribution/v03.rst.txt @@ -0,0 +1,908 @@ + +================== +Version 3 Refactor +================== + +Summary +------- + +This document is aimed at developers who need to work with both v2 code and +the refactor that was originally called v3, but eventually was called sr3. +For developers familiar with v2, the document can serve as a bit of a map +to the source code of sr3, which has now stablizied enough that it substantially +passes the flow_tests. Sr3 is perhaps not really usable yet, but the direction +is well established and further development is now described using the issue +tracker (look at the v3only label.) So this document is now essentially +historical. If someone does not know v2, this document will not help as it is +exclusively about mapping v2 to sr3. Reading the balance of v03 documentation +should be more helpful. + +Abstract Goals of sr3: + +* configuration compatibility (upward compatible from v2.) including all plugins. + +* multi-protocol support. + ability to put in urls for mqtt, or different amqp libraries, perhaps others. + +* internally represent things in v03 notification messages, have something build + v02 ones for compatibility, but operate in v03. + +* less code, simpler code. + more readable, elegant, pythonic code. + make maintenance easier. + + +goals of opportunity +~~~~~~~~~~~~~~~~~~~~ + + * add stuff to make it work as an API? + * potentially new plugin api to allow groups (of notification messages and/or files.) + * Finish off log rotation. + * Assume python >= 3.4 remove old cruft. + * Assume ubuntu >= 18.04 remove old cruft. + * Assume systemd, remove sysv integration. + * have options adopt camelCase where possible. + * fully async, multi-sources and sinks. + +State of the Code +~~~~~~~~~~~~~~~~~ + +As of 2021/08/24, the sr3 code passes all the same flow tests that v2 does +on one laptop (except dynamic in sr3 #407). It runs those same tests using the same configurations, so compatibiliy +goal is achieved. Sr3 accept mqtt broker urls, and an issue is created #389 for amqp v1. +Sr3 is being used to feed a WMO experimental feed, albeit with the need +to restart it regularly ( issue #388 ) + +The new sr3 code has 4000 fewer lines than v2, and includes mqtt.py (extra broker protocol) +as well as a module implementing a compatibility layer with v2 plugins. For example, the +new configuration routine is 30% shorter and more consistent in sr3 than in v2. +The code is also much more pythonic, as the API is much more +natural to work with multiple API levels that can be learned by consulting jupyter notebooks. + + +v2 code:: + + fractal% find -name '*.py' | grep -v .pybuild | grep -v debian | grep -v plugins | xargs wc -l + 133 ./sr_winnow.py + 544 ./sr_sftp.py + 47 ./sr_tailf.py + 365 ./sr_cache.py + 164 ./sr_xattr.py + 1136 ./sr_message.py + 51 ./sr_checksum.py + 129 ./pyads.py + 306 ./sr_http.py + 2204 ./sr_subscribe.py + 403 ./sr_consumer.py + 1636 ./sr_post.py + 265 ./sr1.py + 54 ./sr_log2save.py + 206 ./sr_sarra.py + 286 ./sr_rabbit.py + 567 ./sr_file.py + 28 ./__init__.py + 107 ./sr_report.py + 74 ./sr_watch.py + 126 ./sr_shovel.py + 505 ./sr_retry.py + 956 ./sr_util.py + 355 ./sr_sender.py + 368 ./sr_cfg2.py + 1119 ./sr.py + 753 ./sr_poll.py + 729 ./sr_audit.py + 308 ./sr_credentials.py + 988 ./sr_instances.py + 608 ./sr_amqp.py + 455 ./sr_ftp.py + 3062 ./sr_config.py + 33 ./sum/checksum_s.py + 34 ./sum/checksum_d.py + 34 ./sum/__init__.py + 26 ./sum/checksum_0.py + 30 ./sum/checksum_n.py + 29 ./sum/checksum_a.py + 19223 total + fractal% + +sr3 code:: + + 2157 ./config.py + 342 ./credentials.py + 384 ./diskqueue.py + 183 ./filemetadata.py + 768 ./flowcb/gather/file.py + 53 ./flowcb/gather/message.py + 7 ./flowcb/housekeeping/__init__.py + 130 ./flowcb/housekeeping/resources.py + 250 ./flowcb/__init__.py + 145 ./flowcb/log.py + 24 ./flowcb/nodupe/data.py + 345 ./flowcb/nodupe/__init__.py + 24 ./flowcb/nodupe/name.py + 454 ./flowcb/poll/__init__.py + 14 ./flowcb/post/__init__.py + 55 ./flowcb/post/message.py + 117 ./flowcb/retry.py + 461 ./flowcb/v2wrapper.py + 1617 ./flow/__init__.py + 80 ./flow/poll.py + 34 ./flow/post.py + 18 ./flow/report.py + 29 ./flow/sarra.py + 27 ./flow/sender.py + 16 ./flow/shovel.py + 29 ./flow/subscribe.py + 35 ./flow/watch.py + 16 ./flow/winnow.py + 793 ./__init__.py + 226 ./instance.py + 36 ./identity/arbitrary.py + 93 ./identity/__init__.py + 33 ./identity/md5name.py + 24 ./identity/md5.py + 17 ./identity/random.py + 24 ./identity/sha512.py + 17 ./moth/amq1.py + 585 ./moth/amqp.py + 313 ./moth/__init__.py + 548 ./moth/mqtt.py + 16 ./moth/pika.py + 135 ./pyads.py + 349 ./rabbitmq_admin.py + 26 ./sr_flow.py + 52 ./sr_post.py + 2066 ./sr.py + 50 ./sr_tailf.py + 383 ./transfer/file.py + 514 ./transfer/ftp.py + 361 ./transfer/https.py + 437 ./transfer/__init__.py + 607 ./transfer/sftp.py + 15519 total + + +V02 Plugin Pain Points +---------------------- + +Writing plugins should be a straight-forward activity for people with a rudimentary +knowledge of Python, and some understanding of the task at hand. In version 2, +writing plugins is a lot harder than it should be. + +* syntax error, v2 gives basically a binary response, either reading in the plugin worked + or it didn't... it is very unfriendly compared to normal python. + +* when a setting is put in a config file, it's value is [ value ], and not value (It's in a list.) + +* weird scoping issue of import (import in main does not carry over to on_message, need to import + in the main body of the routine as well as in the python file.) + +* What the heck is self, what the heck is parent? These arguments to plugins are not obvious. + self usually refers to the caller, not the self in a normal class, and parent is the flow, + so no state can be stored in self, and all must be stored in parent. Parent is kind of + a catch all for settings and dynamic values in one pile. + +* bizarre use of python logger API... self.logger? wha? + +* inability to call from python code (no API.) + +* inability to *add* notification messages within a plugin (can only process the message you have.) + +* inability to process groups of notification messages at a time (say for concurrent sends or + downloads, rather than just one at time. + +* poor handling of message acknowledgements. v02 just ackowledges the previous message + when a new one is received. + +* lack of clarity about options, versus working variables, because they are in the same namespace + in a plugin, if you find self.setting==True ... is that because the application set it somewhere, + or because an option was set by a client... is it a setting or a variable? + +* making changes to notification messages is a bit complicated, because they evolved over different message formats. + + +Changes Done to Address Pain Points +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* use importlib from python, much more standard way to register plugins. + now syntax errors will be picked up just like any other python module being imported, + with a reasonable error message. + +* no strange decoration at end of plugins (self.plugin = , etc... just plain python.) + +* The strange choice of *parent* as a place for storing settings is puzzling to people. + *parent* instance variable becomes *options*, *self.parent* becomes *self.o* + +* plural event callbacks replace singular ones: + + * after_accept(self,worklist) replaces on_message(self,parent) + * after_work(self,worklist) replaces on_part/on_file(self,parent) + + +* notification messages are just python dictionaries. fields defined by json.loads( v03 payload format ) + notification messages only contain the actual fields, no settings or other things... + plain data. + +* callbacks move notification messages between worklists. A worklist is just a list of notification messages. There are four: + + * worklist.incoming -- notification messages yet to be processed. + * worklist.rejected -- notification message which are not to be further processed. + * worklist.ok -- notification messages which have been successfully processed. + * worklist.retry -- notification messages for which processing was attempted, but it failed. + + could add others... significant number of applications for something like *deferred* + +* acknowledgements done more pro-actively, as soon as a message is processed + (for rejected or failed notification messages, this is much sooner than in v2.) + +* add scoping mechanism to define plugin properties. + +* properties fed to __init__ of the plugin, parent is gone from the plugins, they should + just refer to self.o for the options/settings they need. (self.o clearly separates options + from working data.) + +* command-line parsing using python standard argParse library. Means that keywords no longer work + with a single -. Settling on standard use of -- for word based options, and - for abbrevs. + examples: Good: --config, and -c, BAD: -config --c . + + + +Ship of Theseus +--------------- + +It might be that the re-factoring inherent in v03 results in a +Ship of Theseus, where it works the same way as v02, but all +the parts are different... obviously a concern/risk... +might be a feature. + +Now that we are a good way throught the process, a +mapping of source code transcriptions between +the two versions, is clear: + ++--------------------------+---------------------------+ +| Version 2 file | Version 3 file | ++--------------------------+---------------------------+ +| sr_config.py | config.py | ++--------------------------+---------------------------+ +| sr_instances.py | sr.py for most mgmt. | +| | instance.py single proc | +| | | ++--------------------------+---------------------------+ +| sr_consumer.py | moth/__init__.py | +| | | +| sr_amqp.py | moth/amqp.py | +| | | +| sr_message.py | | ++--------------------------+---------------------------+ +| sr_checksum.py | identity/ | +| | __init__.py | +| sum/* | * | ++--------------------------+---------------------------+ +| sr_cache.py | flowcb/nodupe.py | ++--------------------------+---------------------------+ +| sr_retry.py | flowcb/retry.py | +| | | +| | diskqueue.py | ++--------------------------+---------------------------+ +| sr_post.py | flowcb/gather/file.py | +| | flow/post.py | +| | | ++--------------------------+---------------------------+ +| sr_poll.py | flowcb/poll/ | +| | __init__.py | +| | | +| | flow/poll.py | +| | | ++--------------------------+---------------------------+ +| | | +| | transfer/__init__.py | +| sr_util.py/sr_proto | * transfer.Protocol | +| | | +| sr_util.py/sr_transport | flow/__init__.py | +| | | +| sr_file.py | transfer/file.py | +| | | +| sr_ftp.py | transfer/ftp.py | +| | | +| sr_http.py | transfer/http.py | +| | | +| sr_sftp.py | transfer/sftp.py | +| | | ++--------------------------+---------------------------+ +| plugins/ | flowcb/ (sr3 ones) | +| | plugins/ still there | +| | for v2 ones. | ++--------------------------+---------------------------+ +| overall flow | flow/__init__.py | ++--------------------------+---------------------------+ +| | | +| sr_poll.py | sr_flow.py | +| | as entry point... | +| sr_post.py | | +| | but generally just use | +| sr_subscribe.py | sr.py as single one. | +| | | +| sr_shovel.py | | +| | | +| sr_report.py | | +| | | +| sr_sarra.py | | +| | | +| sr_sender.py | | +| | | +| sr_watch.py | | +| | | +| sr_winnow.py | | +| | | ++--------------------------+---------------------------+ + + +Mappings +~~~~~~~~ + +v2->sr3 instance variables:: + + self.user_cache_dir --> self.o.cfg_run_dir + +Changes needed in v2 plugins:: + + from sarra.sr_util import --> from sarracenia import + + + +Dictionaries or Members for Properties? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There seems to be a tension between using class members and dictionaries +for settings. Members seem more convenient, but harder to manipulate, +though we have equivalent idioms. Argparse returns options as their own +members of this parsing object. There is a hierarchy to reconcile: + +* protocol defaults +* consumer defaults +* component defaults +* configuration settings (overrides) +* command line options (overrides) + +resolving them to apply overrides, mais more sense as operations +on dictionaries, printing, saving loading, again makes more sense +as dictionaries. In code, members are slightly shorter, and perhaps +more idiomatic:: + + hasattr(cfg,'member') vs. 'member' in cfg (dictionary) + +What makes more sense... Does it make any practical difference? +not sure... need to keep the members for places where +callbacks are called, but can use properties elsewhere, if desired. + + +Known Problems (Solved in sr3) +------------------------------ + +* passing of logs around is really odd. We didn't understand what + python logging objects were. Need to use them in the normal way. + new modules are built that way... + + In new modules, use the logging.getLogger( __name__ ) convention, but + often the name does not match the actual source file... why? + e.g. a log message from config.py parsing shows up like:: + + 2020-08-13 ... [INFO] sarra.sr_credentials parse_file ... msg text... + + why is it labelled sr_credentials? no idea. + + +* this weird try/except thing for importing modules... tried removing + it but it broke parsing of checksums... sigh... have to spend time + on specifically that problem. On new modules ( sarra.config, + sarra.tmpc.*, sr.py ) using normal imports. likely need to + refactor how checksum plugin mechanism works then try again. + + totally refactored now. Identity class is normal, and separate from flowcb. + + +Concrete Plan (Done) +-------------------- + +Replace sarra/sr_config with sarra/sr_cfg2. The new sr_cfg2 uses argparse +and a simpler model for config file parsing. This became config.py + +make sr.py accept operations on subsets, so it becomes the unique entry point. +internalize implementation of all management stuff, declare etc... + +HMPC - Topic Message Protocol Client... a generalization of the message +passing library with a simplified API. abstracts the protocol differences +away. (This later became the Moth module.) + +The method of testing is to make modifications and check them against the +sr_insects development branch. In general, an un-modified sr_insects tests should +work, but since the logs change, there is logic being added on that branch +to parse v2 and sr3 versions in the same way. Thus the development branch tests +are compatible with both stable and work-in-progress versions. + +To get each component working, practice with individual unit tests, and then +get to static-flow tests. Can also do flakey_broker. The work is only going +that far as all the components are converted. Once full conversion is achieved, +then will look at dynamic_flow. + +Purpose is not a finished product, but a product with sufficient and +appropriate structure so that tasks can be delegated with reasonable hope of success. + + +Done +---- + +The functionality of sr_amqp.py is completely reproduced in moth/amqp.py +All the important logic is preserved, but it is transcribed into new classes. +Should have identical failure recovery behaviour. But it doesn't. We have +static flow test passing, but the flakey broker, which tests such recovery, +is currently broken. (2022/03 all good now!) + +sr_cfg2.py was still a stub, it has a lot of features and options, but +it isn't clear how to expand it to all of them. the thing about instances +inheriting from configure... it is odd, but hard to see how changing that +will not break everything, plugin-wise... thinking about having defaults +distributed to the classes that use the settings, and having something +that brings them together, instead of one massive config thing. +renamed to config.py (aka: sarra.config) and exercising it with +sr.py. It is now a complete replacement. + +Replaced the sr_consumer class with a new class that implements the +General Algorithm describe in `Concepts ` +This happenned and became the Flow Module, and *the General Algorithm* got +renamed *the Flow Algorithm*. yes, that is now flow/ class hierarchy. +The main logic is in __init__, and actual components are sub-classes. + +Thinking about just removing sr\_ the prefix from classes for replacements, +since they are in sarra directory anyways. so have an internal class +sarra/instances, sarra/sarra <- replace consumer... This happenned +and became a place holder for progress, meaning that files with sr\_ +prefix in the name, that are not entry-points, indicate v2 code that +has not yet been retired/replaced. + +Added configuration selection to sr.py (e.g. subscribe/\*) and +*setup*, and *cleanup* options. + +add/remove/enable/disable/edit (in sr.py) done. + +'log' dropped for now... (which log ?) + +added list, show, and built prototype shovel... required +a instance (sets state files and logs) and then calls flow... +flow/run() is visibly the general algorithm, +shovel is a sub-class of flow. + +Got a skeleton for v2 plugins working (v2wrapper.py) implemented +import-based and group oriented sr3 plugin framework. ( #213 ) + +cache (now called noDupe) working. + +re-wrote how the sr3 callbacks work to use worklists, and then re-cast +cache and retry v2plugins as sr3 callbacks themselves. + +renamed message queue abstract class from tmpc to moth +(what does a Sarracenia eat?) + +With shovel and winnow replaced by new implementations, it passes +the dynamic flow test, including the Retry module ported to sr3, and +a number of v2 modules used as-is. + +Completed an initial version of the sr3_post component now (in sr3: flowcb.gather.file.File) +Now working on sr_poll, which will take a while because it involve refactoring: sr_file, sr_http, +sr_ftp, sr_sftp into the transfer module + +Mostly done sr_subscribe, which, in the old version, is a base class for all other components, +but in sr3 is just the first component that actually downloads data. So encountering all +issues with data download, and flowcb that do interesting things. Mostly done, but +flowcb not quite working. + +sr_sarra was straightforward once sr_subscribe was done. + +re-implemented Transfer get to have conventional return value as the number of bytes +transferred, and if they differ, that signals an issue. + +sr_sender send now done, involved a lot more thinking about how to set new\_ fields +in notification messages. but once that was done, was able to remove both the sender and sr_subscribe +(the parent class of most components) and allowed removal of sr_cache, sr_consumer, sr_file, +sr_ftp, sr_http, sr_message, sr_retry, and sr_sftp, sum/\*, sr_util. + +That's the end of the most difficult part. + +There was one commit to reformat the entire codebase to PEP style using yapf3. +Now I have the yapf3 pre-commit hook that reformats changes so that the entire codebase +remains yapf3 formatted. + +Also have written message rate limiting into core, so now have message_rate_min, and message_rate_max +settings that replace/deprecate v2 post_rate_limit plugin. + + +Worries Addressed +~~~~~~~~~~~~~~~~~ + +This section contains issues that were taken care of. They were a bother for a while, +so noting down what the solution was. + +* logging using __name__ sometimes ends up claiming to be from the wrong file. + example:: + + 2020-08-16 01:31:52,628 [INFO] sarra.sr_credentials set_newMessageFields FIXME new_dir=/home/peter/sarra_devdocroot/download + + set_newMessageFields is in config.py not sr_credentials... why it is doing that? + Likely wait until all legacy code is replaced before tackling this. + if this doesn't get fixed, then make it a bug report. + + fixed: note... the problem was that the logger declaration must be AFTER all + imports. Concretely:: + + logger = logging.getLogger( __name__ ) + + must be placed after all imports. + +* sr_audit ? what to do. Removed. + +* all non entry_point sr_*.py files can be removed. + remove sum sub-directory. sr_util.py + + +Accel Overhaul +~~~~~~~~~~~~~~ + +plugin compatiblity under review... decided to re-write the accel_* plugins for sr3, and +change the API because the v2 one has fundamental deficiencies: + +* the do_get api deals with failure by raising an exception... there is no checking + of return codes on built-in routines... It is possiby taken care of by try/except, + but would prefer for a normal program flow to be able to trace and + report when an i/o failure happens (keep try/except to as small a scale as we can.) + +* there is a highly... idiosyncratic nature of the do_get, for example in the v2 accel_scp, + where it calls do_get, and then decides not to run and falls through to the built-in + one. This logic is rarely helpful, difficult to explain, and confusing to diagnose + in practice. + +Have re-written accel_wget, and accel_scp to the new api... working through static-flow +to test them. There is also logic to spot v2 invocations of them, and replace with sr3 +in the configuration. And the first attempt was quite convoluted... was not happy. +2nd attempt also... working on a third one. + +Re-wrote again, just adding getAccelerated() to the Transfer API, so it is built-in +instead of being a plugin. Any Transfer class can specify an accelerator and it +will be triggered by accel_threshold. https and sftp/scp accelerators are implemented. + +DoneTodo +-------- + +Items from the TODO list that have been addressed. + +* migrate sr_xattr.py to sarra/xattr.py (now called sarracenia/filemetadata.py) + +* fix flakey_broker test to pass. (done!) + +* update documentation... change everything to use sr3 entry point, yes done. + (See transition point below.) + +* consider transition, life with both versions... should sr.py --> sr3.py ? Yes. Done + should we have a separate debian package with transition entry points + (sr_subscribe and friends only included in compat package, and all) + interactivity natively only happens through sr3? + now called metpx-sr3 + +* perhaps move the whole plugin thing up a level (get rid of directory) + so Plugin becomes a class instantiated in sarra/__init__.py... puts + plugins and built-in code on a more even level... for example how + do plugin transfer protocols work? thinking... This is sort of done + now: plugin became flowcb. Identity is removed from the hierarchy. + Class extension is now a separate kind of plugin (via import) + +* change default topic_prefix to v03.post done 2021/02 + +* change default topic_prefix to v03 done 2021/03 + +* change topic_prefix to topicPrefix done 2021/03 + +* Adjust Programmer's Guide to reflect new API. done 2021/02 + +* log incoherency between 'info' and logging.INFO prevents proper log control. + FIXED 2021/02. + +* missing accelerators: sftp.putAcc, ftp.putAc, ftp.getAc, file.getAc, + +* migrate sr_credentials.py to sarracenia/credentials.py. + +* remove *post* from v03 topic trees. Done! + +* cleanup entry points: sr_audit, sr_tailf, sr_log2save, + +* test with dynamic-flow. + +* MQTT Support (Done!) + + +BUGS/Concerns/Issues +-------------------- + +migrated to github issues with v3only tag. + +After Parity: True Improvements +------------------------------- + +TODO +---- + +At this point am able to report existing problems as issues with the v03only tag. +so below is the things leftover after refactor: + +* added "missing defaults" message, examine list, and see if we should set them all. + check_undeclared_options missing defaults: {'discard', 'exchangeSplit', + 'pipe', 'post_total_maxlag', 'exchangeSuffix', 'destination', 'inplace', + 'report_exchange', 'post_exchangeSplit', 'set_passwords', 'declare_exchange', + 'sanity_log_dead', 'report_daemons', 'realpathFilter', 'reconnect', + 'post_exchangeSuffix', 'save', 'cache_stat', 'declare_queue', + 'bind_queue', 'dry_run', 'sourceFromExchange', 'retry_mode', 'poll_without_vip', 'header'} + #405 + +* #369 ... clean shutdown + +* figure out an AsyncAPI implementation for subscription at least. #401 + +* get partitioned file transfers working again. #396 + ``_ + +* convert existing poll to poll0 ? old poll. #394 + +* alarm_set truncates to integers... hmm.. use setitimer instead? #397 + +* outlet option is missing. #398 + +* vhost support needed. #384 + +* sr_poll active/passive bug #29 + +* realpathFilter is used by CMOI. Seems to be disappeared in sr3. It's there in the C version. #399 + +* port rest of v02 plugins to v03 equivalents and add mappings in config.py, #400 + so that we have barely any v2's left. + +* transfer/sftp.py remove file_index from implementation ( #367 ) depend on NoDupe.py + +* full async mode for MQP's. requires publish_retry functionality. + (again in future plans above.) #392 + +* once full async mode available, allow multiple gathers and publishes. + (again in future plans above.) #392 + +* #33 add hostname to default queue. + +* #348 add statehost to .cache directory tree. + + + +Not Baked/Thinking +------------------ + +Structural code things that are not settled, may change. +Probably need to be settled before having anyone else dive in. + +* scopable properties for internal classes, like they exist for plugins. #402 + I think this is done. Would have to document somewhere, + testing and demoing at the same time. + +* took the code required to implement set_newMessageFields (now called + sarracenia.Message.updateFieldsAccept) verbatim from v2. + It is pretty hairy... perhaps turn into a plugin, to get it out of the + main code? Don't think it will ever go away. It is fairly ugly, but + very useful and heavily used in existing configs. probably OK. + +* changing recovery model, so that all retry/logic is in main loop, #392 + and moth just returns immediately. Point being could have multiple + gathers for multiple upstreams, and get notification messages from whichever is + live... also end up with a single loop that way... cleaner. + likely equivalent to async mode mentioned above. + +* *gather* as a way of separating having multiple input brokers. #392 + so could avoid needing a winnow, but just having a subscriber connect to + multiple upstreams directly. + likely equivalent to async, and multi-gather. + +* think about API by sub-classing flow... and having it auto-integrate + with sr3 entry point... hmm... likely look at this when updating + Programmer's Guide. + +* more worklists? rename failed -> retry or deferred. Add a new failed + where failed represents a permanent failure. and the other represents + to be retried later. + +* `MQTT issues `_ + + +FIXME/Deferred +-------------- + +The point of the main sr3 work is to get a re-factor done to the point where +the code is understandable to new coders, so that tasks can be assigned. +This section includes a mix of tasks that can hopefully be assigned, + +FIXME are things left to the side that need to be seen to. + + +* **RELEASE BLOCKER** hairy. #403 + watch does not batch things. It just dumps an entire tree. + This will need to be re-wored before release into an iterator style approach. + so if you start in a tree with a million files, it will scan the entire million + and present them as a single in memory worklist. This will have performance + problems. want to incrementally proceed though lists one 'prefetch' batch + at a time. + + There is an interim fix to pretend it does batching properly, but the memory + impact and delay to producing the first file is still there, but at least + returns one batch at a time. + +* **RELEASE BLOCKER** logs of sr_poll and watch tend to get humungous way too quickly. #389 + +* try out jsonfile for building notification messages to post. can build json incrementally, #402 + so you do not need to delete the _deleteOnPost elements (can just skip over them) + +* um... add the protocols. mqtt and qpid-proton (amq1) #389 + +* make sure stop actually works... seeing strays after tests... but changing too much + to really know. need to check. It does! + +* We gave up on partitioned sending as a retrenchment for the refactor. It will come in a + later version. + +* reporting features mostly removed. + +Transition +---------- + +Do not know if straightforward (Replacement) upgrade is a good approach. Will it be possible to test sarra +sufficiently such that upgrades of entire pumps are possible? or will incremental (parallel) upgrades +be required? + +It depends on whether sr3 will work as a drop-in replacement or not. There is some incompatibility +we know will happen with do_* plugins. If that is sufficiently well documented and easily +dealt with, then it might not be a problem. On the other hand, if there are subtle +problems, then a parallel approach might be needed. + +Replacement +~~~~~~~~~~~ + +The package has the same name as v2 ones (metpx-sarracenia) differing only in version number. +Installing the new replaces the old completely. This requires that the new version be equal +or better than the old in all aspects, or that installation be confined to test machines +until that point is reached. + +This takes longer to get initial installation, but has much clearer demarcation (you know +when you are done.) + + +Parallel +~~~~~~~~ + +Name the package metpx-sarra3 and have the python class directory be sarra3 (instead of sarra.) +(also ~/.config/sr3 and ~/.cache/sr3. likely the .cache files must be different because +retry files have different formats? validate. ) So one can copy configurations from old to +new and run both versions in parallel. The central entry point would be sr3 (rather than +sr), and to avoid confusion the other entry points (sr_subscribe etc...) would be omitted +so that v2 code would work unchanged. Might require some tweaks to have the sr3 classes +ignore instances from the other versions. + +This is similar to python2 to python3 transition. Allows deployment of sr3 without having +to convert entirely to it. Allows running some components, and building maturity slowly +while others are not ready. It facilitates A:B testing, running the same configuration +with one version or the other without having the install or use a different machine, +facilitating verification of compatibility. + +Conclusion +~~~~~~~~~~ + +Have implemented Parallel model, with APPNAME=sr3 ( ~/.config/sr3, ~/.cache/sr3 ) +sr3\_ prefix replacing sr\_ for all commands, and changing the sarra Python class to +the full sarracenia name to avoid clashing python classes. + + + +Incompatibilities +----------------- + +There are not supposed to be any. This is a running list of things to fix or document. +breaking changes: + + +* in sr3, use -- for full word options, like --config, or --broker. In v2 you could use -config and -broker, + but that will end badly in sr3. In the old command line parser, -config, and --config were the same, which + was idiosyncratic. The new + command line option parser is built on ArgParse, and interprets a single - as prefix a single option where the + the subsequent letters are and argument. Example + + -config hoho.conf -> in v2 refers to loading the hoho.conf file as a configuration. + + in sr3, it will be interpreted as -c (config) load the onfig.conf gile, and hoho.conf is part of some subsequent option. + +* loglevel none -> loglevel notset (now passing loglevel setting directly to python logging module, none isn't defined.) + +* log messages and output in interactive, will be completely different. + +* dropped settings: use_amqplib, use_pika... replaced by separate per protocol implementation libraries. amqp uses the 'amqp' library which is neither of the above. ( commit 02fad37b89c2f51420e62f2f883a3828d2056de1 ) + +* dropping on_watch plugins. afaict, no-one uses them. The way v03 works it would be an after_accept for a watch. + makes more sense that way anyways. + +* plugins that access internals of sr_retry need to be rewritten, as the class is now plugin/retry.py. + the way to queue something for retry in current plugins is to append them to the failed queue. + This is only an issue in the flow tests of sr_insects. + +* do_download and do_send were 1st pass at *schemed* plugins, I think they should be deprecated/replaced + by do_get and do_put. unclear whether there is a need for these anymore (download and send plugins are + at wrong level of abstraction) + +* do_download, do_send, do_get, do_put are *schemed* downloads... that is, rather than stacking so that + all are called, they are registered for particular protocols. in v2, for example accel_* plugins would + register the "download" scheme. an on_message entry point would alter the scheme so that the do_* routine + would be invoked. In v2, the calling signature for all plugins is the same (self, parent) but for + these do_get and do_put cases, that is quite counter productive. so instead have a calling signature + identical to built-in protocol get/put... src_file, dst_file, src_offset, dst_offset, len ) + Resolution: just implement new Transfer classes, does not naturally fit in flowcb. + +* In v2, mirror default settings used to be False in all components except sr_sarra. + but the mirror setting was not honoured in shovel, and winnow (bug #358) + this bug is corrected in sr3, but then you notice that the default is wrong. + + In sr3, the default for mirror is changed to True for all flows except subscribe, which + is the least surprising behaviour given the default to False in v2. + +* in v2, download does not check the length of a file while it is downloading. + in sr3, it does. as an example, when using sftp as a poll, ls will list the size of a symbolic link. + When it downloads, it gets the actual file, and not the symlink, so the size is different. + + Example from flow test:: + + 2021-04-03 10:13:07,310 [ERROR] sarracenia.transfer read_writelocal util/writelocal mismatched file length writing FCAS31_KWBC_031412___39224.slink. Message said to expect 135 bytes. Got 114 bytes. + + the file is 114 bytes, by the link path is 135 bytes... + both v2 and sr3 download the file and not the link, but sr3 produces this error message. + Thinking about this one... is it a bug in poll? + +* In v2, if you delete a file, and then re-create it, an event will be created. + In sr3, if you do the same, the old entry will be in the nodupe cache, and the event will be suppressed. + I have noticed this difference, but not sure which version's behaviour is correct. + it could be fixed, if we decide the old behaviour is right. + + +Features +-------- + +* All the components are now derived from the *flow* class, and run the general algorithm already + designed as the basis of v2, but never implemented as such. + +* The extension API is now vanilla python with no magic settings. just standard classes, using standard import mechanism. + debugging should be much simpler now as the interpreter will provide much better error messages on startup. + The v2 style plugins are now called *flow callbacks*, and there are a number of classes (identity, moth, + transfer, perhaps flow) that permit extension by straightforward sub-classing. This should make it much + easier to add additional protocols for transport and messages, as well checksum algorithms for new data types. + +* sarra.moth class abstracts away AMQP, so messaging protocol becomes pluggable. + +* use the sarracenia/ prefix (already present) to replace sr\_ prefix on modules. + +* API access to flows. (so can build entirely new programs in python by subclassing.) + +* properties/options for classes are now hierarchical, so can set debug to specific classes within app. + +* sr3 ability to select multiple components and configurations to operate on. + +* sr3 list examples is now used to display examples separate from the installed ones. + +* sr3 show is now used to display the parsed configuration. + +* notification messages are acknowledged more quickly, should help with throughput. + +* FlowCB plugin entry_points are now based on groups of notification messages, rather than individual ones, allowing people + to organize concurrent work. + +* identity (checksums) are now plugins. + +* gather (inlet? sources of notification messages) are now plugins. + +* added typing to options settings, so plugins can declare: size, duration, string, or list. + diff --git a/_sources/Explanation/CommandLineGuide.rst.txt b/_sources/Explanation/CommandLineGuide.rst.txt new file mode 100644 index 000000000..188d48ce9 --- /dev/null +++ b/_sources/Explanation/CommandLineGuide.rst.txt @@ -0,0 +1,2641 @@ +================== +Command Line Guide +================== + + +SR3 - Everything +================ + +**sr3** is a command line tool to manage `Sarracenia `_ +configurations, individually or in groups. For the current user, it reads on all +of the configuration files, state files, and consults the process table to determine +the state of all components. It then makes the change requested. + + **sr3** *options* *action* [ *component/configuration* ... ] + +sr3 components are used to publish to and download files from websites or file servers +that provide `sr_post(7) <../Reference/sr_post.7.html>`_ protocol notifications. Such sites +publish notification messages for each file as soon as it is available. Clients connect to a +*broker* (often the same as the server itself) and subscribe to the notifications. +The *sr_post* notifications provide true push notices for web-accessible folders (WAF), +and are far more efficient than either periodic polling of directories, or ATOM/RSS style +notifications. Sr_subscribe can be configured to post notification messages after they are downloaded, +to make them available to consumers for further processing or transfers. + +**sr3** can also be used for purposes other than downloading, (such as for +supplying to an external program) specifying the -n (equal to: *download off*) will +suppress the download behaviour and only post the URL on standard output. The standard +output can be piped to other processes in classic UNIX text filter style. + +The components of sarracenia are groups of defaults on the main algorithm, +to reduce the size of individual components. The components are: + + - cpump - copy notification messages from one pump another second one (a C implementation of shovel.) + - flow - generic flow with no default behaviours. Good basis for building user defined components. + - poll - poll a non-sarracenia web or file server to create notification messages for processing. + - post & watch - create notification messages for files for processing. + - sarra _ - download file from a remote server to the local one, and re-post them for others. + - sender - send files from a local server to a remote one. + - shovel - copy notification messages, only, not files. + - watch - create notification messages for each new file that arrives in a directory, or at a set path. + - winnow - copy notification messages, suppressing duplicates. + +All of these components accept the same options, with the same effects. +There is also `sr3_cpump(1) <../Reference/sr3_cpump.1.html>`_ which is a C version that implements a +subset of the options here, but where they are implemented, they have the same effect. + +The **sr3** command usually takes two arguments: an action followed by a list +of configuration files. When any component is invoked, an operation and a +configuration file are specified. If the configuration is omitted, it means to +apply the action to all configurations. The action is one of: + + - foreground: run a single instance in the foreground logging to stderr + - restart: stop and then start the configuration. + - sanity: looks for instances which have crashed or gotten stuck and restarts them. + - start: start the configuration running + - status: check if the configuration is running. + - stop: stop the configuration from running + +The remaining actions manage the resources (exchanges, queues) used by the component on +the broker, or manage the configurations. + + - cleanup: deletes the component's resources on the server. + - declare: creates the component's resources on the server. + - add: copy to the list of available configurations. + - list: list all the configurations available. + - list plugins: list all the plugins available. + - list examples: list all the examples available. + - show view an interpreted version of a configuration file. + - edit: modify an existing configuration. + - remove: remove a configuration. + - disable: mark a configuration as ineligible to run. + - enable: mark a configuration as eligible to run. + - convert: converts v2 configs to a v3 config. + + +For example: *sr3 foreground subscribe/dd* runs the subscribe component with +the dd configuration as a single foreground instance. + +The **foreground** action is used when building a configuration or for debugging. +The **foreground** instance will run regardless of other instances which are currently +running. Should instances be running, it shares the same notification message queue with them. +A user stop the **foreground** instance by simply using on linux +or use other means to kill the process. + +After a configuration has been refined, *start* to launch the component as a background +service (daemon or fleet of daemons whose number is controlled by the *instances* option.) +If multiple configurations and components need to be run together, the entire fleet +can be similarly controlled using the `sr3(1) <../Reference/sr3.1.html>`_ command. + +To have components run all the time, on Linux one can use `systemd `_ integration, +as described in the `Admin Guide <../How2Guides/Admin.rst>`_ On Windows, one can configure a service, +as described in the `Windows user manual <../Tutorials/Windows.html>`_ + +The actions **cleanup**, **declare**, can be used to manage resources on +the rabbitmq server. The resources are either queues or exchanges. **declare** creates +the resources. + +The **add, remove, list, edit, enable & disable** actions are used to manage the list +of configurations. One can see all of the configurations available using the **list** +action. to view available plugins use **list plugins**. Using the **edit** option, +one can work on a particular configuration. A *disabled* configuration will not be +started or restarted by the **start**, +**foreground**, or **restart** actions. It can be used to set aside a configuration +temporarily. + +The **convert** action is used to translate configuration files written with Sarracenia version 2 +options into Sarracenia version 3 options. The v2 configuration file must be placed in the +*~/.config/sarra/component/v2_config.conf* directory and the translated version will be placed in +the *~/.config/sr3/component/v3_config.conf* directory. For example, one would invoke this action +with *sr3 convert component/config*. + + +ACTIONS +------- + +declare +~~~~~~~ + +Call the corresponding function for each configuration:: + + $ sr3 declare + declare: 2020-09-06 23:22:18,043 [INFO] root declare looking at cpost/pelle_dd1_f04 + 2020-09-06 23:22:18,048 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/) + 2020-09-06 23:22:18,049 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan01 (as: amqp://tfeed@localhost/) + 2020-09-06 23:22:18,049 [INFO] root declare looking at cpost/veille_f34 + 2020-09-06 23:22:18,053 [INFO] sarra.moth.amqp __putSetup exchange declared: xcpublic (as: amqp://tfeed@localhost/) + 2020-09-06 23:22:18,053 [INFO] root declare looking at cpost/pelle_dd2_f05 + ... + 2020-09-06 23:22:18,106 [INFO] root declare looking at cpost/pelle_dd2_f05 + 2020-09-06 23:22:18,106 [INFO] root declare looking at cpump/xvan_f14 + 2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f14.23011811.49631644 (as: amqp://tfeed@localhost/) + 2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup um..: pfx: v03, exchange: xcvan00, values: # + 2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_cpump.xvan_f14.23011811.49631644 with v03.# to xcvan00 (as: amqp://tfeed@localhost/) + 2020-09-06 23:22:18,111 [INFO] root declare looking at cpump/xvan_f15 + 2020-09-06 23:22:18,115 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f15.50074940.98161482 (as: amqp://tfeed@localhost/) + +Declares the queues and exchanges related to each configuration. +One can also invoke it with *\-\-users*, so that it will declare users as well as exchanges and queues:: + + $ sr3 --users declare + ... + 2020-09-06 23:28:56,211 [INFO] sarra.rabbitmq_admin add_user permission user 'ender' role source configure='^q_ender.*|^xs_ender.*' write='^q_ender.*|^xs_ender.*' read='^q_ender.*|^x[lrs]_ender.*|^x.*public$' + ... + +Providing a flow/flows will declare only the users that are specified in the flow(s):: + + $ sr3 --users declare subscribe/dd_amis + ... + declare: 2024-05-17 20:02:18,548 434920 [INFO] sarracenia.rabbitmq_admin add_user permission user 'tfeed@localhost' role feeder configure=.* write=.* read=.* + ... + +dump +~~~~ + +print the three data structure used by sr. There are three lists: + +* processes thought to be related to sr. + +* configurations present + +* contents of the state files. + +**dump** is used for debugging or to get more detail than provided by status:: + + Running Processes + 4238: name:sr_poll.py cmdline:['/usr/bin/python3', '/home/peter/src/sarracenia/sarra/sr_poll.py', '--no', '1', 'start', 'pulse'] + . + . + . + Configs + cpost + veille_f34 : {'status': 'running', 'instances': 1} + + States + cpost + veille_f34 : {'instance_pids': {1: 4251}, 'queue_name': None, 'instances_expected': 0, 'has_state': False, 'missing_instances': []} + + Missing + + +It is quite long, and so a bit too much information to look at in a raw state. +Usually used in conjunction with linux filters, such as grep. +for example:: + + $ sr3 dump | grep stopped + WMO_mesh_post : {'status': 'stopped', 'instances': 0} + shim_f63 : {'status': 'stopped', 'instances': 0} + test2_f61 : {'status': 'stopped', 'instances': 0} + + $ sr3 dump | grep disabled + amqp_f30.conf : {'status': 'disabled', 'instances': 5} + + +provides easy method to determine which configurations are in a particular state. +Another example, if *sr3 status* reports sender/tsource2send_f50 as being partial, then +one can use dump to get more detail:: + + $ sr3 dump | grep sender/tsource2send_f50 + 49308: name:sr3_sender.py cmdline:['/usr/bin/python3', '/usr/lib/python3/dist-packages/sarracenia/instance.py', '--no', '1', 'start', 'sender/tsource2send_f50'] + q_tsource.sr_sender.tsource2send_f50.58710892.12372870: ['sender/tsource2send_f50'] + + +foreground +~~~~~~~~~~ + +run a single instance of a single configuration as an interactive process logging to the current stderr/terminal output. +for debugging. + +**list** + +shows the user the configuration files present:: + + $ sr3 list + User Configurations: (from: /home/peter/.config/sarra ) + cpost/pelle_dd1_f04.conf cpost/pelle_dd2_f05.conf cpost/veille_f34.conf + cpump/xvan_f14.conf cpump/xvan_f15.conf poll/f62.conf + post/shim_f63.conf post/t_dd1_f00.conf post/t_dd2_f00.conf + post/test2_f61.conf sarra/download_f20.conf sender/tsource2send_f50.conf + shovel/rabbitmqtt_f22.conf subscribe/amqp_f30.conf subscribe/cclean_f91.conf + subscribe/cdnld_f21.conf subscribe/cfile_f44.conf subscribe/cp_f61.conf + subscribe/ftp_f70.conf subscribe/q_f71.conf subscribe/rabbitmqtt_f31.conf + subscribe/u_sftp_f60.conf watch/f40.conf admin.conf + credentials.conf default.conf + logs are in: /home/peter/.cache/sarra/log + +The last line says which directory the log files are in. + +Also *list examples* shows included configuration templates available as starting points with the *add* action:: + + $ sr3 list examples + Sample Configurations: (from: /home/peter/Sarracenia/development/sarra/examples ) + cpump/cno_trouble_f00.inc poll/aws-nexrad.conf poll/pollingest.conf + poll/pollnoaa.conf poll/pollsoapshc.conf poll/pollusgs.conf + poll/pulse.conf post/WMO_mesh_post.conf sarra/wmo_mesh.conf + sender/ec2collab.conf sender/pitcher_push.conf shovel/no_trouble_f00.inc + subscribe/WMO_Sketch_2mqtt.conf subscribe/WMO_Sketch_2v3.conf subscribe/WMO_mesh_CMC.conf + subscribe/WMO_mesh_Peer.conf subscribe/aws-nexrad.conf subscribe/dd_2mqtt.conf + subscribe/dd_all.conf subscribe/dd_amis.conf subscribe/dd_aqhi.conf + subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf subscribe/dd_cmml.conf + subscribe/dd_gdps.conf subscribe/dd_ping.conf subscribe/dd_radar.conf + subscribe/dd_rdps.conf subscribe/dd_swob.conf subscribe/ddc_cap-xml.conf + subscribe/ddc_normal.conf subscribe/downloademail.conf subscribe/ec_ninjo-a.conf + subscribe/hpfx_amis.conf subscribe/local_sub.conf subscribe/pitcher_pull.conf + subscribe/sci2ec.conf subscribe/subnoaa.conf subscribe/subsoapshc.conf + subscribe/subusgs.conf watch/master.conf watch/pitcher_client.conf + watch/pitcher_server.conf watch/sci2ec.conf + + + $ sr3 add dd_all.conf + add: 2021-01-24 18:04:57,018 [INFO] sarracenia.sr add copying: /usr/lib/python3/dist-packages/sarracenia/examples/subscribe/dd_all.conf to /home/peter/.config/sr3/subscribe/dd_all.conf + $ sr3 edit dd_all.conf + +The **add, remove, list, edit, enable & disable** actions are used to manage the list +of configurations. One can see all of the configurations available using the **list** +action. to view available plugins use **list plugins**. Using the **edit** option, +one can work on a particular configuration. A *disabled* sets a configuration aside +(by adding *.off* to the name) so that it will not be started or restarted by +the **start**, **foreground**, or **restart** actions. + +show +~~~~ + +View all configuration settings (the result of all parsing... what the flow components actually see):: + + + % sr3 show subscribe/q_f71 + 2022-03-20 15:30:32,507 1084652 [INFO] sarracenia.config parse_file download_f20.conf:35 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept" + 2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file tsource2send_f50.conf:26 obsolete v2:"on_message msg_rawlog" converted to sr3:"logEvents after_accept" + 2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file rabbitmqtt_f22.conf:6 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept" + + Config of subscribe/q_f71: + {'_Config__admin': 'amqp://bunnymaster@localhost/ None True True False False None None', + '_Config__broker': 'amqp://tsource@localhost/ None True True False False None None', + '_Config__post_broker': None, + 'accelThreshold': 0, + 'acceptSizeWrong': False, + 'acceptUnmatched': False, + 'admin': 'amqp://bunnymaster@localhost/ None True True False False None None', + 'attempts': 3, + 'auto_delete': False, + 'baseDir': None, + 'baseUrl_relPath': False, + 'batch': 1, + 'bindings': [('xs_tsource_poll', ['v03', 'post'], ['#'])], + 'broker': 'amqp://tsource@localhost/ None True True False False None None', + 'bufsize': 1048576, + 'byteRateMax': None, + 'cfg_run_dir': '/home/peter/.cache/sr3/subscribe/q_f71', + 'component': 'subscribe', + 'config': 'q_f71', + 'currentDir': None, + 'debug': False, + 'declared_exchanges': [], + 'declared_users': {'anonymous': 'subscriber', 'eggmeister': 'subscriber', 'ender': 'source', 'tfeed': 'feeder', 'tsource': 'source', 'tsub': 'subscriber'}, + 'delete': False, + 'destfn_script': None, + 'directory': '//home/peter/sarra_devdocroot/recd_by_srpoll_test1', + 'discard': False, + 'documentRoot': None, + 'download': True, + 'durable': True, + 'env_declared': ['FLOWBROKER', 'MQP', 'SFTPUSER', 'TESTDOCROOT'], + 'exchange': 'xs_tsource_poll', + 'exchangeDeclare': True, + 'exchangeSuffix': 'poll', + 'expire': 1800.0, + 'feeder': ParseResult(scheme='amqp', netloc='tfeed@localhost', path='/', params='', query='', fragment=''), + 'fileEvents': {'create', 'link', 'modify', 'delete', 'mkdir', 'rmdir' }, + 'file_total_interval': '0', + 'filename': None, + 'fixed_headers': {}, + 'flatten': '/', + 'hostdir': 'fractal', + 'hostname': 'fractal', + 'housekeeping': 300, + 'imports': [], + 'inflight': None, + 'inline': False, + 'inlineByteMax': 4096, + 'inlineEncoding': 'guess', + 'inlineOnly': False, + 'instances': 1, + 'identity_arbitrary_value': None, + 'identity_method': 'sha512', + 'logEvents': {'after_work', 'after_accept', 'on_housekeeping'}, + 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', + 'logLevel': 'info', + 'logReject': False, + 'logRotateCount': 5, + 'logRotateInterval': 86400.0, + 'logStdout': True, + 'log_flowcb_needed': False, + 'masks': ['accept .* into //home/peter/sarra_devdocroot/recd_by_srpoll_test1 with mirror:True strip:.*sent_by_tsource2send/'], + 'messageAgeMax': 0, + 'messageCountMax': 0, + 'messageDebugDump': False, + 'messageRateMax': 0, + 'messageRateMin': 0, + 'message_strategy': {'failure_duration': '5m', 'reset': True, 'stubborn': True}, + 'message_ttl': 0, + 'mirror': True, + 'msg_total_interval': '0', + 'fileAgeMax': 0, + 'nodupe_ttl': 0, + 'overwrite': True, + 'permCopy': True, + 'permDefault': 0, + 'permDirDefault': 509, + 'permLog': 384, + 'plugins_early': [], + 'plugins_late': ['sarracenia.flowcb.log.Log'], + 'post_baseDir': None, + 'post_baseUrl': None, + 'post_broker': None, + 'post_documentRoot': None, + 'post_exchanges': [], + 'post_topicPrefix': ['v03', 'post'], + 'prefetch': 25, + 'pstrip': '.*sent_by_tsource2send/', + 'queueBind': True, + 'queueDeclare': True, + 'queueName': 'q_tsource_subscribe.q_f71.76359618.62916076', + 'queue_filename': '/home/peter/.cache/sr3/subscribe/q_f71/subscribe.q_f71.tsource.qname', + 'randid': 'cedf', + 'randomize': False, + 'realpathPost': False, + 'rename': None, + 'report': False, + 'reset': False, + 'resolved_qname': 'q_tsource_subscribe.q_f71.76359618.62916076', + 'retry_ttl': 1800.0, + 'settings': {}, + 'sleep': 0.1, + 'statehost': False, + 'strip': 0, + 'subtopic': [], + 'timeCopy': True, + 'timeout': 300, + 'timezone': 'UTC', + 'tls_rigour': 'normal', + 'topicPrefix': ['v03', 'post'], + 'undeclared': ['msg_total_interval', 'file_total_interval'], + 'users': False, + 'v2plugin_options': [], + 'v2plugins': {'plugin': ['msg_total_save', 'file_total_save']}, + 'vhost': '/', + 'vip': []} + + % + + +convert +~~~~~~~ + +Converting a config: both formats are accepted, as well as include files:: + + $ sr3 convert poll/sftp_f62 + 2022-06-14 15:00:00,762 1093345 [INFO] root convert converting poll/sftp_f62 from v2 to v3 + + $ sr3 convert poll/sftp_f62.conf + 2022-06-14 15:01:11,766 1093467 [INFO] root convert converting poll/sftp_f62.conf from v2 to v3 + + $ sr3 convert shovel/no_trouble_f00.inc + 2022-06-14 15:03:29,918 1093655 [INFO] root convert converting shovel/no_trouble_f00.inc from v2 to v3 + +To overwrite an existing sr3 configuration use the *--wololo* option. +When overwriting multiple sr3 configurations, one must also use *--dangerWillRobinson=n* in the +normal way... where *n* is the number of configurations to convert. + + +start +~~~~~ + +launch all configured components:: + + $ sr3 start + gathering global state: procs, configs, state files, logs, analysis - Done. + starting...Done + + +stop +~~~~ + +stop all processes:: + + $ sr3 stop + gathering global state: procs, configs, state files, logs, analysis - Done. + stopping........Done + Waiting 1 sec. to check if 93 processes stopped (try: 0) + All stopped after try 0 + + + +status +~~~~~~ + +Sample OK status (sr is running):: + + fractal% sr3 status + status: + Component/Config Processes Connection Lag Rates + State Run Retry msg data Queued LagMax LagAvg Last %rej pubsub messages RxData TxData + ----- --- ----- --- ---- ------ ------ ------ ---- ---- ------ -------- ------ ------ + cpost/veille_f34 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s + cpump/pelle_dd1_f04 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 31.3% 0 Bytes/s 4 msgs/s 0 Bytes/s 0 Bytes/s + cpump/pelle_dd2_f05 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 31.3% 0 Bytes/s 4 msgs/s 0 Bytes/s 0 Bytes/s + cpump/xvan_f14 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s + cpump/xvan_f15 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s + poll/f62 run 1/1 0 100% 0% 0 0.08s 0.04s 1.4s 0.0% 2.0 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s + post/shim_f63 stop 0/0 - - - - - - - + post/test2_f61 stop 0/0 0 100% 0% 0 0.02s 0.01s 0.4s 0.0% 8.1 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s + sarra/download_f20 run 3/3 0 100% 10% 0 13.17s 5.63s 1.8s 0.0% 5.4 KiB/s 4 msgs/s 1.7 KiB/s 0 Bytes/s + sender/tsource2send_f50 run 10/10 0 100% 9% 0 1.37s 1.08s 1.9s 0.0% 8.1 KiB/s 5 msgs/s 0 Bytes/s 1.7 KiB/s + shovel/pclean_f90 run 3/3 136 100% 0% 0 0.00s 0.00s 0.6s 0.0% 4.0 KiB/s 5 msgs/s 0 Bytes/s 0 Bytes/s + shovel/pclean_f92 run 3/3 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s + shovel/rabbitmqtt_f22 run 3/3 0 100% 0% 0 0.89s 0.67s 1.5s 0.0% 8.1 KiB/s 5 msgs/s 0 Bytes/s 0 Bytes/s + shovel/t_dd1_f00 run 3/3 0 100% 0% 124 23.15s 4.50s 0.1s 55.0% 3.9 KiB/s 9 msgs/s 0 Bytes/s 0 Bytes/s + shovel/t_dd2_f00 run 3/3 0 100% 0% 83 11.82s 3.50s 0.1s 49.2% 3.6 KiB/s 8 msgs/s 0 Bytes/s 0 Bytes/s + subscribe/amqp_f30 run 3/3 0 100% 12% 0 18.79s 9.22s 0.1s 0.0% 3.3 KiB/s 4 msgs/s 1.9 KiB/s 0 Bytes/s + subscribe/cclean_f91 run 3/3 145 100% 0% 1 0.00s 0.00s 0.4s 0.0% 2.3 KiB/s 6 msgs/s 0 Bytes/s 0 Bytes/s + subscribe/cdnld_f21 run 3/3 0 100% 17% 12 7.20s 2.81s 0.7s 0.0% 2.3 KiB/s 3 msgs/s 1.7 KiB/s 0 Bytes/s + subscribe/cfile_f44 run 3/3 0 100% 6% 1 3.32s 0.32s 0.4s 0.0% 2.3 KiB/s 6 msgs/s 1.7 KiB/s 0 Bytes/s + subscribe/cp_f61 run 3/3 0 100% 3% 0 6.42s 3.49s 1.6s 0.0% 4.2 KiB/s 6 msgs/s 635 Bytes/s 0 Bytes/s + subscribe/ftp_f70 run 3/3 0 100% 8% 0 1.18s 0.83s 0.2s 0.0% 1.8 KiB/s 3 msgs/s 1.8 KiB/s 0 Bytes/s + subscribe/q_f71 run 3/3 0 100% 0% 0 1.62s 0.57s 0.0s 0.0% 1.2 KiB/s 3 msgs/s 1.2 KiB/s 0 Bytes/s + subscribe/rabbitmqtt_f31 run 3/3 0 100% 11% 0 4.27s 1.95s 1.2s 0.0% 4.2 KiB/s 6 msgs/s 637 Bytes/s 0 Bytes/s + subscribe/u_sftp_f60 run 3/3 0 100% 1% 0 2.69s 2.23s 1.3s 0.7% 4.2 KiB/s 6 msgs/s 644 Bytes/s 0 Bytes/s + watch/f40 run 1/1 0 100% 0% 0 0.10s 0.05s 1.9s 0.0% 4.2 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s + winnow/t00_f10 run 1/1 0 100% 0% 0 12.31s 4.33s 3.5s 50.0% 3.2 KiB/s 3 msgs/s 0 Bytes/s 0 Bytes/s + winnow/t01_f10 run 1/1 0 100% 0% 0 11.59s 3.76s 0.1s 50.5% 4.2 KiB/s 4 msgs/s 0 Bytes/s 0 Bytes/s + Total Running Configs: 25 ( Processes: 64 missing: 0 stray: 0 ) + Memory: uss:2.4 GiB rss:3.3 GiB vms:6.2 GiB + CPU Time: User:39.62s System:4.42s + Pub/Sub Received: 103 msgs/s (80.6 KiB/s), Sent: 63 msgs/s (32.8 KiB/s) Queued: 221 Retry: 281, Mean lag: 2.32s + Data Received: 32 Files/s (11.9 KiB/s), Sent: 5 Files/s (1.7 KiB/s) + + +Full sample:: + + + fractal% sr3 --full status + status: + Component/Config Processes Connection Lag Rates Counters (per housekeeping) Data Counters Memory CPU Time + State Run Retry msg data Queued LagMax LagAvg Last %rej pubsub messages RxData TxData subBytes Accepted Rejected Malformed pubBytes pubMsgs pubMal rxData rxFiles txData txFiles Since uss rss vms user system + ----- --- ----- --- ---- ------ ------ ------ ---- ---- ------ -------- ------ ------ ------- -------- -------- --------- ------- ------ ----- ----- ------- ------ ------- ----- --- --- --- ---- ------ + cpost/veille_f34 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 0 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 121.40s 2.4 MiB 5.9 MiB 15.2 MiB 0.03 0.08 + cpump/pelle_dd1_f04 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 83.5% 0 Bytes/s 11 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 1.4 Kim 1.1 Kim 0 msgs 0 Bytes 230 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 121.40s 3.9 MiB 6.8 MiB 17.2 MiB 0.12 0.11 + cpump/pelle_dd2_f05 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 83.5% 0 Bytes/s 11 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 1.4 Kim 1.1 Kim 0 msgs 0 Bytes 230 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 121.40s 3.9 MiB 7.0 MiB 17.2 MiB 0.17 0.06 + cpump/xvan_f14 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 0 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 12.90s 3.0 MiB 4.7 MiB 16.1 MiB 0.01 0.01 + cpump/xvan_f15 run 1/1 0 100% 0% 0 0.00s 0.00s n/a 0.0% 0 Bytes/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 1 msg 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 8.63s 3.0 MiB 4.6 MiB 16.1 MiB 0.01 0.02 + poll/f62 run 1/1 0 100% 0% 0 0.16s 0.05s 0.8s 0.0% 1.3 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 0 msgs 4.4 Kim 0 msgs 151.1 KiB 420 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 118.56s 45.1 MiB 59.0 MiB 229.3 MiB 2.62 0.32 + post/shim_f63 stop 0/0 - - - - - - - - - - - - - - - - - - - 0 Bytes 0 Bytes 0 Bytes 0.00 0.00 + post/test2_f61 stop 0/0 0 100% 0% 0 0.03s 0.02s 0.5s 0.0% 11.5 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 0 msgs 0 msgs 0 msgs 6.7 KiB 14 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 0.58s 0 Bytes 0 Bytes 0 Bytes 0.00 0.00 + sarra/download_f20 run 3/3 0 100% 0% 0 10.17s 2.87s 1.1s 0.0% 1.1 KiB/s 0 msgs/s 1.1 KiB/s 0 Bytes/s 27.1 KiB 47 msgs 0 msgs 0 msgs 36.0 KiB 47 msgs 0 msgs 65.6 KiB 47 Files 0 Bytes 0 Files 57.57s 132.5 MiB 192.4 MiB 280.1 MiB 2.72 0.40 + sender/tsource2send_f50 run 10/10 0 100% 9% 0 1.37s 0.52s 1.3s 0.0% 5.5 KiB/s 3 msgs/s 0 Bytes/s 1.3 KiB/s 326.0 KiB 421 msgs 0 msgs 0 msgs 326.6 KiB 421 msgs 0 msgs 0 Bytes 0 Files 152.7 KiB 421 Files 118.97s 425.6 MiB 562.8 MiB 2.2 GiB 7.80 0.93 + shovel/pclean_f90 run 3/3 310 100% 0% 0 82.03s 75.72s 0.7s 0.0% 5.6 KiB/s 3 msgs/s 0 Bytes/s 0 Bytes/s 111.4 KiB 120 msgs 0 msgs 0 msgs 99.9 KiB 111 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 38.02s 127.4 MiB 169.0 MiB 249.6 MiB 2.37 0.27 + shovel/pclean_f92 run 3/3 0 100% 0% 0 82.49s 76.06s 19.1s 0.0% 1.7 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s 99.9 KiB 111 msgs 0 msgs 0 msgs 103.0 KiB 111 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 118.28s 126.4 MiB 168.5 MiB 249.2 MiB 2.04 0.21 + shovel/rabbitmqtt_f22 run 3/3 0 100% 0% 0 1.25s 0.54s 1.3s 0.0% 5.5 KiB/s 3 msgs/s 0 Bytes/s 0 Bytes/s 326.0 KiB 421 msgs 0 msgs 0 msgs 326.0 KiB 421 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 118.77s 126.0 MiB 167.6 MiB 248.7 MiB 2.12 0.20 + shovel/t_dd1_f00 run 3/3 0 100% 0% 3 23.15s 3.06s 0.1s 82.3% 3.4 KiB/s 14 msgs/s 0 Bytes/s 0 Bytes/s 231.6 KiB 1.6 Kim 1.3 Kim 0 msgs 168.4 KiB 293 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 118.19s 127.3 MiB 171.0 MiB 250.1 MiB 2.77 0.32 + shovel/t_dd2_f00 run 3/3 0 100% 0% 0 11.82s 2.61s 0.2s 82.2% 3.3 KiB/s 13 msgs/s 0 Bytes/s 0 Bytes/s 225.7 KiB 1.6 Kim 1.3 Kim 0 msgs 166.6 KiB 290 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 118.33s 127.3 MiB 170.6 MiB 250.1 MiB 2.74 0.39 + subscribe/amqp_f30 run 3/3 0 100% 39% 0 18.79s 6.44s 0.7s 0.0% 1.5 KiB/s 2 msgs/s 1.2 KiB/s 0 Bytes/s 181.0 KiB 237 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 146.8 KiB 237 Files 0 Bytes 0 Files 117.95s 126.2 MiB 168.0 MiB 248.9 MiB 2.20 0.27 + subscribe/cclean_f91 run 3/3 213 100% 0% 0 0.00s 0.00s 0.5s 17.4% 2.7 KiB/s 6 msgs/s 0 Bytes/s 0 Bytes/s 35.5 KiB 92 msgs 16 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 13.37s 126.1 MiB 167.9 MiB 248.8 MiB 2.53 0.36 + subscribe/cdnld_f21 run 3/3 0 100% 41% 0 10.43s 3.13s 0.1s 0.0% 1.4 KiB/s 2 msgs/s 1.4 KiB/s 0 Bytes/s 167.1 KiB 262 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 162.7 KiB 262 Files 0 Bytes 0 Files 117.90s 131.6 MiB 190.7 MiB 277.6 MiB 3.39 0.42 + subscribe/cfile_f44 run 3/3 0 100% 40% 0 3.32s 0.30s 0.0s 0.0% 1.5 KiB/s 4 msgs/s 1.4 KiB/s 0 Bytes/s 178.3 KiB 509 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 161.8 KiB 509 Files 0 Bytes 0 Files 118.09s 130.9 MiB 188.5 MiB 278.0 MiB 2.62 0.30 + subscribe/cp_f61 run 3/3 0 100% 12% 0 6.42s 2.09s 0.1s 0.0% 2.8 KiB/s 3 msgs/s 597 Bytes/s 0 Bytes/s 326.6 KiB 421 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 69.0 KiB 123 Files 0 Bytes 0 Files 118.23s 125.9 MiB 166.8 MiB 248.9 MiB 2.48 0.31 + subscribe/ftp_f70 run 3/3 0 100% 39% 0 1.75s 0.77s 0.1s 0.0% 1.3 KiB/s 2 msgs/s 1.4 KiB/s 0 Bytes/s 158.4 KiB 340 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 159.8 KiB 340 Files 0 Bytes 0 Files 118.04s 126.1 MiB 167.4 MiB 248.8 MiB 2.22 0.36 + subscribe/q_f71 run 3/3 0 100% 31% 0 3.12s 1.18s 5.7s 0.0% 1.2 KiB/s 3 msgs/s 1.1 KiB/s 0 Bytes/s 142.7 KiB 396 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 124.6 KiB 396 Files 0 Bytes 0 Files 118.49s 135.0 MiB 191.9 MiB 928.8 MiB 4.30 0.68 + subscribe/rabbitmqtt_f31 run 3/3 0 100% 8% 0 4.27s 1.10s 1.1s 0.0% 2.8 KiB/s 3 msgs/s 598 Bytes/s 0 Bytes/s 326.0 KiB 421 msgs 0 msgs 0 msgs 0 Bytes 0 msgs 0 msgs 69.0 KiB 123 Files 0 Bytes 0 Files 118.15s 126.2 MiB 167.9 MiB 248.9 MiB 2.22 0.27 + subscribe/u_sftp_f60 run 3/3 0 100% 9% 0 2.69s 1.71s 1.2s 0.2% 2.8 KiB/s 3 msgs/s 599 Bytes/s 0 Bytes/s 326.6 KiB 421 msgs 1 msg 0 msgs 0 Bytes 0 msgs 0 msgs 69.0 KiB 122 Files 0 Bytes 0 Files 117.93s 126.4 MiB 167.8 MiB 249.2 MiB 2.05 0.33 + watch/f40 run 1/1 0 100% 0% 0 0.06s 0.02s 1.6s 0.0% 3.0 KiB/s 0 msgs/s 0 Bytes/s 0 Bytes/s 0 Bytes 0 msgs 74 msgs 0 msgs 169.5 KiB 203 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 57.21s 46.3 MiB 65.3 MiB 311.8 MiB 1.95 0.14 + winnow/t00_f10 run 1/1 0 100% 0% 0 6.38s 3.47s 0.2s 51.7% 1.7 KiB/s 2 msgs/s 0 Bytes/s 0 Bytes/s 66.8 KiB 116 msgs 60 msgs 0 msgs 32.3 KiB 56 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 57.79s 42.5 MiB 56.2 MiB 83.3 MiB 0.77 0.12 + winnow/t01_f10 run 1/1 0 100% 0% 0 9.75s 2.53s 0.1s 51.3% 1.8 KiB/s 2 msgs/s 0 Bytes/s 0 Bytes/s 67.2 KiB 117 msgs 60 msgs 0 msgs 32.8 KiB 57 msgs 0 msgs 0 Bytes 0 Files 0 Bytes 0 Files 56.37s 42.3 MiB 55.7 MiB 83.2 MiB 0.81 0.12 + Total Running Configs: 25 ( Processes: 64 missing: 0 stray: 0 ) + Memory: uss:2.5 GiB rss:3.4 GiB vms:7.4 GiB + CPU Time: User:53.06s System:7.00s + Pub/Sub Received: 99 msgs/s (63.2 KiB/s), Sent: 53 msgs/s (29.3 KiB/s) Queued: 3 Retry: 523, Mean lag: 4.54s + Data Received: 18 Files/s (9.3 KiB/s), Sent: 3 Files/s (1.3 KiB/s) + + +The first row of output gives categories for the following line: + +* Processes: report on the number of instances and, and whether any are missing. + +* Connection: indicators of health of connectivity with remote. + +* Lag: the degree of delay being experienced, or how old data is by the time downloads are complete. + +* Last: indicates last successful transfer (for components that move files) or last message received/posted. + +* Rates: the speed of transfers, various measures. + +* Counters: the basis for rate calculations, reset each housekeeping interval. + +* Memory: the memory usage of the processes in the configuration. + +* Cpu Time: the cpu time usage of the processes in the configuration. + +The last three categories will only be listed when the --full option is provided. + +The second row of output gives detailed headings within each category: + +The configurations are listed on the left. For each configuraion, the state +will be: + +* stop: no processes are running. +* run: all processes are running. +* part: some processes are running. +* disa: disabled, configured not to run. + +The next columns to the right give more information, detailing how many processes are Running, out of the number expected. +For example, 3/3 means 3 processes or instances found of the 3 expected to be found. +The Expected entry lists how many processes should be running based on the configuration, and whether it is stopped +or not. + +The Retry column is the number of notification messages stored in the local retry queue, indicating what channels are having +processing difficulties. + +============= ======================================================================================================== +Heading Purpose +------------- -------------------------------------------------------------------------------------------------------- +State General health/status of a given configuration: stop|run|disa|part|wVip|fore +Run Number of processes or instances running compared to expected. 3/10 3 processes running of 10 expected. +Retry The number of messages in retry queues, indicating problems with transfers. +msg The percentage of time connected to a message broker, to subscribe to or publish messages. +data The percentrage of time connected to data sources or sinks to transfer data. +LagQueued The number of messages queued on the (remote) broker, not yet picked up. +LagMax The maximum age of messages on receipt (prior to download), so delay upstream. +LagAvg The average age of messages on receipt (prior to download), so delay upstream. +Last Elapsed time since the last successful file (or message) transfer. +pubsub The rate of the pub/sub messages being downloaded in bytes/second +messages The rate of the pub/sub messages being downloaded in messages/second +%rej The percent of downloaded messages rejected (indicator of health of subscription tuning.) +RxData The amount of data being downloaded (not messages, but file data.) +TxData The amount of data being sent (not messages, but file data.) +subBytes Counter of the bytes of pub/sub messages being received. +Accepted Counter of number of messages accepted +Rejected Counter of number of messages rejected +Malformed Counter of messages which were discarded because they could not be understood. +pubBytes Counter of bytes of messages being published +pubMsgs Counter of messages being published +pubMal Counter of message which failed to publish. +rxData Counter of bytes of files being downloaded. +rxFiles Counter of files being downloaded. +txData Counter of bytes of files being sent. +txFiles Counter of files being sent. +Since How many seconds since the last counter reset (basis for rate calculations.) +uss unique set size (memory usage of instances.) Actual physical unique memory used by processes. +rss resident set size (memory usage of instances) Actual physical memory used, including shared. +vms virtual memory size of all shared and physical and swap allocated together. +user user cpu time used +system system cpu time used. +============= ======================================================================================================== + +At the bottom of the listing is a cumulation of these values. + + +Message Gathering +================= + +Most Metpx Sarracenia components loop on gathering and/or reception of sarracenia +notification messages. Usually, the notification messages are `sr_post(7) <../Reference/sr_post.7.html>`_ +notification messages, announcing the availability of a file by publishing its URL, +but there are also report messages which can be processed using the +same tools. AMQP messages are published to an exchange +on a broker (AMQP server). The exchange delivers notification messages to queues. To receive +notification messages, one must provide the credentials to connect to the broker. Once +connected, a consumer needs to create a queue to hold pending notification messages. +The consumer must then bind the queue to one or more exchanges so that they put +notification messages in its queue. + +Once the bindings are set, the program can receive notification messages. When a notification message is received, +further filtering is possible using regular expressions onto the AMQP messages. +After a notification message passes this selection process, and other internal validation, the +component can run an **after_accept** plugin script to perform additional notification message +processing. If this plugin returns False, the notification message is discarded. If True, +processing continues. + +The following sections explains all the options to set this "consuming" part of +sarracenia programs. + + + +Setting the Broker +------------------ + +**broker [amqp|mqtt]{s}://:@[:port]/** + +A URI is used to configure a connection to a notification message pump, either +an MQTT or an AMQP broker. Some Sarracenia components set a reasonable default for +that option. provide the normal user,host,port of connections. In most configuration files, +the password is missing. The password is normally only included in the credentials.conf file. + +Sarracenia work has not used vhosts, so **vhost** should almost always be **/**. + +for more info on the AMQP URI format: ( https://www.rabbitmq.com/uri-spec.html ) + + +either in the default.conf or each specific configuration file. +The broker option tell each component which broker to contact. + +**broker [amqp|mqtt]{s}://:@[:port]/** + +:: + (default: None and it is mandatory to set it ) + +Once connected to an AMQP broker, the user needs to bind a queue +to exchanges and topics to determine the notification messages of interest. + +Creating the Queue +------------------ + +Once connected to an AMQP broker, the user needs to create a queue. +Common settings for the queue on broker : + +- **queue (default: q_..)** +- **expire (default: 5m == five minutes. RECOMMEND OVERRIDING)** +- **message_ttl (default: None)** +- **prefetch (default: 1)** + + +Usually components guess reasonable defaults for all these values +and users do not need to set them. For less usual cases, the user +may need to override the defaults. The queue is where the notifications +are held on the server for each subscriber. + +[ queue|queue_name|qn ] +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, components create a queue name that should be unique. The +default queue_name components create follows the following convention: + + **q_....** + +Where: + +* *brokerUser* is the username used to connect to the broker (often: *anonymous* ) + +* *programName* is the component using the queue (e.g. *sr_subscribe* ), + +* *configName* is the configuration file used to tune component behaviour. + +* *random* is just a series of characters chosen to avoid clashes from multiple + people using the same configurations + +Users can override the default provided that it starts with **q_**. + +When multiple instances are used, they will all use the same queue, for trivial +multi-tasking. If multiple computers have a shared home file system, then the +queue_name is written to: + + ~/.cache/sarra///__.qname + +Instances started on any node with access to the same shared file will use the +same queue. Some may want use the *queue_name* option as a more explicit method +of sharing work across multiple nodes. + + +AMQP QUEUE BINDINGS +------------------- + +Once one has a queue, it must be bound to an exchange. +Users almost always need to set these options. Once a queue exists +on the broker, it must be bound to an exchange. Bindings define which +notification messages (URL notifications) the program receives. The root of the topic +tree is fixed to indicate the protocol version and type of the +notification message (but developers can override it with the **topicPrefix** +option.) + +These options define which notification messages (URL notifications) the program receives: + + - **exchange (default: xpublic)** + - **exchangeSuffix (default: None)** + - **topicPrefix (default: v03 -- developer option)** + - **subtopic (no default, must appear after exchange)** + + +subtopic (default: #) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Within an exchange's postings, the subtopic setting narrows the product selection. +To give a correct value to the subtopic, +one has the choice of filtering using **subtopic** with only AMQP's limited wildcarding and +length limited to 255 encoded bytes, or the more powerful regular expression +based **accept/reject** mechanisms described below. The difference being that the +AMQP filtering is applied by the broker itself, saving the notices from being delivered +to the client at all. The **accept/reject** patterns apply to notification messages sent by the +broker to the subscriber. In other words, **accept/reject** are client side filters, +whereas **subtopic** is server side filtering. + +It is best practice to use server side filtering to reduce the number of notification messages sent +to the client to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all. + +topicPrefix is primarily of interest during protocol version transitions, +where one wishes to specify a non-default protocol version of notification messages to +subscribe to. + +Usually, the user specifies one exchange, and several subtopic options. +**Subtopic** is what is normally used to indicate notification messages of interest. +To use the subtopic to filter the products, match the subtopic string with +the relative path of the product. + +For example, consuming from DD, to give a correct value to subtopic, one can +browse the our website **http://dd.weather.gc.ca** and write down all directories +of interest. For each directory tree of interest, write a **subtopic** +option as follow: + + **subtopic directory1.*.subdirectory3.*.subdirectory5.#** + +:: + + where: + * matches a single directory name + # matches any remaining tree of directories. + +note: + When directories have these wild-cards, or spaces in their names, they + will be URL-encoded ( '#' becomes %23 ) + When directories have periods in their name, this will change + the topic hierarchy. + + FIXME: + hash marks are URL substituted, but did not see code for other values. + Review whether asterisks in directory names in topics should be URL-encoded. + Review whether periods in directory names in topics should be URL-encoded. + +One can use multiple bindings to multiple exchanges as follows:: + + exchange A + subtopic directory1.*.directory2.# + + exchange B + subtopic *.directory4.# + +Will declare two separate bindings to two different exchanges, and two different file trees. +While default binding is to bind to everything, some brokers might not permit +clients to set bindings, or one might want to use existing bindings. +One can turn off queue binding as follows:: + + subtopic None + +(False, or off will also work.) + + + + + +Client-side Filtering +--------------------- + +We have selected our notification messages through **exchange**, **subtopic** and +perhaps patterned **subtopic** with AMQP's limited wildcarding which +is all done by the broker (server-side). The broker puts the +corresponding notification messages in our queue. The subscribed component +downloads these notification messages. Once the notification message is downloaded, Sarracenia +clients apply more flexible client side filtering using regular expressions. + +Brief Introduction to Regular Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Regular expressions are a very powerful way of expressing pattern matches. +They provide extreme flexibility, but in these examples we will only use a +very trivial subset: The . is a wildcard matching any single character. If it +is followed by an occurrence count, it indicates how many letters will match +the pattern. The * (asterisk) character, means any number of occurrences. +So: + + - .* means any sequence of characters of any length. In other words, match anything. + + - cap.* means any sequence of characters that starts with cap. + + - .*CAP.* means any sequence of characters with CAP somewhere in it. + + - .*cap means any sequence of characters that ends with CAP. In the case + where multiple portions of the string could match, the longest one is selected. + + - .*?cap same as above, but *non-greedy*, meaning the shortest match is chosen. + +Please consult various internet resources for more information on the full +variety of matching possible with regular expressions: + + - https://docs.python.org/3/library/re.html + - https://en.wikipedia.org/wiki/Regular_expression + - http://www.regular-expressions.info/ + + +accept, reject and accept_unmatch +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **accept (optional)** +- **reject (optional)** +- **acceptUnmatched (default: True)** +- **baseUrl_relPath (default: False)** + +The **accept** and **reject** options process regular expressions (regexp). +The regexp is applied to the the notification message's URL for a match. + +If the notification message's URL of a file matches a **reject** pattern, the notification message +is acknowledged as consumed to the broker and skipped. + +One that matches an **accept** pattern is processed by the component. + +In many configurations, **accept** and **reject** options are mixed +with the **directory** option. They then relate accepted notification messages +to the **directory** value they are specified under. + +After all **accept** / **reject** options are processed, normally +the notification message is acknowledged as consumed and skipped. To override that +default, set **accept_unmatch** to True. The **accept/reject** +settings are interpreted in order. Each option is processed orderly +from top to bottom. For example: + +sequence #1:: + + reject .*\.gif + accept .* + +sequence #2:: + + accept .* + reject .*\.gif + + +In sequence #1, all files ending in 'gif' are rejected. In sequence #2, the accept .* (which +accepts everything) is encountered before the reject statement, so the reject has no effect. + +It is best practice to use server side filtering to reduce the number of notification messages sent +to the component to a small superset of what is relevant, and perform only a fine-tuning with the +client side mechanisms, saving bandwidth and processing for all. More details on how +to apply the directives follow: + +Normally the relative path (appended to the base directory) for files which are downloaded +will be set according to the relPath header included in the notification message. if *baseUrl_relPath* +is set, however, the notification message's relPath will be prepended with the sub-directories from +the notification message's baseUrl field. + + +NAMING QUEUES +------------- + +While in most common cases, a good value is generated by the application, in some cases +there may be a need to override those choices with an explicit user specification. +To do that, one needs to be aware of the rules for naming queues: + +1. queue names start with q\_ +2. this is followed by (the owner/user of the queue's broker username) +3. followed by a second underscore ( _ ) +4. followed by a string of the user's choice. + +The total length of the queue name is limited to 255 bytes of UTF-8 characters. + + + + +POSTING +======= + +Just as many components consumer a stream of notification messages, many components +(often the same ones) also product an output stream of notification messages. To make files +available to subscribers, a poster publishes the notification messages to an AMQP or +MQTT server, also called a broker. The post_broker option sets all the +credential information to connect to the output **AMQP** broker. + +**post_broker [amqp|mqtt]{s}://:@[:port]/** + +Once connected to the source AMQP broker, the program builds notifications after +the download of a file has occurred. To build the notification and send it to +the next hop broker, the user sets these options : + +* **post_baseDir (optional)** +* **post_topicPrefix (default: 'v03')** +* **post_exchange (default: xpublic)** +* **post_baseUrl (MANDATORY)** + +FIXME: Examples of what these are for, what they do... + + +NAMING EXCHANGES +---------------- + +1. Exchange names start with x +2. Exchanges that end in *public* are accessible (for reading) by any authenticated user. +3. Users are permitted to create exchanges with the pattern: xs__ such exchanges can be written to only by that user. +4. The system (sr_audit or administrators) create the xr_ exchange as a place to send reports for a given user. It is only readable by that user. +5. Administrative users (admin or feeder roles) can post or subscribe anywhere. + +For example, xpublic does not have xs\_ and a username pattern, so it can only be posted to by admin or feeder users. +Since it ends in public, any user can bind to it to subscribe to notification messages posted. +Users can create exchanges such as xs__public which can be written to by that user (by rule 3), +and read by others (by rule 2.) A description of the conventional flow of notification messages through exchanges on a pump. +Subscribers usually bind to the xpublic exchange to get the main data feed. This is the default in sr_subscribe. + +Another example, a user named Alice will have at least two exchanges: + + - xs_Alice the exhange where Alice posts her file notifications and report messages (via many tools). + - xr_Alice the exchange where Alice reads her report messages from (via sr_shovel). + - Alice can create a new exchange by just posting to it (with sr3_post or sr_cpost) if it meets the naming rules. + +Usually an sr_sarra run by a pump administrator will read from an exchange such as xs_Alice_mydata, +retrieve the data corresponding to Alice´s *post* notification message, and make it available on the pump, +by re-announcing it on the xpublic exchange. + + +POLLING +======= + +Polling is doing the same job as a post, except for files on a remote server. +In the case of a poll, the post will have its url built from the *pollUrl* +option, with the product's path (*path*/"matched file"). There is one +post per file. The file's size is taken from the directory "ls"... but its +checksum cannot be determined, so the default identity method is "cod", asking +clients to calculate the identity Checksum On Download. + +To set when to poll, use the *scheduled_interval* or *scheduled_hour* and *scheduled_minute* +settings. for example:: + + scheduled_interval 30m + +to poll the remote resources every thirty minutes. Alternatively:: + + scheduled_hour 1,13,19 + scheduled_minute 27 + +specifies that poll be run at 1:27, 13:27, and 19:27 each day. + +By default, sr_poll sends its post notification message to the broker with default exchange +(the prefix *xs_* followed by the broker username). The *post_broker* is mandatory. +It can be given incomplete if it is well defined in the credentials.conf file. + +Refer to `sr3_post(1) <../Reference/sr3_post.1.html>`_ - to understand the complete notification process. +Refer to `sr_post(7) <../Reference/sr_post.7.html>`_ - to understand the complete notification format. + +These options set what files the user wants to be notified for and where + it will be placed, and under which name. + +- **path (default: .)** +- **accept [rename=] (must be set)** +- **reject (optional)** +- **permDefault (default: 0o400)** +- **fileAgeMax (default 30d)** + +nodupe_fileAgeMax should be less than nodupe_ttl when using duplicate suppression, +to avoid re-ingesting of files that have aged out of the nodupe cache. + +The option *path* defines where to get the files on the server. +Combined with **accept** / **reject** options, the user can select the +files of interest and their directories of residence. + +The **accept** and **reject** options use regular expressions (regexp) to match URL. +These options are processed sequentially. +The URL of a file that matches a **reject** pattern is not published. +Files matching an **accept** pattern are published. + +The path can have some patterns. These supported patterns concern date/time . +They are fixed... + +**${YYYY} current year** +**${MM} current month** +**${JJJ} current julian** +**${YYYYMMDD} current date** + +**${YYYY-1D} current year - 1 day** +**${MM-1D} current month - 1 day** +**${JJJ-1D} current julian - 1 day** +**${YYYYMMDD-1D} current date - 1 day** + +:: + + ex. path /mylocaldirectory/myradars + path /mylocaldirectory/mygribs + path /mylocaldirectory/${YYYYMMDD}/mydailies + + accept .*RADAR.* + reject .*Reg.* + accept .*GRIB.* + accept .*observations.* + +The **permDefault** option allows users to specify a linux-style numeric octal +permission mask:: + + permDefault 040 + +means that a file will not be posted unless the group has read permission +(on an ls output that looks like: ---r-----, like a chmod 040 command). +The **permDefault** options specifies a mask, that is the permissions must be +at least what is specified. + +As with all other components, the **vip** option can be used to indicate +that a poll should be active on only a single node in a cluster. Note that +other nodes participating in the poll, when they don't have the vip, +will subscribe to the output of the poll to keep their duplicate suppression +caches current. + +files that are more than fileAgeMax are ignored. However, this +can be modified to any specified time limit in the configurations by using +the option *fileAgeMax *. By default in components +other than poll, it is disabled by being set to zero (0). As it is a +duration option, units are in seconds by default, but minutes, hours, +days, and weeks, are available. In the poll component, fileAgeMax +defaults to 30 days. + +Advanced Polling +---------------- + +The built-in Poll lists remote directories and parses the lines returned building +paramiko.SFTPAttributes structures (similar to os.stat) for each file listed. +There is a wide variety of customization available because resources to poll +are so disparate: + +* one can implement a *sarracenia.flowcb* callback with a *poll* routine + to support such services, replacing the default poller. + +* Some servers have non-standard results when listing files, so one can + subclass a sarracenia.flowcb.poll callback with the **on_line** + entry point to normalize their responses and still be able to use the + builtin polling flow. + +* There are many http servers that provide disparately formatted + listings of files, so that sometimes rather than reformatting individual + lines, a means of overriding the parsing of an entire page is needed. + The **on_html_page** entry point in sarracenia.flowcb.poll can be + modified by subclassing as well. + +* There are other servers that provide different services, not covered + by the default poll. One can implement additional *sarracenia.transfer* + classes to add understanding of them to poll. + +The output of a poll is a list of notification messages built from the file names +and SFTPAttributes records, which can then be filtered by elements +after *gather* in the algorithm. + + +COMPONENTS +========== + +All the components do some combination of polling, consuming, and posting. +with variations that accomplish either forwarding of notification messages or +data transfers. The components all apply the same single algorithm, +just starting from different default settings to match common use +cases. + +CPUMP +----- + +*cpump** is an implementation of the `shovel`_ component in C. +On an individual basis, it should be faster than a single python downloader, +with some limitations. + + - doesn't download data, only circulates posts. (shovel, not subscribe) + - runs as only a single instance (no multiple instances). + - does not support any plugins. + - does not support vip for high availability. + - different regular expression library: POSIX vs. python. + - does not support regex for the strip command (no non-greedy regex). + +It can therefore usually, but not always, act as a drop-in replacement for `shovel`_ and `winnow`_ + +The C implementation may be easier to make available in specialized environments, +such as High Performance Computing, as it has far fewer dependencies than the python version. +It also uses far less memory for a given role. Normally the python version +is recommended, but there are some cases where use of the C implementation is sensible. + +**sr_cpump** connects to a *broker* (often the same as the posting broker) +and subscribes to the notifications of interest. If _suppress_duplicates_ is active, +on reception of a post, it looks up the notification message's **integity** field in its cache. If it is +found, the file has already come through, so the notification is ignored. If not, then +the file is new, and the **sum** is added to the cache and the notification is posted. + +FLOW +---- + +Flow is the parent class from which all of the other components except cpost and cpump are built. +Flow has no built-in behaviour. Settings can make it act like any other python component, +or it can be used to build user defined components. Typically used with the *flowMain* option +to run a user defined flow subclass. + + +POLL +---- + +**poll** is a component that connects to a remote server to +check in various directories for some files. When a file is +present, modified or created in the remote directory, the program will +notify about the new product. + +The notification protocol is defined here `sr_post(7) <../Reference/sr_post.7.html>`_ + +**poll** connects to a *broker*. Every *scheduled_interval* seconds (or can used +combination of *scheduled_hour* and *scheduled_minute*) , it connects to +a *pollUrl* (sftp, ftp, ftps). For each of the *directory* defined, it lists +the contents. Polling is only intended to be used for recently modified +files. The *fileAgeMax* option eliminates files that are too old +from consideration. When a file is found that matches a pattern given +by *accept*, **poll** builds a notification message for that product. + +The notification message is then checked in the duplicate cache (time limited by +nodupe_ttl option.) to prevent posting of files which have already +been seen. + +**poll** can be used to acquire remote files in conjunction with an `sarra`_ +subscribed to the posted notifications, to download and repost them from a data pump. + +The pollUrl option specify what is needed to connect to the remote server + +**pollUrl protocol://@[:port]** + +:: + (default: None and it is mandatory to set it ) + +The *pollUrl* should be set with the minimum required information... +**sr_poll** uses *pollUrl* setting not only when polling, but also +in the sr3_post notification messages produced. + +For example, the user can set : + +**pollUrl ftp://myself@myserver** + +And complete the needed information in the credentials file with the line : + +**ftp://myself:mypassword@myserver:2121 passive,binary** + + +Poll gathers information about remote files, to build notification messages about them. +The gather method that is built-in uses sarracenia.transfer protocols, +currently implemented are sftp, ftp, and http. + + + +Repeated Scans and VIP +~~~~~~~~~~~~~~~~~~~~~~ + +When multiple servers are being co-operating to poll a remote server, +the *vip* setting is used to decide which server will actually poll. +All servers participating subscribe to where **poll** is posting, +and use the results to fill in the duplicate suppression cache, so +that if the VIP moves, the alternate servers have current indications +of what was posted. + + + + +POST or WATCH +------------- + +**sr3_post** posts the availability of a file by creating an notification message. +In contrast to most other sarracenia components that act as daemons, +sr3_post is a one shot invocation which posts and exits. +To make files available to subscribers, **sr3_post** sends the notification messages +to an AMQP or MQTT server, also called a broker. + +There are many options for detection changes in directories, for +a detailed discussion of options in Sarracenia, see ``_ + +This manual page is primarily concerned with the python implementation, +but there is also an implementation in C, which works nearly identically. +Differences: + + - plugins are not supported in the C implementation. + - C implementation uses POSIX regular expressions, python3 grammar is slightly different. + - when the *sleep* option ( used only in the C implementation) is set to > 0, + it transforms sr_cpost into a daemon that works like `watch`_. + +The *watch* component is used to monitor directories for new files. +It is equivalent to post (or cpost) with the *sleep* option set to >0. + +The [*-pbu|--post_baseUrl url,url,...*] option specifies the location +subscribers will download the file from. There is usually one post per file. +Format of argument to the *post_baseUrl* option:: + + [ftp|http|sftp]://[user[:password]@]host[:port]/ + or + file: + +When several urls are given as a comma separated list to *post_baseUrl*, the +url´s provided are used round-robin style, to provide a coarse form of load balancing. + +The [*-p|--path path1 path2 .. pathN*] option specifies the path of the files +to be announced. There is usually one post per file. +Format of argument to the *path* option:: + + /absolute_path_to_the/filename + or + relative_path_to_the/filename + +The *-pipe* option can be specified to have sr3_post read path names from standard +input as well. + + +An example invocation of *sr3_post*:: + + sr3_post -pb amqp://broker.com -pbu sftp://stanley@mysftpserver.com/ -p /data/shared/products/foo + +By default, sr3_post reads the file /data/shared/products/foo and calculates its checksum. +It then builds a notification message, logs into broker.com as user 'guest' (default credentials) +and sends the post to defaults vhost '/' and default exchange. The default exchange +is the prefix *xs_* followed by the broker username, hence defaulting to 'xs_guest'. +A subscriber can download the file /data/shared/products/foo by authenticating as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials. +The output of the command is as follows :: + + [INFO] Published xs_guest v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' sum=d,82edc8eb735fd99598a1fe04541f558d parts=1,4574,1,0,0 + +In MetPX-Sarracenia, each post is published under a certain topic. +The log line starts with '[INFO]', followed by the **topic** of the +post. Topics in *AMQP* are fields separated by dot. Note that MQTT topics use +a slash (/) as the topic separator. The complete topic starts with +a topicPrefix (see option), version *v03*, +followed by a subtopic (see option) here the default, the file path separated with dots +*data.shared.products.foo*. + +The second field in the log line is the notification message notice. It consists of a time +stamp *20150813161959.854*, and the source URL of the file in the last 2 fields. + +The rest of the information is stored in AMQP message headers, consisting of key=value pairs. +The *sum=d,82edc8eb735fd99598a1fe04541f558d* header gives file fingerprint (or checksum +) information. Here, *d* means md5 checksum performed on the data, and *82edc8eb735fd99598a1fe04541f558d* +is the checksum value. The *parts=1,4574,1,0,0* state that the file is available in 1 part of 4574 bytes +(the filesize.) The remaining *1,0,0* is not used for transfers of files with only one part. + +Another example:: + + sr3_post -pb amqp://broker.com -pbd /data/web/public_data -pbu http://dd.weather.gc.ca/ -p bulletins/alphanumeric/SACN32_CWAO_123456 + +By default, sr3_post reads the file /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concatenating the post_baseDir and relative path of the source url to obtain the local file path) +and calculates its checksum. It then builds a notification message, logs into broker.com as user 'guest' +(default credentials) and sends the post to defaults vhost '/' and exchange 'xs_guest'. + +A subscriber can download the file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca. + + +File Partitioning +~~~~~~~~~~~~~~~~~ + +use of the *blocksize* option has no effect in sr3. It is used to do file partitioning, +and it will become effective again in the future, with the same semantics. + + +SARRA +----- + +**sarra** is a program that Subscribes to file notifications, +Acquires the files and ReAnnounces them at their new locations. +The notification protocol is defined here `sr_post(7) <../Reference/sr_post.7.html>`_ + +**sarra** connects to a *broker* (often the same as the remote file server +itself) and subscribes to the notifications of interest. It uses the notification +information to download the file on the local server it's running on. +It then posts a notification for the downloaded files on a broker (usually on the local server). + +**sarra** can be used to acquire files from `sr3_post(1) <../Reference/sr3_post.1.html>`_ +or `watch`_ or to reproduce a web-accessible folders (WAF), +that announce its products. + +The **sr_sarra** is an `sr_subscribe(1) <#subscribe>`_ with the following presets:: + + mirror True + + +Specific Consuming Requirements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the notification messages are posted directly from a source, the exchange used is 'xs_'. +To protect against malicious users, administrators should set *sourceFromExchange* to **True**. +Such notification messages may not contain a source nor an origin cluster fields +or a malicious user may set the values incorrectly. + + +- **sourceFromExchange (default: False)** + +Upon reception, the program will set these values in the parent class (here +cluster is the value of option **cluster** taken from default.conf): + +msg['source'] = +msg['from_cluster'] = cluster + +overriding any values present in the notification message. This setting +should always be used when ingesting data from a +user exchange. + + +SENDER +------ + +**sender** is a component derived from `subscribe`_ +used to send local files to a remote server using a file transfer protocol, primarily SFTP. +**sender** is a standard consumer, using all the normal AMQP settings for brokers, exchanges, +queues, and all the standard client side filtering with accept, reject, and after_accept. + +Often, a broker will announce files using a remote protocol such as HTTP, +but for the sender it is actually a local file. In such cases, one will +see a notification message: **ERROR: The file to send is not local.** +An after_accept plugin will convert the web url into a local file one:: + + baseDir /var/httpd/www + flowcb sarracenia.flowcb.tolocalfile.ToLocalFile + +This after_accept plugin is part of the default settings for senders, but one +still needs to specify baseDir for it to function. + +If a **post_broker** is set, **sender** checks if the clustername given +by the **to** option if found in one of the notification message's destination clusters. +If not, the notification message is skipped. + + + +SETUP 1 : PUMP TO PUMP REPLICATION +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For pump replication, **mirror** is set to True (default). + +**baseDir** supplies the directory path that, when combined with the relative +one in the selected notification gives the absolute path of the file to be sent. +The default is None which means that the path in the notification is the absolute one. + +In a subscriber, the baseDir represents the prefix to the relative path on the upstream +server, and is used as a pattern to be replaced in the currently selected base directory +(from a *baseDir* or *directory* option) in the notification message fields: 'fileOp', +which are used when mirroring symbolic links, or files that are renamed. + +The **sendTo** defines the protocol and server to be used to deliver the products. +Its form is a partial url, for example: **ftp://myuser@myhost** +The program uses the file ~/.conf/sarra/credentials.conf to get the remaining details +(password and connection options). Supported protocol are ftp, ftps and sftp. Should the +user need to implement another sending mechanism, he would provide the plugin script +through option **do_send**. + +On the remote site, the **post_baseDir** serves the same purpose as the +**baseDir** on this server. The default is None which means that the delivered path +is the absolute one. + +Now we are ready to send the product... for example, if the selected notification looks like this : + +**20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product** + +**sr_sender** performs the following pseudo-delivery: + +Sends local file [**baseDir**]/relative/path/to/IMPORTANT_product +to **sendTo**/[**post_baseDir**]/relative/path/to/IMPORTANT_product +(**kbytes_ps** is greater than 0, the process attempts to respect +this delivery speed... ftp,ftps,or sftp) + +At this point, a pump-to-pump setup needs to send the remote notification... +(If the post_broker is not set, there will be no posting... just products replication) + +The selected notification contains all the right information +(topic and header attributes) except for url field in the +notice... in our example : **http://this.pump.com/** + +By default, **sr_sender** puts the **sendTo** in that field. +The user can overwrite this by specifying the option **post_baseUrl**. For example: + +**post_baseUrl http://remote.apache.com** + +The user can provide an **on_post** script. Just before the notification message is +published on the **post_broker** and **post_exchange**, the +**on_post** script is called... with the **sr_sender** class instance as an argument. +The script can perform whatever you want... if it returns False, the notification message will not +be published. If True, the program will continue processing from there. + +FIXME: Missing example configuration. + + + +DESTINATION SETUP 2 : METPX-SUNDEW LIKE DISSEMINATION +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this type of usage, we would not usually repost... but if the +**post_broker** and **post_exchange** (**url**,**on_post**) are set, +the product will be announced (with its possibly new location and new name). +Let's reintroduce the options in a different order +with some new ones to ease explanation. + +There are 2 differences with the previous case : +the **directory**, and the **filename** options. + +The **baseDir** is the same, and so are the +**sendTo** and the **post_baseDir** options. + +The **directory** option defines another "relative path" for the product +at its sendTo. It is tagged to the **accept** options defined after it. +If another sequence of **directory**/**accept** follows in the configuration file, +the second directory is tagged to the following accepts and so on. + +The **accept/reject** patterns apply to notification message notice url as above. +Here is an example, here some ordered configuration options : + +:: + + directory /my/new/important_location + + accept .*IMPORTANT.* + + directory /my/new/location/for_others + + accept .* + +If the notification selected is, as above, this : + +**20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product** + +It was selected by the first **accept** option. The remote relative path becomes +**/my/new/important_location** ... and **sr_sender** performs the following pseudo-delivery: + +sends local file [**baseDir**]/relative/path/to/IMPORTANT_product +to **sendTo**/[**post_baseDir**]/my/new/important_location/IMPORTANT_product + + +Usually this way of using **sr_sender** would not require posting of the product. +But if **post_broker** and **post_exchange** are provided, and **url** , as above, is set to +**http://remote.apache.com**, then **sr_sender** would reconstruct : + +Topic: **v03.my.new.important_location.IMPORTANT_product** + +Notice: **20150813161959.854 http://remote.apache.com/ my/new/important_location/IMPORTANT_product** + + + +SHOVEL +------ + +shovel copies notification messages on one broker (given by the *broker* option) to +another (given by the *post_broker* option.) subject to filtering +by (*exchange*, *subtopic*, and optionally, *accept*/*reject*.) + +The *topicPrefix* option must to be set to: + + - to shovel `sr_post(7) <../Reference/sr_post.7.html>`_ notification messages + +shovel is a flow with the following presets:: + + no-download True + suppress_duplicates off + + +SUBSCRIBE +--------- + +Subscribe is the normal downloading flow component, that will connect to a broker, download +the configured files, and then forward the notification messages with an altered baseUrl. + + +WATCH +----- + +Watches a directory and publishes posts when files in the directory change +( added, modified, or deleted). Its arguments are very similar to `sr3_post <../Reference/sr3_post.1.html>`_. +In the MetPX-Sarracenia suite, the main goal is to post the availability and readiness +of one's files. Subscribers use *sr_subscribe* to consume the post and download the files. + +Posts are sent to an AMQP server, also called a broker, specified with the option [ *-pb|--post_broker broker_url* ]. + +The [*-post_baseUrl|--pbu|--url url*] option specifies the protocol, credentials, host and port to which subscribers +will connect to get the file. + +Format of argument to the *url* option:: + + [ftp|http|sftp]://[user[:password]@]host[:port]/ + or + [ftp|http|sftp]://[user[:password]@]host[:port]/ + or + file: + + +The [*-p|--path path*] option tells *watch* what to look for. +If the *path* specifies a directory, *watches* creates a post for any time +a file in that directory is created, modified or deleted. +If the *path* specifies a file, *watch* watches only that file. +In the notification message, it is specified with the *path* of the product. +There is usually one post per file. + +FIXME: in version 3 does it work at all without a configuration? +perhaps we should just create the file if it isn't there? + +An example of an execution of *watch* checking a file:: + + sr3 --post_baseUrl sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo --post_broker amqp://broker.com start watch/myflow + +Here, *watch* checks events on the file /data/shared/products/foo. +Default events settings reports if the file is modified or deleted. +When the file gets modified, *watch* reads the file /data/shared/products/foo +and calculates its checksum. It then builds a notification message, logs into broker.com as user 'guest' (default credentials) +and sends the post to defaults vhost '/' and post_exchange 'xs_stanley' (default exchange) + +A subscriber can download the file /data/shared/products/foo by logging as user stanley +on mysftpserver.com using the sftp protocol to broker.com assuming he has proper credentials. + +The output of the command is as follows :: + + [INFO] v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' + source=guest parts=1,256,1,0,0 sum=d,fc473c7a2801babbd3818260f50859de + +In MetPX-Sarracenia, each post is published under a certain topic. +After the '[INFO]' the next information gives the \fBtopic* of the +post. Topics in *AMQP* are fields separated by dot. In MetPX-Sarracenia +it is made of a *topicPrefix* by default : version *v03* , +followed by the *subtopic* by default : the file path separated with dots, here, *data.shared.products.foo* + +After the topic hierarchy comes the body of the notification. It consists of a time *20150813161959.854* , +and the source url of the file in the last 2 fields. + +The remaining line gives informations that are placed in the amqp message header. +Here it consists of *source=guest* , which is the amqp user, *parts=1,256,0,0,1* , +which suggests to download the file in 1 part of 256 bytes (the actual filesize), trailing 1,0,0 +gives the number of block, the remaining in bytes and the current +block. *sum=d,fc473c7a2801babbd3818260f50859de* mentions checksum information, + +here, *d* means md5 checksum performed on the data, and *fc473c7a2801babbd3818260f50859de* +is the checksum value. When the event on a file is a deletion, sum=R,0 R stands for remove. + +Another example watching a file:: + + sr3 --post_baseDir /data/web/public_data --post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric/SACN32_CWAO_123456 -post_broker amqp://broker.com start watch/myflow + +By default, watch checks the file /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concatenating the baseDir and relative path of the source url to obtain the local file path). +If the file changes, it calculates its checksum. It then builds a notification message, logs into broker.com as user 'guest' +(default credentials) and sends the post to defaults vhost '/' and post_exchange 'sx_guest' (default post_exchange) + +A subscriber can download the file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca. + +An example checking a directory:: + + sr3 -post_baseDir /data/web/public_data -post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric --post_broker amqp://broker.com start watch/myflow + +Here, watch checks for file creation(modification) in /data/web/public_data/bulletins/alphanumeric +(concatenating the baseDir and relative path of the source url to obtain the directory path). +If the file SACN32_CWAO_123456 is being created in that directory, watch calculates its checksum. +It then builds a notification message, logs into broker.com as user 'guest' + +A subscriber can download the created/modified file http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 using http +without authentication on dd.weather.gc.ca. + + + + +WINNOW +------ + +the **winnow** component subscribes to file notification messages and reposts them, suppressing redundant +ones. How to decide which ones are redundant varies by use case. In the most straight-forward case, +the messages have **Identity** header stores a file's fingerprint as described in the `sr_post(7) <../Reference/sr_post.7.html>`_ man page, +and header is used exclusively. There are many other use cases, though. discussed in the following section +on `Duplicate Suppression `_ + +**winnow** has the following options forced:: + + no-download True + suppress_duplicates on + accept_unmatch True + +The suppress_duplicates lifetime can be adjusted, but it is always on. +Other components can use the same duplicate suppression logic. The use of *winnow* components +is purely to make it easier for analysts to understand a configuration at a glance. + + +**winnow** connects to a *broker* (often the same as the posting broker) +and subscribes to the notifications of interest. On reception of a notification, +it looks up its **sum** in its cache. If it is found, the file has already come through, +so the notification is ignored. If not, then the file is new, and the **sum** is added +to the cache and the notification is posted. + +**winnow** can be used to trim notification messages produced by post, `sr3_post <../Reference/sr3_post.1.html>`_, +sr3_cpost, `poll`_ or `watch`_ etc... It is used when there are multiple sources of the same data, so that clients +only download the source data once, from the first source that posted it. + + + + +Configurations +============== + +If one has a ready made configuration called *q_f71.conf*, it can be +added to the list of known ones with:: + + subscribe add q_f71.conf + +In this case, xvan_f14 is included with examples provided, so *add* finds it in the examples +directory and copies into the active configuration one. +Each configuration file manages the consumers for a single queue on +the broker. To view the available configurations, use:: + + $ subscribe list + + configuration examples: ( /usr/lib/python3/dist-packages/sarra/examples/subscribe ) + all.conf all_but_cap.conf amis.conf aqhi.conf cap.conf cclean_f91.conf + cdnld_f21.conf cfile_f44.conf citypage.conf clean_f90.conf cmml.conf cscn22_bulletins.conf + ftp_f70.conf gdps.conf ninjo-a.conf q_f71.conf radar.conf rdps.conf + swob.conf t_f30.conf u_sftp_f60.conf + + user plugins: ( /home/peter/.config/sarra/plugins ) + destfn_am.py destfn_nz.py msg_tarpush.py + + general: ( /home/peter/.config/sarra ) + admin.conf credentials.conf default.conf + + user configurations: ( /home/peter/.config/sarra/subscribe ) + cclean_f91.conf cdnld_f21.conf cfile_f44.conf clean_f90.conf ftp_f70.conf q_f71.conf + t_f30.conf u_sftp_f60.conf + + +one can then modify it using:: + + $ subscribe edit q_f71.conf + +(The edit action uses the EDITOR environment variable, if present.) +Once satisfied, one can start the the configuration running:: + + $ subscibe foreground q_f71.conf + +What goes into the files? See next section: + + +* The forward slash (/) as the path separator in Sarracenia configuration files on all + operating systems. Use of the backslash character as a path separator (as used in the + cmd shell on Windows) may not work properly. When files are read on Windows, the path + separator is immediately converted to the forward slash, so all pattern matching, + in accept, reject, strip etc... directives should use forward slashes when a path + separator is needed. + +Example:: + + directory A + accept X + +Places files matching X in directory A. + +vs:: + accept X + directory A + +Places files matching X in the current working directory, and the *directory A* setting +does nothing in relation to X. + +To provide non-functional descriptions of configurations, or comments, use lines that begin with a **#**. + +**All options are case sensitive.** **Debug** is not the same as **debug** nor **DEBUG**. +Those are three different options (two of which do not exist and will have no effect, +but should generate an ´unknown option warning´). + +Options and command line arguments are equivalent. Every command line argument +has a corresponding long version starting with '--'. For example *-u* has the +long form *--url*. One can also specify this option in a configuration file. +To do so, use the long form without the '--', and put its value separated by a space. +The following are all equivalent: + + - **url ** + - **-u ** + - **--url ** + +Settings are interpreted in order. Each file is read from top to bottom. +For example: + +sequence #1:: + + reject .*\.gif + accept .* + + +sequence #2:: + + accept .* + reject .*\.gif + + +.. note:: + FIXME: does this match only files ending in 'gif' or should we add a $ to it? + will it match something like .gif2 ? is there an assumed .* at the end? + + +In sequence #1, all files ending in 'gif' are rejected. In sequence #2, the +accept .* (which accepts everything) is encountered before the reject statement, +so the reject has no effect. Some options have global scope, rather than being +interpreted in order. for thoses cases, a second declaration overrides the first. + +Options to be reused in different config files can be grouped in an *include* file: + + - **--include ** + +The includeConfigPath would normally reside under the same config dir of its +master configs. If a URL is supplied as an includeConfigPATH, then a remote +configuraiton will be downloaded and cached (used until an update on the server +is detected.) See `Remote Configurations`_ for details. + +Environment variables, and some built-in variables can also be put on the +right hand side to be evaluated, surrounded by ${..} The built-in variables are: + + - ${BROKER_USER} - the user name for authenticating to the broker (e.g. anonymous) + - ${PROGRAM} - the name of the component (subscribe, shovel, etc...) + - ${CONFIG} - the name of the configuration file being run. + - ${HOSTNAME} - the hostname running the client. + - ${RANDID} - a random id that will be consistent within a single invocation. + + +flowCallbacks +============= + +Sarracenia makes extensive use of small python code snippets that customize +processing called *flowCallback* Flow_callbacks define and use additional settings:: + + flowCallback sarracenia.flowcb.log.Log + +There is also a shorter form to express the same thing:: + + callback log + +Either way, the module refers to the sarracenia/flowcb/msg/log.py file in the +installed package. In that file, the Log class is the one searched for entry +points. The log.py file included in the package is like this:: + + from sarracenia.flowcb import FlowCB + import logging + + logger = logging.getLogger( __name__ ) + + class Log(Plugin): + + def after_accept(self,worklist): + for msg in worklist.incoming: + logger.info( "msg/log received: %s " % msg ) + return worklist + +It's a normal python class, declared as a child of the sarracenia.flowcb.FlowCB +class. The methods (function names) in the plugin describe when +those routines will be called. For more details consult the +`Programmer's Guide <../Explanation/SarraPluginDev.rst>`_ + +To add special processing of notification messages, create a module in the python +path, and have it include entry points. + +There is also *flowCallbackPrepend* which adds a flowCallback class to the front +of the list (which determines relative execution order among flowCallback classes.) + + +callback options +---------------- + +callbacks that are delivered with metpx-sr3 follow the following convention: + +* they are placed in the sarracenia/flowcb directory tree. +* the name of the primary class is the same as the name of file containing it. + +so we provide the following syntactic sugar:: + + callback log (is equivalent to *flowCallback sarracenia.flowcb.log.Log* ) + +There is similarly a *callback_prepend* to fill in. + +Importing Extensions +-------------------- + +The *import* option works in a way familiar to Python developers, +Making them available for use by the Sarracenia core, or flowCallback. +Developers can add additional protocols for notification messages or +file transfer. For example:: + + import torr + +would be a reasonable name for a Transfer protocol to retrieve +resources with bittorrent protocol. A skeleton of such a thing +would look like this:: + + + import logging + + logger = logging.getLogger(__name__) + + import sarracenia.transfer + + class torr(sarracenia.transfer.Transfer): + pass + + logger.warning("loading") + +For more details on implementing extensions, consult the +`Programmer's Guide <../Explanation/SarraPluginDev.rst>`_ + +Deprecated v2 plugins +--------------------- + +There is and older (v2) style of plugins as well. That are usually +prefixed with the name of the plugin:: + + msg_to_clusters DDI + msg_to_clusters DD + + on_message msg_to_clusters + +A setting 'msg_to_clusters' is needed by the *msg_to_clusters* plugin +referenced in the *on_message* the v2 plugins are a little more +cumbersome to write. They are included here for completeness, but +people should use version 3 (either flowCallback, or extensions +discussed next) when possible. + +Reasons to use newer style plugins: + +* Support for running v2 plugins is accomplished using a flowcb + called v2wrapper. It performs a lot of processing to wrap up + the v3 data structures to look like v2 ones, and then has + to propagate the changes back. It's a bit expensive. + +* newer style extensions are ordinary python modules, unlike + v2 ones which require minor magic incantations. + +* when a v3 (flowCallback or imported) module has a syntax error, + all the tools of the python interpreter apply, providing + a lot more feedback is given to the coder. with v2, it just + says there is something wrong, much more difficult to debug. + +* v3 api is strictly more powerful than v2, as it works + on groups of notification messages, rather than individual ones. + + + +Environment Variables +--------------------- + +On can also reference environment variables in configuration files, +using the *${ENV}* syntax. If Sarracenia routines needs to make use +of an environment variable, then they can be set in configuration files:: + + declare env HTTP_PROXY=localhost + + +LOGS and MONITORING +------------------- + +- debug + Setting option debug is identical to use **logLevel debug** + +- logMessageDump (default: off) boolean flag + if set, all fields of a notification message are printed, rather than just a url/path reference. + +- logEvents ( default after_accept,after_work,on_housekeeping ) + emit standard log messages at the given points in notification message processing. + other values: on_start, on_stop, post, gather, ... etc... + +- logLevel ( default: info ) + The level of logging as expressed by python's logging. Possible values are : critical, error, info, warning, debug. + +- --logStdout ( default: False ) EXPERIMENTAL FOR DOCKER use case + + The *logStdout* disables log management. Best used on the command line, as there is + some risk of creating stub files before the configurations are completely parsed:: + + sr3 --logStdout start + + All launched processes inherit their file descriptors from the parent. so all output is like an interactive session. + + This is in contrast to the normal case, where each instance takes care of its logs, rotating and purging periodically. + In some cases, one wants to have other software take care of logs, such as in docker, where it is preferable for all + logging to be to standard output. + + It has not been measured, but there is a reasonable likelihood that use of *logStdout* with large configurations (dozens + of configured instances/processes) will cause either corruption of logs, or limit the speed of execution of all processes + writing to stdout. + +- log_reject ( default: False ) + print a log message when *rejecting* notification messages (choosing not to download the corresponding files) + +- log ( default: ~/.cache/sarra/log ) (on Linux) + The directory to store log files in. + +- statehost ( default: False ) + In large data centres, the home directory can be shared among thousands of + nodes. Statehost adds the node name after the cache directory to make it + unique to each node. So each node has it's own statefiles and logs. + example, on a node named goofy, ~/.cache/sarra/log/ becomes ~/.cache/sarra/goofy/log. + +- logRotateCount ( default: 5 ) + Maximum number of logs archived. + +- logRotateInterval [] ( default: 1d = 86,400s ) + The duration of the interval with an optional time unit (ie 5m, 2h, 3d) + +- permLog ( default: 0600 ) + The permission bits to set on log files. + +See the `Subscriber Guide <../How2Guides/subscriber.rst>` for a more detailed discussion of logging +options and techniques. + + + + +CREDENTIALS +----------- + +One normally does not specify passwords in configuration files. Rather they are placed +in the credentials file:: + + edit ~/.config/sr3/credentials.conf + +For every url specified that requires a password, one places +a matching entry in credentials.conf. +The broker option sets all the credential information to connect to the **RabbitMQ** server + +- **broker amqp{s}://:@[:port]/** + +:: + + (default: amqps://anonymous:anonymous@dd.weather.gc.ca/ ) + +For all **sarracenia** programs, the confidential parts of credentials are stored +only in ~/.config/sarra/credentials.conf. This includes the url and broker +passwords and settings needed by components. The format is one entry per line. Examples: + +- **amqp://user1:password1@host/** +- **amqps://user2:password2@host:5671/dev** + +- **amqps://usern:passwd@host/ login_method=PLAIN** + +- **sftp://user5:password5@host** +- **sftp://user6:password6@host:22 ssh_keyfile=/users/local/.ssh/id_dsa** + +- **ftp://user7:password7@host passive,binary** +- **ftp://user8:password8@host:2121 active,ascii** + +- **ftps://user7:De%3Aize@host passive,binary,tls** +- **ftps://user8:%2fdot8@host:2121 active,ascii,tls,prot_p** +- **https://ladsweb.modaps.eosdis.nasa.gov/ bearer_token=89APCBF0-FEBE-11EA-A705-B0QR41911BF4** + + +In other configuration files or on the command line, the url simply lacks the +password or key specification. The url given in the other files is looked +up in credentials.conf. + +Credential Details +~~~~~~~~~~~~~~~~~~ + +You may need to specify additional options for specific credential entries. These details can be added after the end of the URL, with multiple details separated by commas (see examples above). + +Supported details: + +- ``ssh_keyfile=`` - (SFTP) Path to SSH keyfile +- ``passive`` - (FTP) Use passive mode +- ``active`` - (FTP) Use active mode +- ``binary`` - (FTP) Use binary mode +- ``ascii`` - (FTP) Use ASCII mode +- ``ssl`` - (FTP) Use SSL/standard FTP +- ``tls`` - (FTP) Use FTPS with TLS +- ``prot_p`` - (FTPS) Use a secure data connection for TLS connections (otherwise, clear text is used) +- ``bearer_token=`` (or ``bt=``) - (HTTP) Bearer token for authentication +- ``login_method=`` - (AMQP) By default, the login method will be automatically determined. This can be overriden by explicity specifying a login method, which may be required if a broker supports multiple methods and an incorrect one is automatically selected. + +Note:: + SFTP credentials are optional, in that sarracenia will look in the .ssh directory + and use the normal SSH credentials found there. + + These strings are URL encoded, so if an account has a password with a special + character, its URL encoded equivalent can be supplied. In the last example above, + **%2f** means that the actual password isi: **/dot8** + The next to last password is: **De:olonize**. ( %3a being the url encoded value for a colon character. ) + + + + +PERIODIC PROCESSING +=================== + +Most processing occurs on receipt of a notification message, but there is some periodic maintenance +work that happens every *housekeeping* interval (default is 5 minutes.) Evey housekeeping, all of the +configured *on_housekeeping* plugins are run. By default there are three present: + + * log - prints "housekeeping" in the log. + * nodupe - ages out old entries in the reception cache, to minimize its size. + * memory - checks the process memory usage, and restart if too big. + +The log will contain notification messages from all three plugins every housekeeping interval, and +if additional periodic processing is needed, the user can configure addition +plugins to run with the *on_housekeeping* option. + +sanity_log_dead (default: 1.5*housekeeping) +------------------------------------------------------ + +The **sanity_log_dead** option sets how long to consider too long before restarting +a component. + +nodup_ttl (default: off) +------------------------------------- + +The cleanup of expired elements in the duplicate suppression store happens at +each housekeeping. + + +ERROR RECOVERY +============== + +The tools are meant to work well unattended, and so when transient errors occur, they do +their best to recover elegantly. There are timeouts on all operations, and when a failure +is detected, the problem is noted for retry. Errors can happen at many times: + + * Establishing a connection to the broker. + * losing a connection to the broker + * establishing a connection to the file server for a file (for download or upload.) + * losing a connection to the server. + * during data transfer. + +Initially, the programs try to download (or send) a file a fixed number (*attempts*, default: 3) times. +If all three attempts to process the file are unsuccessful, then the file is placed in an instance's +retry file. The program then continues processing of new items. When there are no new items to +process, the program looks for a file to process in the retry queue. It then checks if the file +is so old that it is beyond the *retry_expire* (default: 2 days). If the file is not expired, then +it triggers a new round of attempts at processing the file. If the attempts fail, it goes back +on the retry queue. + +This algorithm ensures that programs do not get stuck on a single bad product that prevents +the rest of the queue from being processed, and allows for reasonable, gradual recovery of +service, allowing fresh data to flow preferentially, and sending old data opportunistically +when there are gaps. + +While fast processing of good data is very desirable, it is important to slow down when errors +start occurring. Often errors are load related, and retrying quickly will just make it worse. +Sarracenia uses exponential back-off in many points to avoid overloading a server when there +are errors. The back-off can accumulate to the point where retries could be separated by a minute +or two. Once the server begins responding normally again, the programs will return to normal +processing speed. + + +EXAMPLES +======== + +Here is a short complete example configuration file:: + + broker amqps://dd.weather.gc.ca/ + + subtopic model_gem_global.25km.grib2.# + accept .* + +This above file will connect to the dd.weather.gc.ca broker, connecting as +anonymous with password anonymous (defaults) to obtain notification messages about +files in the http://dd.weather.gc.ca/model_gem_global/25km/grib2 directory. +All files which arrive in that directory or below it will be downloaded +into the current directory (or just printed to standard output if -n option +was specified.) + +A variety of example configuration files are available here: + + `https://github.com/MetPX/sarracenia/tree/main/sarracenia/examples `_ + + + +QUEUES and MULTIPLE STREAMS +--------------------------- + +When executed, **subscribe** chooses a queue name, which it writes +to a file named after the configuration file given as an argument to **subscribe** +with a .queue suffix ( ."configfile".queue). +If subscribe is stopped, the posted notification messages continue to accumulate on the +broker in the queue. When the program is restarted, it uses the queuename +stored in that file to connect to the same queue, and not lose any notification messages. + +File downloads can be parallelized by running multiple subscribes using +the same queue. The processes will share the queue and each download +part of what has been selected. Simply launch multiple instances +of subscribe in the same user/directory using the same configuration file. + +You can also run several subscribe with different configuration files to +have multiple download streams delivering into the the same directory, +and that download stream can be multi-streamed as well. + +.. Note:: + + While the brokers keep the queues available for some time, Queues take resources on + brokers, and are cleaned up from time to time. A queue which is not accessed for + a long (implementation dependent) period will be destroyed. A queue which is not + accessed and has too many (implementation defined) files queued will be destroyed. + Processes which die should be restarted within a reasonable period of time to avoid + loss of notifications. + + +report and report_exchange +------------------------------- + +For each download, by default, an amqp report message is sent back to the broker. +This is done with option : + +- **report (default: True)** +- **report_exchange (default: xreport|xs_*username* )** + +When a report is generated, it is sent to the configured *report_exchange*. Administrative +components post directly to *xreport*, whereas user components post to their own +exchanges (xs_*username*). The report daemons then copy the notification messages to *xreport* after validation. + +These reports are used for delivery tuning and for data sources to generate statistical information. +Set this option to **False**, to prevent generation of reports. + + +INSTANCES +--------- + +Sometimes one instance of a component and configuration is not enough to process & send all available notifications. + +**instances (default:1)** + +The instance option allows launching several instances of a component and configuration. +When running sender for example, a number of runtime files are created. +In the ~/.cache/sarra/sender/configName directory:: + + A .sender_configname.state is created, containing the number instances. + A .sender_configname_$instance.pid is created, containing the PID of $instance process. + +In directory ~/.cache/sarra/log:: + + A .sender_configname_$instance.log is created as a log of $instance process. + +.. NOTE:: + known bug in the management interface `sr _` means that instance should + always be in the .conf file (not a .inc) and should always be an number + (not a substituted variable or other more complex value. + +.. note:: + FIXME: indicate Windows location also... dot files on Windows? + + +.. Note:: + + While the brokers keep the queues available for some time, Queues take resources on + brokers, and are cleaned up from time to time. A queue which is not + accessed and has too many (implementation defined) files queued will be destroyed. + Processes which die should be restarted within a reasonable period of time to avoid + loss of notifications. A queue which is not accessed for a long (implementation dependent) + period will be destroyed. + + + +vip - ACTIVE/PASSIVE OPTIONS +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**sr3** can be used on a single server node, or multiple nodes +could share responsibility. Some other, separately configured, high availability +software presents a **vip** (virtual ip) on the active server. Should +the server go down, the **vip** is moved on another server. +Both servers would run **sr3**. It is for that reason that the +following options were implemented: + + - **vip (None)** + +When you run only one **sr3** on one server, these options are not set, +and sr3 will run in 'standalone mode'. + +In the case of clustered brokers, you would set the options for the +moving vip. + +**vip 153.14.126.3** + +When **sr3** does not find the vip, it sleeps for 5 seconds and retries. +If it does find the vip, it consumes and processes a notification message and than rechecks for the vip. +if multiple vips are specified, then posession of any of the vips is sufficient +to be declared active. + + +[--blocksize ] (default: 0 (auto)) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This **blocksize** option controls the partitioning strategy used to post files. +The value should be one of:: + + 0 - autocompute an appropriate partitioning strategy (default) + 1 - always send entire files in a single part. + - used a fixed partition size (example size: 1M ) + +Files can be announced as multiple parts. Each part has a separate checksum. +The parts and their checksums are stored in the cache. Partitions can traverse +the network separately, and in parallel. When files change, transfers are +optimized by only sending parts which have changed. + +The *outlet* option allows the final output to be other than a post. +See `sr3_cpump(1) <../Reference/sr3_cpump.1.html>`_ for details. + +[-pbd|--post_baseDir ] (optional) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *post_baseDir* option supplies the directory path that, when combined (or found) +in the given *path*, gives the local absolute path to the data file to be posted. +The *post_baseDir* part of the path will be removed from the posted notification message. +For sftp urls it can be appropriate to specify a path relative to a user account. +Example of that usage would be: -pbd ~user -url sftp:user@host +For file: url's, baseDir is usually not appropriate. To post an absolute path, +omit the -pbd setting, and just specify the complete path as an argument. + +post_baseUrl (MANDATORY) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **post_baseUrl** option sets how to get the file... it defines the protocol, +host, port, and optionally, the user. It is best practice to not include +passwords in urls. + +post_exchange (default: xpublic) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **post_exchange** option set under which exchange the new notification +will be posted. In most cases it is 'xpublic'. + +Whenever a publish happens for a product, a user can set to trigger a script. +The option **on_post** would be used to do such a setup. + +post_exchangeSplit (default: 0) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The **post_exchangeSplit** option appends a two digit suffix resulting from +hashing the last character of the checksum to the post_exchange name, +in order to divide the output amongst a number of exchanges. This is currently used +in high traffic pumps to allow multiple instances of winnow, which cannot be +instanced in the normal way. Example:: + + post_exchangeSplit 5 + post_exchange xwinnow + +will result in posting notification messages to five exchanges named: xwinnow00, xwinnow01, +xwinnow02, xwinnow03 and xwinnow04, where each exchange will receive only one fifth +of the total flow. + +Remote Configurations +--------------------- + +One can specify URI's as configuration files, rather than local files. Example: + + - **--config http://dd.weather.gc.ca/alerts/doc/cap.conf** + +On startup, subscribe checks if the local file cap.conf exists in the +local configuration directory. If it does, then the file will be read to find +a line like so: + + - **--remote_config_url http://dd.weather.gc.ca/alerts/doc/cap.conf** + +In which case, it will check the remote URL and compare the modification time +of the remote file against the local one. The remote file is not newer, or cannot +be reached, then the component will continue with the local file. + +If either the remote file is newer, or there is no local file, it will be downloaded, +and the remote_config_url line will be prepended to it, so that it will continue +to self-update in future. + +Extensions +---------- + +One can override or add functionality with python scripting. + +Sarracenia comes with a variety of example plugins, and uses some to implement base functionality, +such as logging (implemented by default use of msg_log, file_log, post_log plugins):: + + $ sr3 list fcb + Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia ) + flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py flowcb/filter/pclean_f90.py flowcb/filter/pclean_f92.py + flowcb/gather/file.py flowcb/gather/message.py flowcb/line_log.py flowcb/line_mode.py + flowcb/log.py flowcb/nodupe.py flowcb/pclean.py flowcb/post/message.py + flowcb/retry.py flowcb/sample.py flowcb/script.py flowcb/v2wrapper.py + flowcb/work/rxpipe.py + $ + +Users can place their own scripts in the script sub-directory of their config directory +tree ( on Linux, the ~/.config/sarra/plugins). + +flowCallback and flowCallbackPrepend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The flowCallback directive takes a class to load can scan for entry points as an argument:: + + flowCallback sarracenia.flowcb.log.Log + +With this directive in a configuration file, the Log class found in installed package will be used. +That module logs messages *after_accept* (after notification messages have passed through the accept/reject masks.) +and the *after_work* (after the file has been downloaded or sent). Here is the source code +for that callback class:: + + import json + import logging + from sarracenia.flowcb import FlowCB + + logger = logging.getLogger(__name__) + + + class Log(FlowCB): + def __init__(self, options): + + # FIXME: should a logging module have a logLevel setting? + # just put in a cookie cutter for now... + if hasattr(options, 'logLevel'): + logger.setLevel(getattr(logging, options.logLevel.upper())) + else: + logger.setLevel(logging.INFO) + + def after_accept(self, worklist): + for msg in worklist.incoming: + logger.info("accepted: %s " % msg_dumps(msg) ) + + def after_work(self, worklist): + for msg in worklist.ok: + logger.info("worked successfully: %s " % msg.dumps() ) + +If you have multiple callbacks configured, they will be called in the same order they are +configuration file. components in sr3 are often differentiated by the callbacks configured. +For example, a *watch* is a flow with sarracenia.flowcb.gather.file.File class that +is used to scan directories. A Common need when a data source is not easily accessed +with python scripts is to use the script callback:: + + flowCallbackPrepend sarracenia.flowcb.script.Script + + script_gather get_weird_data.sh + +Using the *_prepend* variant of the *flowCallback* option, will have the Script callback +class's entry point, before the File callback... So A script will be executed, and then +the directory will be checked for new files. Here is part of the Script callback class:: + + import logging + from sarracenia.flowcb import FlowCB + import subprocess + + logger = logging.getLogger(__name__) + + + class Script(FlowCB): + + . + . + . + + def run_script( self, script ): + try: + subprocess.run( self.o.script_gather, check=True ) + except Exception as err: + logging.error("subprocess.run failed err={}".format(err)) + logging.debug("Exception details:", exc_info=True) + + + def gather(self ): + if hasattr( self.o, 'script_gather') and self.o.script_gather is not None : + self.run_script( self.o.script_gather ) + return [] + + +Identity +--------- + +One can use the *import* directive to add new checksum algorithms by sub-classing +sarracenia.identity.Identity. + +Transfer +-------- + +One can add support for additional methods of downloading data by sub-classing +sarracenia.transfer.Transfer. + +Transfer protocol scripts should be declared using the **import** option. +Aside the targetted built-in function(s), a module **registered_as** that defines +a list of protocols that these functions provide. Example : + +def registered_as(self) : + return ['ftp','ftps'] + + +See the `Programming Guide <../Explanation/SarraPluginDev.rst>`_ for more information on Extension development. + + + +ROLES - feeder/admin/declare +---------------------------- + +*of interest only to administrators* + +Administrative options are set using:: + + edit ~/.config/sr3/admin.conf + +The *feeder* option specifies the account used by default system transfers for components such as +shovel, sarra and sender (when posting). + +- **feeder amqp{s}://:@[:port]/** + +- **admin (default: None)** + +The admin user is used to do maintenance operations on the pump such as defining +the other users. Most users are defined using the *declare* option. The feeder can also be declared in that +way. + +- **declare (no defaults)** + +subscriber +~~~~~~~~~~ + + A subscriber is user that can only subscribe to data and return report messages. Subscribers are + not permitted to inject data. Each subscriber has an xs_ named exchange on the pump, + where if a user is named *Acme*, the corresponding exchange will be *xs_Acme*. This exchange + is where an subscribe process will send its report messages. + + By convention/default, the *anonymous* user is created on all pumps to permit subscription without + a specific account. + +source +~~~~~~ + + A user permitted to subscribe or originate data. A source does not necessarily represent + one person or type of data, but rather an organization responsible for the data produced. + So if an organization gathers and makes available ten kinds of data with a single contact + email or phone number for questions about the data and its availability, then all of + those collection activities might use a single 'source' account. + + Each source gets a xs_ exchange for injection of data posts, and, similar to a subscriber + to send report messages about processing and receipt of data. Source may also have an xl_ + exchange where, as per report routing configurations, report messages of consumers will be sent. + +feeder +~~~~~~ + + A user permitted to write to any exchange. Sort of an administrative flow user, meant to pump + notification messages when no ordinary source or subscriber is appropriate to do so. Is to be used in + preference to administrator accounts to run flows. + + +User credentials are placed in the credentials files, and *audit* will update +the broker to accept what is specified in that file, as long as the admin password is +already correct. + + +CONFIGURATION FILES +------------------- + +While one can manage configuration files using the *add*, *remove*, +*list*, *edit*, *disable*, and *enable* actions, one can also do all +of the same activities manually by manipulating files in the settings +directory. The configuration files for an sr_subscribe configuration +called *myflow* would be here: + + - linux: ~/.config/sarra/subscribe/myflow.conf (as per: `XDG Open Directory Specication `_ ) + + + - Windows: %AppDir%/science.gc.ca/sarra/myflow.conf , this might be: + C:\Users\peter\AppData\Local\science.gc.ca\sarra\myflow.conf + + - MAC: FIXME. + +The top of the tree has *~/.config/sarra/default.conf* which contains settings that +are read as defaults for any component on start up. In the same directory, *~/.config/sarra/credentials.conf* +contains credentials (passwords) to be used by sarracenia ( `CREDENTIALS`_ for details. ) + +One can also set the XDG_CONFIG_HOME environment variable to override default placement, or +individual configuration files can be placed in any directory and invoked with the +complete path. When components are invoked, the provided file is interpreted as a +file path (with a .conf suffix assumed.) If it is not found as a file path, then the +component will look in the component's config directory ( **config_dir** / **component** ) +for a matching .conf file. + +If it is still not found, it will look for it in the site config dir +(linux: /usr/share/default/sarra/**component**). + +Finally, if the user has set option **remote_config** to True and if he has +configured web sites where configurations can be found (option **remote_config_url**), +The program will try to download the named file from each site until it finds one. +If successful, the file is downloaded to **config_dir/Downloads** and interpreted +by the program from there. There is a similar process for all *plugins* that can +be interpreted and executed within sarracenia components. Components will first +look in the *plugins* directory in the users config tree, then in the site +directory, then in the sarracenia package itself, and finally it will look remotely. + + + +SUNDEW COMPATIBILITY OPTIONS +---------------------------- + +For compatibility with Sundew, there are some additional delivery options which can be specified. + +destfn_script + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

API Documentation

+
+

Config

+

Second version configuration parser

+

FIXME: pas 2023/02/05… missing options from v2: max_queue_size, outlet, pipe

+
+
+class sarracenia.config.Config(parent=None)[source]
+

The option parser to produce a single configuration.

+

it can be instantiated with one of:

+
    +
  • one_config(component, config, action, isPost=False) – read the options for +a given component an configuration, (all in one call.)

  • +
+

On the other hand, a configu can be built up from the following constructors:

+
    +
  • default_config() – returns an empty configuration, given a config file tree.

  • +
  • no_file_config() – returns an empty config without any config file tree.

  • +
+

Then just add settings manually:

+
cfg = no_file_config()
+
+cfg.broker = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+cfg.topicPrefix = [ 'v02', 'post']
+cfg.component = 'subscribe'
+cfg.config = 'flow_demo'
+cfg.action = 'start'
+cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]
+cfg.queueName='q_anonymous.subscriber_test2'
+cfg.download=True
+cfg.batch=1
+cfg.messageCountMax=5
+
+# set the instance number for the flow class.
+cfg.no=0
+
+
+

# and at the end call finalize

+

cfg.finalize()

+
+
+class addBinding(option_strings, dest, nargs=None, const=None, default=None, type=None, choices=None, required=False, help=None, metavar=None)[source]
+

called by argparse to deal with queue bindings.

+
+ +
+
+add_option(option, kind='list', default_value=None, all_values=None)[source]
+

options can be declared in any plugin. There are various kind of options, where the declared type modifies the parsing.

+
    +
  • ‘count’ integer count type.

  • +
  • ‘octal’ base-8 (octal) integer type.

  • +
  • +
    ‘duration’ a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)

    modified by a unit suffix ( m-minute, h-hour, w-week )

    +
    +
    +
  • +
  • ‘flag’ boolean (True/False) option.

  • +
  • ‘float’ a simple floating point number.

  • +
  • +
    ‘list’ a list of string values, each succeeding occurrence catenates to the total.

    all v2 plugin options are declared of type list.

    +
    +
    +
  • +
  • +
    ‘set’ a set of string values, each succeeding occurrence is unioned to the total.

    if all_values is provided, then constrain set to that.

    +
    +
    +
  • +
  • ‘size’ integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.

  • +
  • +
    ‘str’ an arbitrary string value, as will all of the above types, each

    succeeding occurrence overrides the previous one.

    +
    +
    +
  • +
+

If a value is set to None, that could mean that it has not been set.

+
+ +
+
+applyComponentDefaults(component)[source]
+

overlay defaults options for the given component to the given configuration.

+
+ +
+
+dictify()[source]
+

return a dict version of the cfg…

+
+ +
+
+dump()[source]
+

print out what the configuration looks like.

+
+ +
+
+finalize(component=None, config=None)[source]
+

Before final use, take the existing settings, and infer any missing needed defaults from what is provided. +Should be called prior to using a configuration.

+

There are default options that apply only if they are not overridden…

+
+ +
+
+mask_ppstr(mask)[source]
+

return a pretty print string version of the given mask, easier for humans to read.

+
+ +
+
+merge(oth)[source]
+

merge to lists of options.

+

merge two lists of options if one is cumulative then merge, +otherwise if not None, then take value from oth

+
+ +
+
+override(oth)[source]
+

override a value in a set of options.

+

why override() method and not just assign values to the dictionary? +in the configuration file, there are various ways to have variable substituion. +override invokes those, so that they are properly interpreted. Otherwise, +you just end up with a literal value.

+
+ +
+
+parse_args(isPost=False)[source]
+
+
user information:

accept a configuration, apply argParse library to augment the given configuration +with command line settings.

+

the post component has a different calling convention than others, so use that flag +if called from post.

+
+
development notes:

Use argparse.parser to modify defaults. +FIXME, many FIXME notes below. this is a currently unusable placeholder. +have not figured this out yet. many issues.

+

FIXME #1: +parseArgs often sets the value of the variable, regardless of it’s presence (normally a good thing.) +( if you have ‘store_true’ then default needed, for broker, just a string, it ignores if not present.) +This has the effect of overriding settings in the file parsed before the arguments. +Therefore: often supply defaults… but… sigh…

+

but there is another consideration stopping me from supplying defaults, wish I remembered what it was. +I think it is: +FIXME #2: +arguments are parsed twice: once to get basic stuff (loglevel, component, action) +and if the parsing fails there, the usage will print the wrong defaults…

+
+
+
+ +
+
+parse_file(cfg, component=None)[source]
+

add settings from a given config file to self

+
+ +
+
+sundew_dirPattern(pattern, urlstr, basename, destDir)[source]
+

does substitutions for patterns in directories.

+
+ +
+
+variableExpansion(cdir, message=None) str[source]
+
+

replace substitution patterns, variable substitutions as described in +https://metpx.github.io/sarracenia/Reference/sr3_options.7.html#variables

+

returns: the given string with the substiturions done.

+
+
examples: ${YYYYMMDD-70m} becomes 20221107 assuming that was the current date 70 minutes ago.

environment variables, and built-in settings are replaced also.

+
+
+
+

timeoffset -70m

+
+ +
+ +
+
+sarracenia.config.config_path(subdir, config, mandatory=True, ctype='conf')[source]
+

Given a subdir/config look for file in configish places.

+

return Tuple: Found (True/False), path_of_file_found|config_that_was_not_found

+
+ +
+
+sarracenia.config.get_log_filename(hostdir, component, configuration, no)[source]
+

return the name of a single logfile for a single instance.

+
+ +
+
+sarracenia.config.get_metrics_filename(hostdir, component, configuration, no)[source]
+

return the name of a single logfile for a single instance.

+
+ +
+
+sarracenia.config.get_pid_filename(hostdir, component, configuration, no)[source]
+

return the file name for the pid file for the specified instance.

+
+ +
+
+sarracenia.config.get_user_cache_dir(hostdir)[source]
+

hostdir = None if statehost is false,

+
+ +
+
+sarracenia.config.logger = <Logger sarracenia.config (WARNING)>
+

respect appdir stuff using an environment variable. +for not just hard coded as a class variable appdir_stuff

+
+
Type:
+

FIXME

+
+
+
+ +
+
+sarracenia.config.no_file_config()[source]
+

initialize a config that will not use Sarracenia configuration files at all. +meant for use by people writing independent programs to start up instances +with python API calls.

+
+ +
+
+class sarracenia.config.octal_number(value)[source]
+
+ +
+
+sarracenia.config.one_config(component, config, action, isPost=False)[source]
+

single call return a fully parsed single configuration for a single component to run.

+

read in admin.conf and default.conf

+

apply component default overrides ( maps to: component/check ?) +read in component/config.conf +parse arguments from command line. +return config instance item.

+

appdir_stuff can be to override file locations for testing during development.

+
+ +
+
+sarracenia.config.str_options = ['action', 'admin', 'baseDir', 'broker', 'cluster', 'directory', 'exchange', 'exchange_suffix', 'feeder', 'filename', 'flatten', 'flowMain', 'header', 'hostname', 'identity', 'inlineEncoding', 'logLevel', 'pollUrl', 'post_baseUrl', 'post_baseDir', 'post_broker', 'post_exchange', 'post_exchangeSuffix', 'post_format', 'post_topic', 'queueName', 'sendTo', 'rename', 'report_exchange', 'source', 'strip', 'timezone', 'nodupe_ttl', 'nodupe_driver', 'nodupe_basis', 'tlsRigour', 'topic']
+

for backward compatibility,

+

convert some old plugins that are hard to get working with +v2wrapper, into v3 plugin.

+

the fdelay ones makes in depth use of sr_replay function, and +that has changed in v3 too much.

+

accelerators and rate limiting are now built-in, no plugin required.

+
+ +
+
+

Sarracenia

+
+
+class sarracenia.Message[source]
+

A notification message in Sarracenia is stored as a python dictionary, with a few extra management functions.

+

The internal representation is very close to the v03 format defined here: https://metpx.github.io/sarracenia/Reference/sr_post.7.html

+

Unfortunately, sub-classing of dict means that to copy it from a dict will mean losing the type, +and hence the need for the copyDict member.

+
+
+__init__()[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+computeIdentity(path, o, offset=0)[source]
+

check extended attributes for a cached identity sum calculation. +if extended attributes are present, and +* the file mtime is not too new, and +* the cached sum us using the same method +then use the cached value.

+

otherwise, calculate a checksum. +set the file’s extended attributes for the new value. +the method of checksum calculation is from options.identity.

+

sets the message ‘identity’ field if appropriate.

+
+ +
+
+copyDict(d)[source]
+

copy dictionary into message.

+
+ +
+
+dumps() str[source]
+

FIXME: used to be msg_dumps. +print a message in a compact but relatively compact way. +msg is a python dictionary. if there is a field longer than maximum_field_length, +truncate.

+
+ +
+
+static fromFileData(path, o, lstat=None)[source]
+

create a message based on a given file, calculating the checksum. +returns a well-formed message, or none.

+
+ +
+
+static fromFileInfo(path, o, lstat=None)[source]
+

based on the fiven information about the file (it’s name and a stat record if available) +and a configuration options object (sarracenia.config.Config) +return an sarracenia.Message suitable for placement on a worklist.

+

A message is a specialized python dictionary with a certain set of fields in it. +The message returned will have the necessary fields for processing and posting.

+

The message is built for a file is based on the given path, options (o), and lstat (output of os.stat)

+

The lstat record is used to build ‘atime’, ‘mtime’ and ‘mode’ fields if +timeCopy and permCopy options are set.

+

if no lstat record is supplied, then those fields will not be set.

+
+ +
+
+static fromStream(path, o, data=None)[source]
+

Create a file and message for the given path. +The file will be created or overwritten with the provided data. +then invoke fromFileData() for the resulting file.

+
+ +
+
+getContent(options=None)[source]
+

Retrieve the data referred to by a message. The data may be embedded +in the messate, or this routine may resolve a link to an external server +and download the data.

+

does not handle authentication. +This routine is meant to be used with small files. using it to download +large files may be very inefficient. Untested in that use-case.

+

Return value is the data.

+

often on server where one is publishing data, the file is available as +a local file, and one can avoid the network usage by using a options.baseDir setting. +this behaviour can be disabled by not providing the options or not setting baseDir.

+
+ +
+
+setReport(code, text=None)[source]
+

FIXME: used to be msg_set_report +set message fields to indicate result of action so reports can be generated.

+

set is supposed to indicate final message dispositions, so in the case +of putting a message on worklist.failed… no report is generated, since +it will be retried later. FIXME: should we publish an interim failure report?

+
+ +
+
+updatePaths(options, new_dir=None, new_file=None)[source]
+

set the new_* fields in the message based on changed file placement. +if new_* options are ommitted updaste the rest of the fields in +the message based on their current values.

+

If you change file placement in a flow callback, for example. +One would change new_dir and new_file in the message. +This routines updates other fields in the message (e.g. relPath, +baseUrl, topic ) to match new_dir/new_file.

+

msg[‘post_baseUrl’] defaults to msg[‘baseUrl’]

+
+ +
+
+validate()[source]
+

FIXME: used to be msg_validate +return True if message format seems ok, return True, else return False, log some reasons.

+
+ +
+ +
+
+class sarracenia.Sarracenia[source]
+

Core utilities of Sarracenia. The main class here is sarracenia.Message. +a Sarracenia.Message is subclassed from a dict, so for most uses, it works like the +python built-in, but also we have a few major entry points some factoryies:

+

Building a message from a file

+

m = sarracenia.Message.fromFileData( path, options, lstat )

+

builds a notification message from a given existing file, consulting options, a parsed +in memory version of the configuration settings that are applicable

+

Options

+

see the sarracenia.config.Config class for functions to parse configuration files +and create corresponding python option dictionaries. One can supply small +dictionaries for example:

+
options['topicPrefix'] = [ 'v02', 'post' ]
+options['bindings'] = [ ('xpublic', [ 'v02', 'post'] , [ '#' ] )]
+options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_SomethingHelpfulToYou'
+
+
+

Above is an example of a minimal options dictionary taken from the tutorial +example called moth_api_consumer.py. often

+

If you don’t have a file

+

If you don’t have a local file, then build your notification message with:

+

m = sarracenia.Message.fromFileInfo( path, options, lstat )

+

where you can make up the lstat values to fill in some fields in the message. +You can make a fake lstat structure to provide these values using sarracenia.filemetadata +class which is either an alias for paramiko.SFTPAttributes +( https://docs.paramiko.org/en/latest/api/sftp.html#paramiko.sftp_attr.SFTPAttributes ) +if paramiko is installed, or a simple emulation if not.

+

from sarracenia.filemetadata import FmdStat

+

lstat = FmdStat() +lstat.st_mtime= utcinteger second count in UTC (numeric version of a Sarracenia timestamp.) +lstat.st_atime= +lstat.st_mode=0o644 +lstat.st_size= size_in_bytes

+

optional fields that may be of interest: +lstat.filename= “nameOfTheFile” +lstat.longname= ‘lrwxrwxrwx 1 peter peter 20 Oct 11 20:28 nameOfTheFile’

+

that you can then provide as an lstat argument to the above fromFileInfo() +call. However the notification message returned will lack an identity checksum field. +once you get the file, you can add the Identity field with:

+

m.computeIdentity(path, o):

+

In terms of consuming notification messages, the fields in the dictionary provide metadata +for the announced resource. The anounced data could be embedded in the notification message itself, +or available by a URL.

+

Messages are generally gathered from a source such as the Message Queueing Protocol wrapper +class: moth… sarracenia.moth.

+

data = m.getContent()

+

will return the content of the announced resource as raw data.

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+class sarracenia.TimeConversions[source]
+

Time conversion routines.

+
    +
  • os.stat, and time.now() return floating point

  • +
  • The floating point representation is a count of seconds since the beginning of the epoch.

  • +
  • beginning of epoch is platform dependent, and conversion to actual date is fraught (leap seconds, etc…)

  • +
  • Entire SR_* formats are text, no floats are sent over the protocol +(avoids byte order issues, null byte / encoding issues, and enhances readability.)

  • +
  • str format: YYYYMMDDHHMMSS.msec goal of this representation is that a naive +conversion to floats yields comparable numbers.

  • +
  • but the number that results is not useful for anything else, so need these +special routines to get a proper epochal time.

  • +
  • also OK for year 2032 or whatever (rollover of time_t on 32 bits.)

  • +
  • string representation is forced to UTC timezone to avoid having to communicate timezone.

  • +
+

timestr2flt() - accepts a string and returns a float.

+

caveat

+

FIXME: this encoding will break in the year 10000 (assumes four digit year) +and requires leading zeroes prior to 1000. One will have to add detection of +the decimal point, and change the offsets at that point.

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+sarracenia.durationToSeconds(str_value, default=None) float[source]
+

this function converts duration to seconds. +str_value should be a number followed by a unit [s,m,h,d,w] ex. 1w, 4d, 12h +return 0.0 for invalid string.

+
+ +
+
+sarracenia.durationToString(d) str[source]
+

given a numbner of seconds, return a short, human readable string.

+
+ +
+
+sarracenia.stat(path) SFTPAttributes[source]
+

os.stat call replacement which improves on it by returning +and SFTPAttributes structure, in place of the OS stat one, +featuring:

+
    +
  • mtime and ctime with subsecond accuracy

  • +
  • fields that can be overridden (not immutable.)

  • +
+
+ +
+
+sarracenia.timeflt2str(f=None)[source]
+

timeflt2str - accepts a float and returns a string.

+

flow is a floating point number such as returned by time.now() +(number of seconds since beginning of epoch.)

+

the str is YYYYMMDDTHHMMSS.sssss

+

20210921T011331.0123

+

translates to: Sept. 21st, 2021 at 01:13 and 31.0123 seconds. +always UTC timezone.

+
+ +
+
+

Sarracenia.FlowCB

+

Callbacks from running Sarracenia.Flows

+
+
+class sarracenia.flowcb.FlowCB(options, class_logger=None)[source]
+

Bases: object

+

Flow Callback is the main class for implementing plugin customization to flows.

+

sample activation in a configuration file:

+

flowCallback sarracenia.flowcb.name.Name

+

will instantiate an object of that type whose appropriately name methods +will be called at the right time.

+

__init__ accepts options as an argument.

+

options is a sarracenia.config.Config object, used to override default behaviour

+

a setting is declared in a configuration file like so:

+
set sarracenia.flowcb.filter.log.Log.level debug
+
+
+

(the prefix for the setting matches the type hierarchy in flowCallback) +the plugin should get the setting:

+
options.level = 'debug'
+
+
+

worklist given to on_plugins…

+
    +
  • worklist.incoming –> new messages to continue processing

  • +
  • worklist.ok –> successfully processed

  • +
  • worklist.rejected –> messages to not be further processed.

  • +
  • worklist.failed –> messages for which processing failed. Failed messages will be retried.

  • +
  • worklist.directories_ok –> list of directories created during processing.

  • +
+

Initially, all messages are placed in incoming. +if a plugin entry_point decides:

+
    +
  • a message is not relevant, it is moved to the rejected worklist.

  • +
  • all processing has been done, it moves it to the ok worklist

  • +
  • an operation failed and it should be retried later, append it to the failed +worklist

  • +
+

Do not remove any message from all lists, only move messages between them. +it is necessary to put rejected messages in the appropriate worklist +so they can be acknowledged as received. Messages can only removed after ack.

+

def __init__(self,options) -> None:

+
Task: initialization of the flowCallback at instantiation time.
+
+usually contains:
+
+self.o = options
+
+
+

def ack(self,messagelist) -> None:

+
Task: acknowledge messages from a gather source.
+
+
+

def gather(self, messageCountMax) -> (gather_more, messages)

+
Task: gather messages from a source... return a tuple:
+
+      * gather_more ... bool whether to continue gathering
+      * messages ... list of messages
+
+      or just return a list of messages.
+
+      In a poll, gather is always called, regardless of vip posession.
+
+      In all other components, gather is only called when in posession
+      of the vip.
+
+return (True, list)
+ OR
+return list
+
+
+

def after_accept(self,worklist) -> None:

+
Task: just after messages go through accept/reject masks,
+      operate on worklist.incoming to help decide which messages to process further.
+      and move messages to worklist.rejected to prevent further processing.
+      do not delete any messages, only move between worklists.
+
+
+

def after_work(self,worklist) -> None:

+
Task: operate on worklist.ok (files which have arrived.)
+
+All messages on the worklist.ok list have been acknowledged, so to suppress posting
+of them, or futher processing, the messages must be removed from worklist.ok.
+
+worklist.failed processing should occur in here as it will be zeroed out after this step.
+The flowcb/retry.py plugin, for example, processes failed messages.
+
+
+

def destfn(self,msg) -> str:

+
Task: look at the fields in the message, and perhaps settings and
+      return a new file name for the target of the send or download.
+
+kind of a last resort function, exists mostly for sundew compatibility.
+can be used for selective renaming using accept clauses.
+
+
+

def download(self,msg) -> bool:

+
Task: looking at msg['new_dir'], msg['new_file'], msg['new_inflight_file']
+      and the self.o options perform a download of a single file.
+      return True on a successful transfer, False otherwise.
+
+      if self.o.dry_run is set, simulate the output of a download without
+      performing it.
+
+This replaces built-in download functionality, providing an override.
+for individual file transfers. ideally you set checksums as you download.
+
+
+

def metricsReport(self) -> dict:

+
+

Return a dictionary of metrics. Example: number of messages remaining in retry queues.

+
+
+
def on_cleanup(self) -> None::

allow plugins to perform additional work after broker resources are eliminated. +local state files are still present when this runs.

+
+
def on_declare(self) -> None::

local state files are still already present when this runs. +allow plugins to perform additional work besides broker resource setup.

+
+
+

def on_housekeeping(self) -> None:

+
do periodic processing.
+
+
+

def on_start(self) -> None:

+
After the connection is established with the broker and things are instantiated, but
+before any message transfer occurs.
+
+
+

def on_stop(self) -> None:

+
what it says on the tin... clean up processing when stopping.
+
+
+

def poll(self) -> list:

+
Task: gather messages from a destination... return a list of messages.
+      works like a gather, but...
+
+      When specified, poll replaces the built-in poll of the poll component.
+      it runs only when the machine running the poll has the vip.
+      in components other than poll, poll is never called.
+return []
+
+
+

def post(self,worklist) -> None:

+
Task: operate on worklist.ok, and worklist.failed. modifies them appropriately.
+      message acknowledgement has already occurred before they are called.
+
+to indicate failure to process a message, append to worklist.failed.
+worklist.failed processing should occur in here as it will be zeroed out after this step.
+
+
+

def send(self,msg) -> bool:

+
Task: looking at msg['new_dir'], msg['new_file'], and the self.o options perform a transfer
+      of a single file.
+      return True on a successful transfer, False otherwise.
+
+      if self.o.dry_run is set, simulate the output of a send without
+      performing it.
+
+This replaces built-in send functionality for individual files.
+
+
+
+
def please_stop(self):

Pre-warn a flowcb that a stop has been requested, allowing processing to wrap up +before the full stop happens.

+
+
+
+
+__init__(options, class_logger=None)[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+please_stop()[source]
+

flow callbacks should not time.sleep for long periods, but only nap and check +between naps if a stop has been requested.

+
+ +
+ +
+
+sarracenia.flowcb.load_library(factory_path, options)[source]
+

Loading the entry points for a python module. It searches +the normal python module path using the importlib module.

+

the factory_path is a combined file specification with a dot separator +with a special last entry being the name of the class within the file.

+

factory_path a.b.c.C

+

means import the module named a.b.c and instantiate an object of type +C. In that class-C object, look for the known callback entry points.

+

or C might be guessed by the last class in the path not following +python convention by not starting with a capital letter, in which case, +it will just guess.

+

re +note that the ~/.config/sr3/plugins will also be in the python library +path, so modules placed there will be found, in addition to those in the +package itself in the sarracenia/flowcb directory

+
+
callback foo -> foo.Foo

sarracenia.flowcb.foo.Foo

+
+
callback foo.bar -> foo.bar.Bar

sarracenia.flowcb.foo.bar.Bar +foo.bar +sarracenia.flowcb.foo.bar

+
+
+
+ +
+
+

Sarracenia.Moth

+
+
+class sarracenia.moth.Moth(props=None, is_subscriber=True)[source]
+

Bases: object

+
+

Moth … Messages Organized by Topic Headers +(en français: Messages organisés par thème hierarchique. )

+

A multi-protocol library for use by hierarchical message passing implementations, +(messages which have a ‘topic’ header that is used for routing by brokers.)

+
    +
  • regardless of protocol, the message format returned should be the same.

  • +
  • the message is turned into a sarracenia.Message object, which acts like a python +dictionary, corresponding to key-value pairs in the message body, and properties.

  • +
  • topic is a special key that may end up in the message body, or some sort of property +or metadata.

  • +
  • the protocol should support acknowledgement under user control. Such control indicated +by the presence of an entry_point called “ack”. The entry point accepts “ack_id” as +a message identifier to be passed to the broker. Whatever protocol symbol is used +by the protocol, it is passed through this message property. Examples: +in rabbitmq/amqp ack takes a “delivery_tag” as an argument, in MQTT, it takes a “message-id” +so when receiving an AMQP message, the m[‘ack_id’] is assigned the delivery_tag from the message.

  • +
  • There is a special dict item: “_DeleteOnPost”, +to identify keys which are added only for local use. +they will be removed from the message when publishing. +examples: topic (sent outside body), message-id (used for acknowledgements.) +new_basedir, ack_id, new_… (settings…)

  • +
+

Intent is to be specialized for topic based data distribution (MQTT style.) +API to allow pass-through of protocol specific properties, but apply templates for genericity.

+

Target protocols (and corresponding libraries.): AMQP, MQTT, ?

+

Things to specify:

+
    +
  • broker

  • +
  • topicPrefix

  • +
  • subTopic

  • +
  • queueName (for amqp, used as client-id for mqtt)

  • +
+

this library knows nothing about Sarracenia, the only code used from sarracenia is to interpret +duration properties, from the root sarracenia/__init__.py, the broker argument from sarracenia.credentials

+

usage:

+
import sarracenia.moth
+import sarracenia.credentials
+
+
+props = sarracenia.moth.default_options
+props['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+props['expire'] = 300
+props['batch'] = 1
+is_subscriber=True
+
+c= Moth( props, is_subscriber  )
+
+messages = c.newMessages()
+
+# if there are new messages from a publisher, return them, otherwise return
+# an empty list []].
+
+p=Moth( { 'batch':1 }, False )
+
+p.putNewMessage()
+
+p.close()
+# tear down connection.
+
+
+

Initialize a broker connection. Connections are unidirectional. +either for subscribing (with subFactory) or publishing (with pubFactory.)

+

The factories return objects subclassed to match the protocol required +by the broker argument.

+

arguments to the factories are:

+
    +
  • broker … the url of the broker to connect to.

  • +
  • props is a dictionary or properties/parameters.

  • +
  • supplied as overrides to the default properties listed above.

  • +
+

Some may vary among protocols:

+
Protocol     library implementing    URL to select
+--------     --------------------    -------------
+
+AMQPv0.9 --> amqplib from Celery --> amqp, amqps
+
+AMQPv0.9 --> pika                --> pika, pikas
+
+MQTTv3   --> paho                --> mqtt, mqtts
+
+AMQPv1.0 --> qpid-proton         --> amq1, amq1s
+
+
+
+

messaging_strategy

+

how to manage the connection. Covers whether to treat the connection +as new or assume it is set up. Also, If something goes wrong. +What should be done.

+
    +
  • reset: on startup… erase any state, and re-initialize.

  • +
  • stubborn: If set to True, loop forever if something bad happens. +Never give up. This sort of setting is desired in operations, especially unattended. +if set to False, may give up more easily.

  • +
  • failure_duration is to advise library how to structure connection service level.

    +
      +
    • 5m - make a connection that will recover from transient errors of a few minutes, +but not tax the broker too much for prolonged outages.

    • +
    • 5d - duration outage to striving to survive connection for five days.

    • +
    +
  • +
+

Changing recovery_strategy setting, might result in having to destroy and re-create +consumer queues (AMQP.)

+

Options

+

both

+
    +
  • ‘topicPrefix’ : [ ‘v03’ ]

  • +
  • ‘messageDebugDump’: False, –> enable printing of raw messages.

  • +
  • ‘inline’: False, - Are we inlining content within messages?

  • +
  • ‘inlineEncoding’: ‘guess’, - what encoding should we use for inlined content?

  • +
  • ‘inlineByteMax’: 4096, - Maximum size of messages to inline.

  • +
+

for get

+
    +
  • ‘batch’ : 100 # how many messages to get at once

  • +
  • ‘broker’ : an sr_broker ?

  • +
  • ‘queueName’ : Mandatory, name of a queue. (only in AMQP… hmm…)

  • +
  • ‘bindings’ : [ list of bindings ]

  • +
  • ‘loop’

  • +
+

optional:

+
    +
  • ‘message_ttl’

  • +
+

for put:

+
    +
  • ‘exchange’ (only in AMQP… hmm…)

  • +
+
+
+__init__(props=None, is_subscriber=True) None[source]
+

If is_subscriber=True, then this is a consuming instance. +expect calls to get* routines.

+

if is_subscriber=False, then expect/permit only calls to put*

+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+ack(message: Message) None[source]
+

tell broker that a given message has been received.

+

ack uses the ‘ack_id’ property to send an acknowledgement back to the broker.

+
+ +
+
+cleanup() None[source]
+

get rid of server-side resources associated with a client. (queues/id’s, etc…)

+
+ +
+
+property default_options: dict
+

get default properties to override, used by client for validation.

+
+ +
+
+static findAllSubclasses(cls) set[source]
+

Recursively finds all subclasses of a class. __subclasses__() only gives direct subclasses.

+
+ +
+
+getNewMessage() Message[source]
+

If there is one new message available, return it. Otherwise return None. Do not block.

+
+
side effects:

metrics. +self.metrics[‘RxByteCount’] should be incremented by size of payload. +self.metrics[‘RxGoodCount’] should be incremented by 1 if a good message is received. +self.metrics[‘RxBadCount’] should be incremented by 1 if an invalid message is received (&discarded.)

+
+
+
+ +
+
+metricsDisconnect() None[source]
+

tear down an existing connection.

+
+ +
+
+newMessages() list[source]
+

If there are new messages available from the broker, return them, otherwise return None.

+

On Success, this routine returns immediately (non-blocking) with either None, or a list of messages.

+

On failure, this routine blocks, and loops reconnecting to broker, until interaction with broker is successful.

+
+ +
+
+putNewMessage(message: Message, content_type: str = 'application/json', exchange: str | None = None) bool[source]
+

publish a message as set up to the given topic.

+

return True is succeeded, False otherwise.

+
+
side effect

self.metrics[‘TxByteCount’] should be incremented by size of payload. +self.metrics[‘TxGoodCount’] should be incremented by 1 if a good message is received. +self.metrics[‘TxBadCount’] should be incremented by 1 if an invalid message is received (&discarded.)

+
+
+
+ +
+ +
+
+exception sarracenia.transfer.TimeoutException[source]
+

Bases: Exception

+

timeout exception

+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+ +
+
+class sarracenia.transfer.Transfer(proto, options)[source]
+

Bases: object

+

This is a sort of abstract base class for implementing transfer protocols. +Implemented subclasses include support for: local files, https, sftp, and ftp.

+

This class has routines that do i/o given descriptors opened by the sub-classes, +so that each one does not need to re-implement copying, for example.

+

Each subclass needs to implement the following routines:

+

if downloading:

+
get    ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 )
+getAccellerated( msg, remote_file, local_file, length )
+ls     ()
+cd     (dir)
+delete (path)
+
+
+

if sending:

+
put    ( msg, remote_file, local_file, remote_offset=0, local_offset=0, length=0 )
+putAccelerated ( msg, remote_file, local_file, length=0 )
+cd     (dir)
+mkdir  (dir)
+umask  ()
+chmod  (perm)
+rename (old,new)
+
+
+

Note that the ls() call returns are polymorphic. One of:

+
    +
  • a dictionary where the key is the name of the file in the directory, +and the value is an SFTPAttributes structure for if (from paramiko.) +(sftp.py as an example)

  • +
  • a dictionary where the key is the name of the file, and the value is a string +that looks like the output of a linux ls command. +(ftp.py as an example.)

  • +
  • a sequence of bytes… will be parsed as an html page. +(https.py as an example)

  • +
+

The first format is the vastly preferred one. The others are fallbacks when the first +is not available. +The flowcb/poll/__init__.py lsdir() routing will turn ls tries to transform any of +these return values into the first form (a dictionary of SFTPAttributes) +Each SFTPAttributes structure needs st_mode set, and folders need stat.S_IFDIR set.

+

if the lsdir() routine gets a sequence of bytes, the on_html_page() and on_html_parser_init(, +or perhaps handle_starttag(..) and handle_data() routines) will be used to turn them into +the first form.

+

web services with different such formats can be accommodated by subclassing and overriding +the handle_* entry points.

+

uses options (on Sarracenia.config data structure passed to constructor/factory.) +* credentials - used to authentication information. +* sendTo - server to connect to. +* batch - how many files to transfer before a connection is torn down and re-established. +* permDefault - what permissions to set on files transferred. +* permDirDefault - what permission to set on directories created. +* timeout - how long to wait for operations to complete. +* byteRateMax - maximum transfer rate (throttle to avoid exceeding) +* bufsize - size of buffers for file transfers.

+
+
+__init__(proto, options)[source]
+
+ +
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+on_data(chunk) bytes[source]
+

transform data as it is being read. +Given a buffer, return the transformed buffer. +Checksum calculation is based on pre transformation… likely need +a post transformation value as well.

+
+ +
+ +
+
+sarracenia.transfer.alarm_set(time)[source]
+

FIXME: replace with set itimer for > 1 second resolution… currently rouding to nearest second.

+
+ +
+
+class sarracenia.identity.Identity[source]
+

Bases: object

+

A class for algorithms to get a fingerprint for a file being announced. +Appropriate fingerprinting algorithms vary according to file type.

+

required methods in subclasses:

+
+
def registered_as(self):

return a one letter string identifying the algorithm (mostly for v2.) +in v3, the registration comes from the identity sub-class name in lower case.

+
+
def set_path(self,path):

start a checksum for the given path… initialize.

+
+
def update(self,chunk):

update the checksum based on the given bytes from the file (sequential access assumed.)

+
+
+
+
+__weakref__
+

list of weak references to the object (if defined)

+
+ +
+
+update_file(path)[source]
+

read the entire file, check sum it. +this is kind of last resort as it cost an extra file read. +It is better to call update( as the file is being read for other reasons.

+
+ +
+
+property value
+

return the current value of the checksum calculation.

+
+ +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/Addenda_Admin_Rabbit.html b/fr/CommentFaire/Addenda_Admin_Rabbit.html new file mode 100644 index 000000000..4709b679b --- /dev/null +++ b/fr/CommentFaire/Addenda_Admin_Rabbit.html @@ -0,0 +1,536 @@ + + + + + + + Administration de Rabbitmq Adddendum — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Administration de Rabbitmq Adddendum

+

Les anciennes modifications que les gens voulaient garder?

+
+

Introduction

+

AMQP signifie Advanced Message Queuing Protocol. +C’est la définition d’un protocole qui vient de la nécessité de standardiser un système de changement de message asynchrone. +Dans le jargon de l’AMQP, nous parlerons des producteurs de messages, des consommateurs de messages et des courtiers.

+
+
+

installation RABBITMQ-SERVER

+

Sur nos machines qui doivent traiter les messages AMQP, +nous installons le broker, en installant le paquet rabbitmq-server_3.3.5-1_all.deb. +L’installation de base se fait comme suit sur toutes nos machines

+
# installing package taken on the rabbitmq homepage
+# rabbitmq-server version > 3.3.x  required to use ldap for passwords verification only
+
+apt-get install erlang-nox
+dpkg -i /tmp/rabbitmq-server_3.3.5-1_all.deb
+
+# create anonymous user
+# password ********* provided in potato
+#                                          conf write read
+rabbitmqctl add_user anonymous *********
+rabbitmqctl set_permissions -p / anonymous   "^xpublic|^amq.gen.*$|^cmc.*$"     "^amq.gen.*$|^cmc.*$"    "^xpublic|^amq.gen.*$|^cmc.*$"
+rabbitmqctl list_user_permissions anonymous
+
+# create feeder user
+# password ********* provided in potato
+#                                       conf write read
+rabbitmqctl add_user feeder ********
+rabbitmqctl set_permissions -p / feeder  ".*"  ".*"  ".*"
+rabbitmqctl list_user_permissions feeder
+
+# create administrator user
+# password ********* provided in potato
+
+rabbitmqctl add_user root   *********
+rabbitmqctl set_user_tags root administrator
+
+# takeaway administrator privileges from guest
+rabbitmqctl set_user_tags guest
+rabbitmqctl list_user_permissions guest
+rabbitmqctl change_password guest *************
+
+# list users
+rabbitmqctl list_users
+
+
+# enabling management web application
+# this is important since sr_rabbit uses this management facility/port access
+# to retrieve some important info
+
+rabbitmq-plugins enable rabbitmq_management
+/etc/init.d/rabbitmq-server restart
+
+
+
+
+

Installation d’un RABBITMQ-SERVER

+

Sur le bunny, nous avons opté pour une installation en cluster. +Pour ce faire, nous suivons les instructions suivantes:

+
Stop rabbitmq-server on all nodes....
+
+/var/lib/rabbitmq/.erlang.cookie  same on all nodes
+
+on each node restart  /etc/init.d/rabbitmq-server stop/start
+
+on one of the node
+
+rabbitmqctl stop_app
+rabbitmqctl join_cluster rabbit@"other node"
+rabbitmqctl start_app
+rabbitmqctl cluster_status
+
+
+# having high availability queue...
+# here all queues that starts with "cmc." will be highly available on all the cluster nodes
+
+rabbitmqctl set_policy ha-all "^cmc\." '{"ha-mode":"all"}'
+
+
+
+
+

installation ldap RABBITMQ-SERVER

+

Sur les serveurs où nous voulons avoir une authentification en utilisant les instructions suivantes:

+
rabbitmq-plugins enable rabbitmq_auth_backend_ldap
+
+# replace username by ldap username
+# clear password (will be verified through the ldap one)
+rabbitmqctl add_user username aaa
+rabbitmqctl clear_password username
+rabbitmqctl set_permissions -p / username "^xpublic|^amq.gen.*$|^cmc.*$" "^amq.gen.*$|^cmc.*$" "^xpublic|^amq.gen.*$|^cmc.*$"
+
+
+

Et nous configurons les services LDAP dans le fichier de configuration rabbitmq-server +(ancienne configuration de test de ldap-dev qui ne fonctionnait que…):

+
cat /etc/rabbitmq/rabbitmq.config
+[
+{rabbit, [{auth_backends, [ {rabbit_auth_backend_ldap,rabbit_auth_backend_internal}, rabbit_auth_backend_internal]}]},
+{rabbitmq_auth_backend_ldap,
+    [
+    {servers,               ["ldap-dev.cmc.ec.gc.ca"]},
+    {user_dn_pattern,       "uid=${username},ou=People,ou=depot,dc=ec,dc=gc,dc=ca"},
+    {use_ssl,               false},
+    {port,                  389},
+    {log,                   true},
+    {network,               true},
+    {vhost_access_query,    {in_group,
+                            "ou=${vhost}-users,ou=vhosts,dc=ec,dc=gc,dc=ca"}},
+    {resource_access_query,
+    {for, [{permission, configure, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+            {permission, write,
+            {for, [{resource, queue,    {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, exchange, {constant, true}}]}},
+            {permission, read,
+            {for, [{resource, exchange, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, queue,    {constant, true}}]}}
+            ]
+    }},
+    {tag_queries,           [{administrator, {constant, false}},
+                            {management,    {constant, true}}]}
+    ]
+}
+].
+
+
+
+
+

Utilisation de l’AMQP sur DD (DDI, DD.BETA)

+

Nous (Peter) voulions faire une implémentation d’AMQP dans METPX. +Pour ce faire, nous utilisons la bibliothèque python-amqplib qui implémente les fonctionnalités +nécessaires d’AMQP en python. +Nous avons ainsi développé un pxSender de type amqp qui est le producteur de messages de notification +ainsi qu’un pxReceiver de type amqp qui sert de consommateur de messages de notification. +En tant que courtier, nous utilisons rabbitmq-server qui est un paquet Debian standard d’un courtier AMQP.

+

Un pxSender de type amqp, lit le contenu d’un fichier dans sa fil d’attente, +crée un message auquel il joint un “topic” et l’envoie au broker. +Un pxReceiver de type amqp annoncera au broker le “topic” pour lequel il est +intéressé à recevoir des messages de notification, et le broker lui enverra +chaque message correspondant à son choix.

+

Comme un message peut être n’importe quoi, au niveau du pxSender, +nous avons également joint le nom du fichier d’où provient le message. +Ainsi, dans notre pxReceiver, nous pouvons assurer le contenu du message dans le nom de fichier correspondant. +Cette astuce n’est inutile que pour les changements amqp entre un expéditeur et un récepteur amqp…

+
+

Notifications pour DD

+

Nous avons trouvé dans AMQP une opportunité d’annoncer des produits lorsqu’ils arrivent sur DD. +Donc un utilisateur, au lieu de vérifier constamment si un produit est présent sur DD. +Pour le modifier, il pouvait s’abonner (topic AMQP) pour recevoir un message (l’url du produit) +qui ne serait omis qu’à la livraison du produit sur DD. +Nous ne ferions pas cet exercice pour les newsletters… mais pour d’autres produits (grib, images… etc.)

+

Pour mettre cela en œuvre, nous avons utilisé une possibilité de pxSender, le sender_script. +Nous avons écrit un script sftp_amqp.py qui effectue les livraisons à DD et pour chaque produit, il crée un fichier +contenant l’URL sous laquelle le produit sera présent. Voici le début de la configuration de wxo-b1-oper-dd.conf

+
type script
+send_script sftp_amqp.py
+
+# connection info
+protocol    ftp
+host        wxo-b1.cmc.ec.gc.ca
+user        wxofeed
+password    **********
+ftp_mode    active
+
+noduplicates false
+
+# no filename validation (pds format)
+validation  False
+
+# delivery method
+lock  umask
+chmod 775
+batch 100
+
+
+

Nous voyons dans cette configuration que toutes les informations pour un expéditeur à fichier unique sont là. +Mais parce que le type est script… et la send_script sftp_amqp.py est fournie, nous sommes en mesure de +demander à notre expéditeur d’en faire plus…

+

Le fichier contenant l’URL est placé sous le txq d’un expéditeur AMQP +/apps/px/txq/dd-notify-wxo-b1 pour que la notification AMQP soit effectuée. +Pour envoyer les fichiers dans cette fil d’attente, un expéditeur doit avoir +écrit dd-notify-wxo-b1.conf qui est configuré comme suit

+
type amqp
+
+validation False
+noduplicates False
+
+protocol amqp
+host wxo-b1.cmc.ec.gc.ca
+user feeder
+password ********
+
+exchange_name cmc
+exchange_key  exp.dd.notify.${0}
+exchange_type topic
+
+reject ^ensemble.naefs.grib2.raw.*
+
+accept ^(.*)\+\+.*
+
+
+

Encore une fois, le cl du topic contient une partie programmée. +La partie ${0} contient l’arborescence où le produit est placé sur dd… +Par exemple, voici une ligne de journal de dd-notify-wxo-b1.log:

+
2013-06-06 14:47:11,368 [INFO] (86 Bytes) Message radar.24_HR_ACCUM.GIF.XSS++201306061440_XSS_24_HR_ACCUM_MM.gif:URP:XSS:RADAR:GIF::20130606144709  delivered (lat=1.368449,speed=168950.887119)
+
+
+ + + + + + + + + + + + +

Et ainsi serait le cl.

exp.dd.notify.radar.24_HR_ACCUM.GIF.XSS

Et l’emplacement du fichier

http://dd1.weather.gc.ca/radar/24_HR_ACCUM/GIF/XSS

Et l’URL complète dans le message

http://dd1.weather.gc.ca/radar/24_HR_ACCUM/GIF/XSS/201306061440_XSS_24_HR_ACCUM_MM.gif

+
+
+

Utilitaires installés sur les serveurs DD

+

Lorsqu’un client se connecte au broker (rabbitmq-server), il doit créer une file +d’attente et l’attacher à un échange. Nous pouvons donner à cette fil d’attente +l’option qu’elle s’autodétruit lorsqu’elle n’est plus utilisée ou qu’elle est +conservée et continue d’empiler les produits si le client est hors ligne. +En général, nous aimerions que la fil d’attente soit préservée et donc que la +reprise de la connexion redémarre la collection de produits sans perte.

+
+
queue_manager.py

Le rabbitmq-server ne détruira jamais une fil d’attente créée par un client +si elle n’est pas en mode de suppression automatique (et encore moins si elle est créée avec durabilité). +Cela peut causer un problème. Par exemple, un client qui développe un processus, peut changer d’IDE plusieurs +fois et entasser sur le serveur une multitude de files d’attente qui ne seront jamais utilisées. +Nous avons donc créé un script queue_manager.py qui vérifie si les files d’attente inutilisées ont +plus de X produits en attente ou prennent plus de Y Mo… +Si c’est le cas, ils sont détruits par le script.

+

Au moment de la rédaction du présent document, les limites sont les suivantes : 25000 messages and 50Mb.

+
+
dd-xml-inotify.py

Sur notre datamart public, il y a des produits qui ne proviennent pas directement de pds/px/pxatx. +Comme nos notifications sont effectuées à partir de la livraison du produit, nous n’avons pas de +messages de notification pour eux. C’est le cas pour les produits XML sous les répertoires : +citypage_weather and marine_weather. Pour surmonter cette situation, le démon dd-xml-inotify.py +a été créé et installé. Ce script python utilise inotify pour surveiller la modification des produits +sous leurs répertoires. +Si un produit est modifié ou ajouté, une notification amqp est envoyée au serveur. +Ainsi, tous les produits du datamart sont couverts par l’envoi de message.

+
+
+
+
+
+

Utilisation d’AMQP avec URP, BUNNY, PDS-OP

+
+

Note

+

s’applique également au développement…

+
+
+

De URP-1/2 annoncer à BUNNY-OP qu’un produit est prêt

+

Sur urp-1/2 un metpx roule l’expéditeur amqp_expose_db.conf qui annonce qu’un produit +vient d’arriver dans la db de metpx avec un message de la forme

+
Md5sum of product name           file-size  url                        dbname
+a985c32cbdee8af2ab5d7b8f6022e781 498081     http://urp-1.cmc.ec.gc.ca/ db/20150120/RADAR/URP/IWA/201501201810~~PA,60,10,PA_PRECIPET,MM_HR,MM:URP:IWA:RADAR:META::20150120180902
+
+
+

Ces messages AMQP sont envoyés au serveur rabbitmq sur bunny-op avec une clé d’échange qui commence par +v00.urp.input suivie par convention par le chemin de db avec le ‘/’ remplacé par ‘.’.

+
+

Note

+

que urp-1/2 exécute apache et que l’annonce du produit se trouve dans la base de données de +metpx et est visible à partir de l’URL du message.

+
+
+
+

BUNNY-OP et dd_dispatcher.py

+

bunny-op est un vip qui vit sur bunny1-op ou bunny2-op. +C’est avec keepalived que nous nous assurons que ce vip réside sur l’un des bunny-op. +Nous testons également que rabbitmq-server fonctionne sur le même serveur. +La partie configuration de keepalived qui traite de le vip est:

+
vip bunny-op 142.135.12.59 port 5672
+
+vrrp_script chk_rabbitmq {
+        script "killall -0 rabbitmq-server"
+        interval 2
+}
+
+vrrp_instance bunny-op {
+        state BACKUP
+        interface eth0
+        virtual_router_id 247
+        priority 150
+        track_interface {
+                eth0
+        }
+        advert_int 1
+        preempt_delay 5
+        authentication {
+                auth_type PASS
+                auth_pass bunop
+        }
+        virtual_ipaddress {
+# bunny-op
+                142.135.12.59 dev eth0
+        }
+        track_script {
+                chk_rabbitmq
+        }
+}
+
+
+

Les serveurs rabbitmq sur ces machines sont installés dans un cluster. +Nous mettons la haute disponibilité sur les files d’attente en commençant par cmc.*. +Sur chacune des machines, exécutez l’utilitaire dd_dispatcher.py. +Ce programme vérifie si le vip bunny-op et proc dera a son travail uniquement sur le serveur où vit le vip. +(S’il y a un commutateur, détection automatique en 5 secondes et les files d’attente restent inchangées)

+

L’utilitaire dd_dispatcher.py s’abonne aux messages de notification v00.urp.input.# et redirige +ainsi les messages de notification des 2 serveurs opérationnels URP. +À la réception d’un premier produit, le md5dum du produit est placé dans une cache et le message est re-expédié +mais cette fois avec v00.urp.notify comme clé d’échange. +Si un autre message arrive de v00.urp.input avec le même md5sum que le premier, il est ignoré, +de sorte que les produits annoncés à partir de la clé d’échange v00.urp.notify +sont uniques et représentent la première arrivée des 2 URP opérationnels.

+
+
+

Réceptions PDS-OP de messages de notification de répartition, wget de produits radar

+

Sur pds-op, un récepteur pull_urp, exécutez le fx_script pull_amqp_wget.py. +Dans ce script, la commande suivante

+
# shared queue : each pull receive 1 message (prefetch_count=1)
+self.channel.basic_qos(prefetch_size=0,prefetch_count=1,a_global=False)
+
+
+

fait que la distribution des messages de notification v00.urp.notify sera répartie +également sur les 5 serveurs sous pds-op. Nous garantissons donc une traction distribuée. +Pour chaque message du formulaire

+
a985c32cbdee8af2ab5d7b8f6022e781 498081 http://urp-1.cmc.ec.gc.ca/ db/20150120/RADAR/URP/IWA/201501201810~~PA,60,10,PA_PRECIPET,MM_HR,MM:URP:IWA:RADAR:META::20150120180902
+
+
+

l’url est rebuted à partir des 2 derniers champs du message et un wget du produit est fait +et placé dans la fil d’attente du récepteur qui est ensuite ignoré / acheminé de manière ordinaire.

+
+
+

Vérification / Dépannage

+

Dans l’ordre de production

+
    +
  1. +
    Sur urp-1/2:
      +
    • Vérifiez que les produits radar sont générés sur urp-1/2.

    • +
    • Vérifiez que les notifications sont générées sur urp-1/2 /apps/px/log/tx_amqp_expose_db.log

    • +
    +
    +
    +
  2. +
  3. +
    Sur bunny1/2-op
      +
    • Vérifiez où réside bunny-op

    • +
    • Vérifiez les journaux de dd_dispatcher.py /var/log/dd_dispatcher_xxxx.log où xxxx est le processus pid

    • +
    +
    +
    +
  4. +
  5. +
    Sur pds-op
      +
    • Vérifiez le pull_urp

    • +
    +
    +
    +
  6. +
+

La réparation des processus qui ne fonctionnent pas correctement devrait résoudre les problèmes en général. +Plus de détails seront ajoutés ici au fur et à mesure que les problèmes sont rencontrés et corrigés.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/Admin.html b/fr/CommentFaire/Admin.html new file mode 100644 index 000000000..4fe813126 --- /dev/null +++ b/fr/CommentFaire/Admin.html @@ -0,0 +1,1409 @@ + + + + + + + Administration des pompes de données AMQP — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Administration des pompes de données AMQP

+
+

Note

+

FIXME: Les sections manquantes sont mises en évidence par FIXME. Ce qui est ici est exact.

+
+
+

Aperçu

+

Décrit la configuration et le fonctionnement d’une pompe de données MetPX-Sarracenia à l’aide de +Rabbitmq en tant que courtier de protocole de mise en fil d’attente de messages. Pour l’administration, +la plupart des tâches sont différentes, selon le courtier utilisé. Si vous utilisez +un autre courtier, il doit y avoir un autre guide d’administration.

+
+
+

Pré-requis

+

Idéalement, il faut être familier avec l’accès au niveau de l’utilisateur aux pompes existantes comme un +subscriber or a source +avant de passer à l’administration. Ce manuel se veut prescriptif plutôt qu’explicatif. +Pour les raisons pour lesquelles les choses sont construites comme elles sont vues +Concepts.rst

+
+

Exigences minimales

+

Le courtier AMQP est extrêmement léger sur les serveurs d’aujourd’hui. Les +exemples dans ce manuel a été implémenté sur un serveur privé virtuel +commercial (VPS) avec 256 Mo de RAM, et 700 Mo de swap à partir d’un disque +de 20 Go. Un tel une configuration minuscule est capable de suivre une +alimentation presque complète à partir de dd.weather.gc.ca (qui comprend +tous les services météorologiques et les services à l’intention du public). +données environnementales d’Environnement et changements climatiques Canada. +gros fichiers de prédiction numérique (GRIB et plusieurs GRIB dans les +fichiers tar) ont été exclus pour réduire l’utilisation de la bande +passante, mais en termes de performance dans la transmission des messages, +il a assez bien supporter un client.

+

Chaque processus Sarra représente environ 80 Mo de mémoire virtuelle, mais +seulement 3 Mo environ est résident, et vous avez besoin d’en exécuter +suffisamment pour suivre (sur le petit VPS,) donc environ 30 mégaoctets +de RAM réellement utilisés. Le RAM du courtier est ce qui détermine le +nombre de clients qui peuvent être servis. Plus lent les clients ont +besoin de plus de RAM pour leurs files d’attente. Donc, s’occuper des tâches +de courtage et un nettoyage agressif peut réduire l’empreinte mémoire globale. +Le courtier était configuré pour utiliser 128 Mo de RAM dans les exemples de +ce manuel. Le reste de la RAM a été utilisé par les processus apache pour le +moteur de transport web.

+

Bien que ce qui précède soit adéquat pour la preuve de concept, le nombre +de clients qui peuvent être soutenu est assez limitée. 1 Go de RAM pour +tous les sarra relatifs à la sarra les activités devraient être suffisantes +pour de nombreux cas utiles.

+
+
+
+

Opérations

+

Pour faire fonctionner une pompe, un utilisateur doit être désigné comme +administrateur. L’administrateur est différent des autres usagers, surtout en +ce qui concerne l’autorisation accordée de créer des échanges arbitraires, et +la capacité d’exécuter des processus qui envoient des avis à des échanges +communs (xpublic, xreport, etc….) Tous les autres utilisateurs sont limités +à la possibilité de n’accéder qu’à leurs propres ressources (échange et +files d’attente).

+

Le nom d’utilisateur administratif est un choix d’installation, et exactement +comme pour n’importe quel autre utilisateur. Les fichiers de configuration sont +placés sous ~/.config/sarra/, les fichiers de configuration sont placés +sous ~/.config/sarra/, avec l’option par défaut sous admin.conf, et les +configurations pour les composants sous les répertoires nommés d’après chaque +composant. Dans les répertoires des composants, Les fichiers de configuration +ont le suffixe .conf.

+

Les processus administratifs effectuent la validation des écritures à partir +des sources. Une fois ils sont validés, transmettent les messages aux bourses +publiques pour que les abonnés y aient accès. Les processus qui sont +généralement exécutés sur un courtier :

+
    +
  • sr_audit - purger les files d’attente inutiles, créer des échanges et des utilisateurs, définir les permissions des utilisateurs en fonction de leurs rôles.

  • +
  • sr_poll - pour les sources qui ne produisent pas d´avis , retour au sondage +explicite pour l’injection initiale de donneées.

  • +
  • sr_sarra - diverses configurations pour extraire les données d’autres pompes +afin de les rendre disponibles à partir de la pompe locale.

  • +
  • sr_sender - envoyer des données aux clients ou à d’autres pompes qui ne peuvent +pas s´abonner à la pompe locale (généralement à cause des pare-feu).

  • +
  • sr_winnow - lorsqu’il y a plusieurs sources de données redondantes, sélectionner la première à arriver et alimenter sr_sarra.

  • +
  • sr_shovel - copie des avis d’une pompe à une autre, généralement pour alimenter sr_winnow.

  • +
+

Comme pour tout autre utilisateur, il peut y avoir un nombre illimité de configurations. +à mettre en place, et il se peut qu’ils aient tous besoin de courir en même temps. +Pour le faire facilement, on peut invoquer:

+
sr3 start
+
+
+

pour démarrer tous les fichiers avec les configurations nommées de chaque +composant (sarra, subscribe, winnow, log, log, etc….) Il y a deux +utilisateurs/rôles qui doivent être réglés pour utiliser une pompe. Ce sont +les options admin et feeder. Ils sont définis dans ~/.config/sarra/admin.conf +comme suit:

+
feeder amqp://pumpUser@localhost/
+admin  amqps://adminUser@boule.example.com/
+
+
+

Ensuite, les composants du rapport et de l’audit sont également lancés. Il est +la convention d’utiliser une utilisateur feeder (différente utilisateur +administrateur AMQP) pour les movement d´avis et données à l´intérieur +d´une pompe: des tâches de flux de données, telles que l’extraction et la +comptabilisation des données, effectuées par l’utilisateur du serveur. +Normalement, on place les informations d’identification dans +~/.config/sarra/credentials.conf et les tâches ponctuels tel la +création d´un l’échange ou un utilisateurs, sont effectuées par l’administrateur.

+
+

Entretien ménager - sr_audit

+

Lorsqu’un client se connecte à un courtier, il crée une fil d’attente qui est +ensuite liée à une bourse. L’utilisateur peut choisir d’avoir +l’autodestruction du client lorsqu’il est déconnecté (auto-delete), ou il +peut spécifier durable ce qui signifie qu’il doit rester, en attendant que +le client se connecte à nouveau, même si le courtier ou serveur est reparti. +Les clients veulent souvent reprendre là où ils se sont arrêtés, de sorte que +les files d’attente doivent rester.

+

Le courtier rabbitmq ne détruira jamais une fil d’attente qui n’est pas en +auto-delete (ou durable). Ils s’accumuleront au fil du temps, alors sr_audit +périodiquement rechercher les files d’attente inutilisées et les nettoyer. +Actuellement, la valeur par défaut est que toute fil d’attente inutilisée +ayant plus de 25000 messages sera supprimée. On peut changer cette limite +en ayant l’option max_queue_size 50000 dans default.conf.

+
+
+

Excès de fil d’attente/performance

+

Lorsque rabbitmq a des centaines de milliers de messages en fil d’attente, la +performance du courtier peut en souffrir. Un tel accumulation peuvent se +produire lorsque la destination d’un expéditeur est en panne pour une période +prolongée, ou n’est pas disponible pour une raison quelconque. Dans de nombreux +cas, on peut simplement fermer l’expéditeur et supprimer la fil d’attente du +courtier. Bien que cela résout le problème de la performance des courtiers, +l’utilisateur ne recevra pas les avis.

+

D’un autre côté, on peut juste le laisser tranquille, et laisser Sarracenia s’en occuper en utilisant son +files d’attente de relance sur disque. Essentiellement, il stockera les enregistrements liés aux transferts échoués sur le disque, +et réessayez à intervalles raisonnables, sans rester bloqué sur un élément en particulier.

+

Lorsqu’une destination revient en service, les données actuelles ont une priorité plus élevée et elles seront envoyées +réessayez les données, qui sont déjà en retard, uniquement lorsqu’il y a de la place pour le faire dans le flux de données actuel. +( plus de détails, en anglais: https://github.com/MetPX/sarracenia/issues/620 )

+

Si l’on arrive au point où le trafic à travers une fil d’attente est excessif +(plusieurs centaines de messages par seconde à une seule fil d’attente), +surtout s’il y a plusieurs instances partageant la même fil d’attente. +(si plus de 40 instances pour desservir une seule fil d’attente) alors on +peut se heurter à un point où l’ajout d’instances n’améliore pas le débit +global. Par exemple, rabbitmq utilise un seul processeur pour servir une file +d’attente. Dans de tels cas, la création de configurations multiples, +(chacun avec sa propre fil d’attente) diviser le trafic entre eux permettra +d’autres améliorations de débit.

+

sr_winnow est utilisé pour supprimer les doublons. +Notez que le cache de suppression des doublons est local pour chaque instance. +Lorsque N instances +partagent une fil d’attente, la première fois qu’un message est reçu, il +pourrait être choisi par une instance, et si un duplicata est reçu il +serait probablement pris en charge par une autre instance. Pour une suppression +efficace des doublons avec les instances, il faut déployer deux couches +d’abonnés. Utiliser une première couche d’abonnés (sr_shovels) +avec la suppression de doublons désactivée et avec post_exchange_split, qui +route les messages par checksum jusqu’à une +seconde couche de d’abonnées (sr_winnow) dont les caches de suppression de doublons sont actives.

+
+
+
+

Routage

+

L’interconnexion de plusieurs pompes se fait, côté données, par chaînage en guirlande. +sr_sarra et/ou sr_sender d’une pompe à l’autre.

+

les en-têtes to_clusters et source sont utilisés pour les décisions de routage. +implémenté dans les plugins msg_to_clusters, et msg_by_source respectivement. +d’être utilisateur par émetteur ou par composants sarra pour limiter les +transferts de données entre pompes.

+

Pour la gamme d’états, l’en-tête from_cluster est interprété par l’attribut +msg_from_cluster plugin. Les messages de rapport sont définis dans la page +de manuel sr_report(7) Ils sont émis par les +consommateurs à la fin, ainsi que par les feeders comme les les messages +traversent les pompes. Les messages de rapport sont envoyés à l’échange +xs_<user> exchange, et après validation envoyée à l’échange xreport par +des configurations shovel créées par sr_audit.

+

Les messages dans xreports destinés à d’autres clusters sont routés vers des destinations par +pelles configurées manuellement. Voir la section Rapports pour plus de détails.

+
+
+

Que se passe-t-il ?

+

La commande sr_report peut être invoquée pour lier à ‘xreport’ au lieu de +l’échange d’utilisateurs par défaut pour obtenir des informations de +rapport pour l’ensemble d’un courtier.

+

La configuration sr_report avec une action on_message peut être configurée pour +recueillir de l’information statistique.

+
+

Note

+

FIXME:FIXME: la première configuration sr_report en conserve serait speedo….. +speedo : taux total de poteaux/seconde, taux total de logs/seconde. +question : les messages doivent-ils aussi aller dans le journal ? +avant les opérations, nous devons trouver comment Nagios va le surveiller.

+

Est-ce que tout cela est nécessaire, ou est-ce que l’interface utilisateur +graphique du lapin est suffisante ?

+
+
+

Intégration Init

+

Par défaut, lorsque sarracenia est installé, il s’agit d’un outil utilisateur +et non d’une ressource à l’échelle du système. Le répertoire +tools/sous-répertoire permet l’intégration avec des outils pour différents +scénarios d’utilisation.

+
+

Note

+

tools/sr.init – script pour sysv-init où upstart +tools/sarra_system.service – pour systemd et déploiment système +tools/sarra_user.service – pour systemd par usage.

+
+

Processus d’installation du système, par l’administrateur:

+
groupadd sarra
+useradd sarra
+cp tools/sarra_system.service /etc/systemd/system/sarra.service  (if a package installs it, it should go in /usr/lib/systemd/system )
+cp tools/sarra_user.service /etc/systemd/user/sarra.service (or /usr/lib/systemd/user, if installed by a package )
+systemctl daemon-reload
+
+
+

Il est alors supposé que l’on utilise le compte ‘sarra’ pour +stocker la configuration sarra orientée démon (ou à l’échelle du système). +Les utilisateurs peuvent également exécuter leur configuration personnelle +dans les sessions via:

+
systemctl --user enable sarra
+systemctl --user start sarra
+
+
+

Sur un système basé sur upstart ou sysv-init:

+
cp tools/sr.init /etc/init.d/sr
+<insert magic here to get that activated.>
+
+
+
+
+
+

Installation Rabbitmq

+

Exemple d’information sur l´implantation d’un courtier rabbitmq pour Sarracenia. Le +courtier n’est pas tenu de être sur le même hôte que n’importe quoi d’autre, +mais il doit y être accessible à partir d’au moins l’un de ces hôtes +moteurs de transport.

+
+

Installation

+

D’une manière générale, nous voulons rester au-dessus de la version 3.x.

+

https://www.rabbitmq.com/install-debian.html

+

Brièvement:

+
apt-get update
+apt-get install erlang-nox
+apt-get install rabbitmq-server
+
+
+

Dans les distributions à jour, vous pouvez probablement simplement prendre la version de distribution.

+
+
+

WebUI

+

Fondamentalement, à partir d’un shell administrative, il faut:

+
rabbitmq-plugins enable rabbitmq_management
+
+
+

qui activera l’interface web pour le courtier. Pour empêcher l’accès +à la gestion interface pour les indésirables, l’utilisation de +pare-feu, ou l’écoute uniquement de localhost interface pour +la gestion ui est suggérée.

+
+
+

TLS

+

Il faut crypter le trafic des courtiers. L’obtention de certificats +n’entre pas dans le champ d’application de ces instructions, de +sorte qu’il n’est pas discuté en détail. Aux fins de l’exemple, +une méthode consiste à obtenir des certificats à partir +de http://www.letsencrypt.org

+
root@boule:~# git clone https://github.com/letsencrypt/letsencrypt
+Cloning into 'letsencrypt'...
+remote: Counting objects: 33423, done.
+remote: Total 33423 (delta 0), reused 0 (delta 0), pack-reused 33423
+Receiving objects: 100% (33423/33423), 8.80 MiB | 5.74 MiB/s, done.
+Resolving deltas: 100% (23745/23745), done.
+Checking connectivity... done.
+root@boule:~# cd letsencrypt
+root@boule:~/letsencrypt#
+root@boule:~/letsencrypt# ./letsencrypt-auto certonly --standalone -d boule.example.com
+Checking for new version...
+Requesting root privileges to run letsencrypt...
+   /root/.local/share/letsencrypt/bin/letsencrypt certonly --standalone -d boule.example.com
+IMPORTANT NOTES:
+ - Congratulations! Your certificate and chain have been saved at
+   /etc/letsencrypt/live/boule.example.com/fullchain.pem. Your
+   cert will expire on 2016-06-26. To obtain a new version of the
+   certificate in the future, simply run Let's Encrypt again.
+ - If you like Let's Encrypt, please consider supporting our work by:
+
+   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
+   Donating to EFF:                    https://eff.org/donate-le
+
+root@boule:~# ls /etc/letsencrypt/live/boule.example.com/
+cert.pem  chain.pem  fullchain.pem  privkey.pem
+root@boule:~#
+
+
+

Ce processus produit des fichiers clés lisibles uniquement par root. Pour faire les fichiers +lisible par le courtier (qui fonctionne sous le nom d’utilisateur rabbitmq) on aura +pour ajuster les permissions afin de permettre au courtier de lire les fichiers. +probablement que la façon la plus simple de le faire est de les copier ailleurs:

+
root@boule:~# cd /etc/letsencrypt/live/boule*
+root@boule:/etc/letsencrypt/archive# mkdir /etc/rabbitmq/boule.example.com
+root@boule:/etc/letsencrypt/archive# cp -r * /etc/rabbitmq/boule.example.com
+root@boule:~# cd /etc/rabbitmq
+root@boule:~# chown -R rabbitmq.rabbitmq boule*
+
+
+

Maintenant que nous avons la bonne chaîne de certificats, configurez +rabbitmq pour utilisez que le RabbitMQ TLS Support (voir +également RabbitMQ Management):

+
root@boule:~#  cat >/etc/rabbitmq/rabbitmq.config <<EOT
+
+[
+  {rabbit, [
+     {tcp_listeners, [{"127.0.0.1", 5672}]},
+     {ssl_listeners, [5671]},
+     {ssl_options, [{cacertfile,"/etc/rabbitmq/boule.example.com/fullchain.pem"},
+                    {certfile,"/etc/rabbitmq/boule.example.com/cert.pem"},
+                    {keyfile,"/etc/rabbitmq/boule.example.com/privkey.pem"},
+                    {verify,verify_peer},
+                    {fail_if_no_peer_cert,false}]}
+   ]}
+  {rabbitmq_management, [{listener,
+     [{port,     15671},
+           {ssl,      true},
+           {ssl_opts, [{cacertfile,"/etc/rabbitmq/boule.example.com/fullchain.pem"},
+                          {certfile,"/etc/rabbitmq/boule.example.com/cert.pem"},
+                          {keyfile,"/etc/rabbitmq/boule.example.com/privkey.pem"} ]}
+     ]}
+  ]}
+].
+
+EOT
+
+
+

Maintenant, le courtier et l’interface de gestion sont configurés pour +crypter tout le trafic entre le client et le courtier. Un écouteur non crypté +a été configuré pour localhost, où le cryptage sur la machine locale est +inutile, et ajoute la charge du processeur. Mais la direction seulement +a un seul écouteur crypté configuré.

+
+

Note

+

Actuellement, sr_audit sr_audit s’attend à ce que l’interface de gestion +soit sur le port 15671 si elle est cryptée, 15672 sinon. Sarra n’a pas de +réglage de configuration pour lui dire le contraire. Choisir un autre +port brisera sr_audit. FIXME.

+
+
+
+

Modifier les valeurs par défaut

+

Afin d’effectuer des changements de configuration, le courtier doit être en +cours d’exécution. Il faut démarrer le courtier rabbitmq. Sur les systèmes +ubuntu plus anciens, cela serait fait par:

+
service rabbitmq-server start
+
+
+

Sur les nouveaux systèmes avec systemd, la meilleure méthode est:

+
systemctl start rabbitmq-server
+
+
+

Par défaut, l’installation d’un serveur rabbitmq fait de l’utilisateur guest l’administrateur…. avec mot de passe guest. +Avec un serveur rabbitmq en cours d’exécution, on peut maintenant changer cela pour une implémentation opérationnelle….. +Pour annuler l’utilisateur invité, nous suggérons:

+
rabbitmqctl delete_user guest
+
+
+

Un autre administrateur doit être défini…. appelons-le bunnymaster, en fixant le mot de passe à MaestroDelConejito

+
root@boule:~# rabbitmqctl add_user bunnymaster MaestroDelConejito
+Creating user "bunnymaster" ...
+...done.
+root@boule:~#
+
+root@boule:~# rabbitmqctl set_user_tags bunnymaster administrator
+Setting tags for user "bunnymaster" to [administrator] ...
+...done.
+root@boule:~# rabbitmqctl set_permissions bunnymaster ".*" ".*" ".*"
+Setting permissions for user "bunnymaster" in vhost "/" ...
+...done.
+root@boule:~#
+
+
+

Créez un compte linux local sous lequel les tâches administratives de sarra s’exécuteront (disons Sarra). +C’est là que les informations d’identification et la configuration pour les activités au niveau de la pompe seront stockées. +Comme la configuration est maintenue avec cet utilisateur, on s’attend à ce qu’il soit utilisé activement. +par les humains, et devrait donc avoir un environnement de coquille interactif approprié. Un peu d’administration +l’accès est nécessaire, donc l’utilisateur est ajouté au groupe sudo:

+
root@boule:~# useradd -m sarra
+root@boule:~# usermod -a -G sudo sarra
+root@boule:~# mkdir ~sarra/.config
+root@boule:~# mkdir ~sarra/.config/sarra
+
+
+

d’abord besoin d’entrées dans les fichiers credentials.conf et admin.conf:

+
root@boule:~# echo "amqps://bunnymaster:MaestroDelConejito@boule.example.com/" >~sarra/.config/sarra/credentials.conf
+root@boule:~# echo "admin amqps://bunnymaster@boule.example.com/" >~sarra/.config/sarra/admin.conf
+root@boule:~# chown -R sarra.sarra ~sarra/.config
+root@boule:~# passwd sarra
+Enter new UNIX password:
+Retype new UNIX password:
+passwd: password updated successfully
+root@boule:~#
+root@boule:~# chsh -s /bin/bash sarra  # for comfort
+
+
+

l’aide de TLS (aka amqps), la vérification empêche l’utilisation +de localhost même pour l’accès sur la machine locale, le nom d’hôte +pleinement qualifié doit être utilisé. Suivant:

+
root@boule:~#  cd /usr/local/bin
+root@boule:/usr/local/bin# wget https://boule.example.com:15671/cli/rabbitmqadmin
+--2016-03-27 23:13:07--  https://boule.example.com:15671/cli/rabbitmqadmin
+Resolving boule.example.com (boule.example.com)... 192.184.92.216
+Connecting to boule.example.com (boule.example.com)|192.184.92.216|:15671... connected.
+HTTP request sent, awaiting response... 200 OK
+Length: 32406 (32K) [text/plain]
+Saving to: ‘rabbitmqadmin’
+
+rabbitmqadmin              100%[=======================================>]  31.65K  --.-KB/s   in 0.04s
+
+2016-03-27 23:13:07 (863 KB/s) - ‘rabbitmqadmin’ saved [32406/32406]
+
+root@boule:/usr/local/bin#
+root@boule:/usr/local/bin# chmod 755 rabbitmqadmin
+
+
+

Il est nécessaire de télécharger rabbitmqadmin, une commande +d’aide qui est incluse dans RabbitMQ, mais qui n’est pas installée +automatiquement. Il faut le télécharger à partir de l’interface de +gestion, et le placer dans un emplacement raisonnable dans le chemin +d’accès, donc qu’il sera trouvé lorsqu’il est appelé par sr_admin:

+
root@boule:/usr/local/bin# su - sarra
+
+
+

A partir de ce point, la racine n’est généralement pas nécessaire, car toute +la configuration peut être effectuée à partir du compte sarra non privilégié.

+
+

Note

+

Hors de la portée de cette discussion, mais à part les permissions du système de fichiers, +il est pratique de permettre à l’utilisateur de sarra sudo d’accéder à rabbitmqctl. +Grâce à cela, l’ensemble du système peut être administré sans accès administratif au système.

+
+
+
+

Gestion des utilisateurs d’une pompe à l’aide de Sr_audit

+

Pour configurer une pompe, on a besoin d’un utilisateur administratif courtier +(dans les exemples : sarra.). et un utilisateur de feeder (dans les exemples: +feeder.) La gestion des autres utilisateurs se fait à l’aide de le programme +sr_audit.

+

Tout d’abord, écrivez les informations d’identification correctes pour les +utilisateurs admin et feeder dans le fichier le fichier +credentials.config/sarra/credentials.conf

+
amqps://bunnymaster:MaestroDelConejito@boule.example.com/
+amqp://feeder:NoHayPanDuro@localhost/
+amqps://feeder:NoHayPanDuro@boule.example.com/
+amqps://anonymous:anonyomous@boule.example.com/
+amqps://peter:piper@boule.example.com/
+
+
+

Notez que les informations d’identification du serveur sont présentées deux +fois, une fois pour permettre un accès non crypté par l’intermédiaire de +localhost, et une deuxième fois pour permettre l’accès par TLS, potentiellement +à partir d’autres hôtes (nécessaire) lorsqu’un courtier opère dans un cluster, +avec des processus d’alimentation fonctionnant sur plusieurs transports nœuds +du moteur.) L’étape suivante est de mettre les rôles +dans .config/sarra/admin.conf

+
admin  amqps://root@boule.example.com/
+feeder amqp://feeder@localhost/
+
+
+

Spécifiez tous les utilisateurs connus que vous voulez implémenter avec leurs rôles. +dans le fichier .config/sarra/admin.conf

+
declare subscriber anonymous
+declare source peter
+
+
+

Maintenant, pour configurer la pompe, exécutez ce qui suit:

+
sr3 --users declare
+
+
+

Resultat:

+
fractal% sr3 --users declare
+2020-09-06 23:28:56,211 [INFO] sarra.rabbitmq_admin add_user permission user 'ender' role source  configure='^q_ender.*|^xs_ender.*' write='^q_ender.*|^xs_ender.*' read='^q_ender.*|^x[lrs]_ender.*|^x.*public$'
+...
+020-09-06 23:32:50,903 [INFO] root declare looking at cpost/pelle_dd1_f04
+2020-09-06 23:32:50,907 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,908 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan01 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,908 [INFO] root declare looking at cpost/veille_f34
+2020-09-06 23:32:50,912 [INFO] sarra.moth.amqp __putSetup exchange declared: xcpublic (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,912 [INFO] root declare looking at cpost/pelle_dd2_f05
+2020-09-06 23:32:50,916 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+...
+020-09-06 23:32:50,973 [INFO] root declare looking at post/shim_f63
+2020-09-06 23:32:50,973 [INFO] root declare looking at post/test2_f61
+2020-09-06 23:32:50,973 [INFO] root declare looking at report/tsarra_f20
+2020-09-06 23:32:50,978 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_report.tsarra_f20.76069129.80068939 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,978 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_report.tsarra_f20.76069129.80068939 with v02.post.# to xsarra (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,978 [INFO] root declare looking at sarra/download_f20
+2020-09-06 23:32:50,982 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_sarra.download_f20.01191787.94585787 (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,982 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_sarra.download_f20.01191787.94585787 with v03.# to xsarra (as: amqp://tfeed@localhost/)
+2020-09-06 23:32:50,982 [INFO] root declare looking at sender/tsource2send_f50
+2020-09-06 23:32:50,987 [INFO] sarra.moth.amqp __getSetup queue declared q_tsource.sr_sender.tsource2send_f50.60675197.29220410 (as: amqp://tsource@localhost/)
+
+
+

Le programme sr3 :

+
    +
  • utilise le compte admin de .config/sarra/admin.conf pour s’authentifier auprès du courtier.

  • +
  • crée des échanges xpublic et xreport s’ils n’existent pas.

  • +
  • lit les rôles dans .config/sarra/admin.conf.

  • +
  • obtient une liste d’utilisateurs et d’échanges sur la pompe.

  • +
  • pour chaque utilisateur dans une option déclarer:

    +
    declare the user on the broker if missing.
    +set    user permissions corresponding to its role (on creation)
    +create user exchanges   corresponding to its role
    +
    +
    +
  • +
  • les utilisateurs qui n’ont pas de rôle déclaré sont supprimés.

  • +
  • les échanges d’utilisateurs qui ne correspondent pas aux rôles des utilisateurs sont supprimés (‘xl_*,xs_*,xs_*’)

  • +
  • les échanges qui ne commencent pas par “x” (à l’exception de ceux qui sont intégrés) sont supprimés.

  • +
+

On peut inspecter si la commande sr_audit a fait tout ce qu’elle devait faire en utilisant l’interface graphique de gestion. +ou l’outil en ligne de commande:

+
sarra@boule:~$ sudo rabbitmqctl  list_exchanges
+Listing exchanges ...
+      direct
+amq.direct    direct
+amq.fanout    fanout
+amq.headers   headers
+amq.match     headers
+amq.rabbitmq.log      topic
+amq.rabbitmq.trace    topic
+amq.topic     topic
+xl_peter      topic
+xreport       topic
+xpublic       topic
+xs_anonymous  topic
+xs_peter      topic
+...done.
+sarra@boule:~$
+sarra@boule:~$ sudo rabbitmqctl  list_users
+Listing users ...
+anonymous     []
+bunnymaster   [administrator]
+feeder        []
+peter []
+...done.
+sarra@boule:~$ sudo rabbitmqctl  list_permissions
+Listing permissions in vhost "/" ...
+anonymous     ^q_anonymous.*  ^q_anonymous.*|^xs_anonymous$   ^q_anonymous.*|^xpublic$
+bunnymaster   .*      .*      .*
+feeder        .*      .*      .*
+peter ^q_peter.*      ^q_peter.*|^xs_peter$   ^q_peter.*|^xl_peter$|^xpublic$
+...done.
+sarra@boule:~$
+
+
+

De ce qui précède, il semble que sr_audit a fait son travail. +En bref, voici les permissions et les échanges sr_audit gère:

+
admin user        : the only one creating users...
+admin/feeder users: have all permission over queues and exchanges
+
+subscribe user    : can write report messages to exchanged beginning with  xs_<brokerUser>
+                    can read notification messages from exchange xpublic
+                    have all permissions on queue named  q_<brokerUser>*
+
+source user       : can write notification messages to exchanges beginning with xs_<brokerUser>
+                    can read post messages from exchange  xpublic
+                    can read  report messages from exchange  xl_<brokerUser> created for him
+                    have all permissions on queue named   q_<brokerUser>*
+
+
+

Pour ajouter Alice en utilisant sr_audit, on ajouterait ce qui suit à ~/.config/sarra/admin.conf

+
declare source Alice
+
+
+

puis ajoutez une entrée amqp appropriée dans ~/.config/sarra/credentials.conf pour définir le mot de passe, +puis lancez:

+
sr3 --users declare
+
+
+

Pour supprimer des utilisateurs, il suffit de supprimer declare source Alice +du fichier admin.conf, et d’exécuter:

+
# FIXME: functionality not present.
+
+
+

encore. Pour supprimer des utilisateurs, on peut utiliser directement les interfaces de gestion rabbitmq existantes. +La création est automatisée car les modèles de lecture/écriture/configuration sont lourds à faire manuellement.

+
    +
  • Remarque: Par défaut, tous les utilisateurs sont déclarés. Toutefois, des flux peuvent être spécifiés sur +la ligne de commande pour limiter les utilisateurs déclarés à ceux du flux donné. Par exemple,

    +
      +
    • sr3 --users declare déclarera tous les utilisateurs

    • +
    • sr3 --users declare subscribe/dd_amis ne déclarera que les utilisateurs spécifiés dans subscribe/dd_amis

    • +
    +
  • +
+
+
+

Premier abonnement

+

Lors de la configuration d’une pompe, le but est normalement de la connecter à une autre pompe. Pour régler +le paramétrage d’un abonnement nous aide à paramétrer les paramètres pour sarra plus tard. Donc d’abord +essayer un abonnement à une pompe amont:

+
sarra@boule:~$ ls
+sarra@boule:~$ cd ~/.config/sarra/
+sarra@boule:~/.config/sarra$ mkdir subscribe
+sarra@boule:~/.config/sarra$ cd subscribe
+sarra@boule:~/.config/sarra/subscribe$ sr_subscribe edit dd.conf
+broker amqps://anonymous@dd.weather.gc.ca/
+
+mirror True
+directory /var/www/html
+
+# numerical weather model files will overwhelm a small server.
+reject .*/\.tar
+reject .*/model_giops/.*
+reject .*/grib2/.*
+
+accept .*
+
+
+

ajouter le mot de passe de la pompe amont dans credentials.conf

+
sarra@boule:~/.config/sarra$ echo "amqps://anonymous:anonymous@dd.weather.gc.ca/" >>../credentials.conf
+
+
+

puis faites un court passage au premier plan, pour voir si ça marche. Appuyez sur Ctrl-C pour +l’arrêter après quelques messages:

+
sarra@boule:~/.config/sarra$ sr_subscribe foreground dd
+2016-03-28 09:21:27,708 [INFO] sr_subscribe start
+2016-03-28 09:21:27,708 [INFO] sr_subscribe run
+2016-03-28 09:21:27,708 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2016-03-28 09:21:28,375 [INFO] Binding queue q_anonymous.sr_subscribe.dd.78321126.82151209 with key v02.post.# from exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+2016-03-28 09:21:28,933 [INFO] Received notice  20160328130240.645 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml
+2016-03-28 09:21:29,297 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWRM 20160328130240.645 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml 201 boule.example.com anonymous 1128.560235 parts=1,6451,1,0,0 sum=d,f17299b2afd78ae8d894fe85d3236488 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWRM/2016-03-28-1300-CWRM-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:29,389 [INFO] Received notice  20160328130240.646 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml
+2016-03-28 09:21:29,662 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWSK 20160328130240.646 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml 201 boule.example.com anonymous 1128.924688 parts=1,7041,1,0,0 sum=d,8cdc3420109c25910577af888ae6b617 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWSK/2016-03-28-1300-CWSK-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:29,765 [INFO] Received notice  20160328130240.647 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml
+2016-03-28 09:21:30,045 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CWWA 20160328130240.647 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml 201 boule.example.com anonymous 1129.306662 parts=1,7027,1,0,0 sum=d,aabb00e0403ebc9caa57022285ff0e18 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CWWA/2016-03-28-1300-CWWA-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:30,138 [INFO] Received notice  20160328130240.649 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml
+2016-03-28 09:21:30,431 [INFO] 201 Downloaded : v02.report.observations.swob-ml.20160328.CXVG 20160328130240.649 http://dd2.weather.gc.ca/ observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml 201 boule.example.com anonymous 1129.690082 parts=1,7046,1,0,0 sum=d,186fa9627e844a089c79764feda781a7 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM rename=/var/www/html/observations/swob-ml/20160328/CXVG/2016-03-28-1300-CXVG-AUTO-swob.xml message=Downloaded
+2016-03-28 09:21:30,524 [INFO] Received notice  20160328130240.964 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/CA/CWAO/13/CACN00_CWAO_281300__TBO_05037
+^C2016-03-28 09:21:30,692 [INFO] signal stop
+2016-03-28 09:21:30,693 [INFO] sr_subscribe stop
+sarra@boule:~/.config/sarra/subscribe$
+
+
+

La connexion à l’amont est donc fonctionnelle. La connexion au serveur signifie +qu’une fil d’attente est allouée sur le serveur, et il continuera à accumuler +des messages, en attendant que le client se connecte à nouveau. Ce n’était qu’un test +alors on veut que le serveur supprime la fil d’attente:

+
sarra@boule:~/.config/sarra/subscribe$ sr_subscribe cleanup dd
+
+
+

permet maintenant de s’assurer que l’abonnement ne démarre pas automatiquement:

+
sarra@boule:~/.config/sarra/subscribe$ mv dd.conf dd.off
+
+
+

et se tourner vers une application de sarra.

+
+
+

Sarra d’une autre pompe

+

Sarra est utilisé pour permettre à une pompe en aval de ré-annoncer des +produits à partir d’une pompe en amont. Sarra a besoin de toute la +configuration d’un abonnement, mais a aussi besoin de la configuration pour +poster vers le courtier en aval. Le compte d’alimentation du courtier est +utilisé pour ce travail, et est un utilisateur semi-administratif, capable +de publier des avis à n’importe quel échange. Supposons qu’Apache est +configuré (non couvert ici) avec un racine du document /var/www/html. Le +compte linux que nous avons créé pour exécuter tous les processus sr3 est’sarra’. +la racine du document est inscriptible dans ces processus:

+
sarra@boule:~$ cd ~/.config/sarra/sarra
+sarra@boule:~/.config/sarra/sarra$ sudo chown sarra.sarra /var/www/html
+
+
+

Ensuite, nous créons une configuration:

+
sarra@boule:~$ cat >>dd.off <<EOT
+
+broker amqps://anonymous@dd.weather.gc.ca/
+exchange xpublic
+
+msg_to_clusters DD
+on_message msg_to_clusters
+
+mirror False  # usually True, except for this server!
+
+# Numerical Weather Model files will overwhelm a small server.
+reject .*/\.tar
+reject .*/model_giops/.*
+reject .*/grib2/.*
+
+directory /var/www/html
+accept .*
+
+url http://boule.example.com/
+document_root /var/www/html
+post_broker amqps://feeder@boule.example.com/
+
+EOT
+
+
+

Par rapport à l’exemple précédent, Nous avons ajouté :

+
+
exchange xpublic

sarra est souvent utilisé pour les transferts spécialisés, de sorte que l’échangexpublic n’est pas supposé, comme c’est le cas pour les abonnements.

+
+
+

msg_to_clusters DD

+

on_message msg_to_clusters

+
+

sarra implémente le routage par cluster, donc si les données ne sont pas destinées à ce cluster, il sautera (et non pas téléchargera) un produit. +L’inspection de la sortie sr_subscribe ci-dessus révèle que les produits sont destinés à la grappe DD. +pour cela, afin que le téléchargement se fasse.

+
+
+
url et document_root

ces derniers sont nécessaires pour construire les postes locaux qui seront affichés sur le ….

+
+
post_broker

où nous annoncerons à nouveau les fichiers que nous avons téléchargés.

+
+
miroir Faux

Ceci n’est généralement pas nécessaire, quand on copie entre pompes, il est normal de faire des copies directes. +Cependant, la pompe dd.weather.gc.ca est antérieure à la norme du préfixe jour/source. +facilité de nettoyage.

+
+
+

alors essayez-le:

+
sarra@boule:~/.config/sarra/sarra$ sr_sarra foreground dd.off
+2016-03-28 10:38:16,999 [INFO] sr_sarra start
+2016-03-28 10:38:16,999 [INFO] sr_sarra run
+2016-03-28 10:38:17,000 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2016-03-28 10:38:17,604 [INFO] Binding queue q_anonymous.sr_sarra.dd.off with key v02.post.# from exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+2016-03-28 10:38:19,172 [INFO] Received v02.post.bulletins.alphanumeric.20160328.UA.CWAO.14 '20160328143820.166 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422' parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM
+2016-03-28 10:38:19,172 [INFO] downloading/copying into /var/www/html/bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422
+2016-03-28 10:38:19,515 [INFO] 201 Downloaded : v02.report.bulletins.alphanumeric.20160328.UA.CWAO.14 20160328143820.166 http://dd2.weather.gc.ca/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422 201 boule.bsqt.example.com anonymous -0.736602 parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM message=Downloaded
+2016-03-28 10:38:19,517 [INFO] Published: '20160328143820.166 http://boule.bsqt.example.com/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422' parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM
+2016-03-28 10:38:19,602 [INFO] 201 Published : v02.report.bulletins.alphanumeric.20160328.UA.CWAO.14.UANT01_CWAO_281438___22422 20160328143820.166 http://boule.bsqt.example.com/ bulletins/alphanumeric/20160328/UA/CWAO/14/UANT01_CWAO_281438___22422 201 boule.bsqt.example.com anonymous -0.648599 parts=1,124,1,0,0 sum=d,cfbcb85aac0460038babc0c5a8ec0513 from_cluster=DD source=metpx to_clusters=DD,DDI.CMC,DDI.EDM message=Published
+^C2016-03-28 10:38:20,328 [INFO] signal stop
+2016-03-28 10:38:20,328 [INFO] sr_sarra stop
+sarra@boule:~/.config/sarra/sarra$
+
+
+

Le fichier a le suffixe ‘off’ de sorte qu’il ne sera pas invoqué par défaut lorsque +toute la configuration de sarra est démarrée. On peut toujours démarrer le fichier +quand il est dans le réglage off, en spécifiant le chemin (dans ce cas, il est dans +le répertoire courant) donc initialement avoir des fichiers ‘off’ pendant le +débogage des paramètres. Comme la configuration fonctionne correctement, +renommez-la pour qu’elle soit utilisée au démarrage:

+
sarra@boule:~/.config/sarra/sarra$ mv dd.off dd.conf
+sarra@boule:~/.config/sarra/sarra$
+
+
+
+
+

Rapports

+

Maintenant que les données circulent, nous devons jeter un coup d’oeil au +flux des messages de rapport, qui sont essentiellement utilisés par chaque +pompe pour indiquer en amont que les données ont été téléchargées. Sr_audit +aide au routage en créant les configurations suivantes:

+
    +
  • Pour chaque abonné, une configuration de pelle nommée rr_<user>2xreport.conf est créée.

  • +
  • Pour chaque source, une configuration de pelle nommée rr_xreport2<user>user>user.conf est créée.

  • +
+

Les pelles 2xreport s’abonne aux messages postés dans l’échange xs_ de +chaque utilisateur et les poste à l’échange xreport commun. Exemple de fichier +de configuration:

+
# Initial report routing configuration created by sr_audit, tune to taste.
+#     To get original back, just remove this file, and run sr_audit (or wait a few minutes)
+#     To suppress report routing, rename this file to rr_anonymous2xreport.conf.off
+
+broker amqp://tfeed@localhost/
+exchange xs_anonymous
+topicPrefix v02.report
+subtopic #
+accept_unmatch True
+on_message None
+on_post None
+report False
+post_broker amqp://tfeed@localhost/
+post_exchange xreport
+
+
+
+
Explications :
    +
  • Les pelles de routage de rapports sont des fonctions administratives, et c’est donc l’utilisateur de l’alimentateur qui est utilisé.

  • +
  • Cette configuration permet d’acheminer les rapports soumis par l’utilisateur “ anonyme “.

  • +
  • on_message None, on_post None, réduire la journalisation non désirée sur le système local.

  • +
  • report_back Faux réduire les rapports non désirés (les sources veulent-elles comprendre la circulation des pelleteuses ?

  • +
  • poster sur l’échange xreport.

  • +
+
+
+

Les pelles 2<user> regardent tous les messages dans l’échange xreport, et les copient aux utilisateurs xr_ exchange. +Échantillon:

+
# Routage du rapport initial vers la configuration des sources, par sr_audit, réglage au goût.
+# Pour récupérer l'original, supprimez simplement ce fichier, et lancez sr_audit (ou attendez quelques minutes)
+# Pour supprimer le routage des rapports, renommez ce fichier en rr_xreport2tsource2tsource2.conf.off.
+
+
+broker amqp://tfeed@localhost/
+exchange xreport
+topicPrefix v02.report
+subtopic #
+accept_unmatch True
+msg_by_source tsource2
+on_message msg_by_source
+on_post None
+report False
+post_broker amqp://tfeed@localhost/
+post_exchange xr_tsource2
+
+
+
+
Explications :

msg_by_source tsource2 sélectionne que seuls les rapports pour les données +injectées par l’utilisateur tsource2 doivent être sélectionnés. +les rapports sélectionnés doivent être copiés dans l’échange xr_ de +l’utilisateur, où l’utilisateur qui invoque sr_report les trouvera.

+
+
+

Lorsqu’une source invoque le composant sr_report, l’échange par défaut sera +xr_ (eXchange for Reporting). Tous les rapports reçus des abonnés aux données +de cette source seront acheminées vers cet échange.

+

Si un administrateur invoque sr_report, il sera par défaut sur l’échange +xreport, et affichera les rapports de tous les abonnés sur le cluster.

+

Exemple:

+
blacklab% more boulelog.conf
+
+broker amqps://feeder@boule.example.com/
+exchange xreport
+accept .*
+
+blacklab%
+
+blacklab% sr_report foreground boulelog.conf
+2016-03-28 16:29:53,721 [INFO] sr_report start
+2016-03-28 16:29:53,721 [INFO] sr_report run
+2016-03-28 16:29:53,722 [INFO] AMQP  broker(boule.example.com) user(feeder) vhost(/)
+2016-03-28 16:29:54,484 [INFO] Binding queue q_feeder.sr_report.boulelog.06413933.71328785 with key v02.report.# from exchange xreport on broker amqps://feeder@boule.example.com/
+2016-03-28 16:29:55,732 [INFO] Received notice  20160328202955.139 http://boule.example.com/ radar/CAPPI/GIF/XLA/201603282030_XLA_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.040751
+2016-03-28 16:29:56,393 [INFO] Received notice  20160328202956.212 http://boule.example.com/ radar/CAPPI/GIF/XMB/201603282030_XMB_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.159043
+2016-03-28 16:29:56,479 [INFO] Received notice  20160328202956.179 http://boule.example.com/ radar/CAPPI/GIF/XLA/201603282030_XLA_CAPPI_1.0_SNOW.gif 201 blacklab anonymous 0.143819
+2016-03-28 16:29:56,561 [INFO] Received notice  20160328202956.528 http://boule.example.com/ radar/CAPPI/GIF/XMB/201603282030_XMB_CAPPI_1.0_SNOW.gif 201 blacklab anonymous -0.119164
+2016-03-28 16:29:57,557 [INFO] Received notice  20160328202957.405 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01910 201 blacklab anonymous -0.161522
+2016-03-28 16:29:57,642 [INFO] Received notice  20160328202957.406 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01911 201 blacklab anonymous -0.089808
+2016-03-28 16:29:57,729 [INFO] Received notice  20160328202957.408 http://boule.example.com/ bulletins/alphanumeric/20160328/SN/CWVR/20/SNVD17_CWVR_282000___01912 201 blacklab anonymous -0.043441
+2016-03-28 16:29:58,723 [INFO] Received notice  20160328202958.471 http://boule.example.com/ radar/CAPPI/GIF/WKR/201603282030_WKR_CAPPI_1.5_RAIN.gif 201 blacklab anonymous -0.131236
+2016-03-28 16:29:59,400 [INFO] signal stop
+2016-03-28 16:29:59,400 [INFO] sr_report stop
+blacklab%
+
+
+

on peut voir qu’un abonné sur blacklab télécharge activement depuis la +nouvelle pompe sur boule. Fondamentalement, les deux sortes de pelles +construites automatiquement par sr_audit feront tout le routage nécessaire +au sein d’un cluster. Lorsqu’il y a des problèmes de volume, ces configurations +peuvent être modifiées pour augmenter le nombre d’instances ou l’utilisation. +post_exchange_split le cas échéant.

+

La configuration manuelle de la pelle est également nécessaire pour acheminer +les messages entre les groupes. C’est juste une variation de routage des +rapports intra-cluster.

+
+
+

Sarra D’une source

+

Lorsque l’on lit les messages directement depuis une source, il faut activer +la validation. FIXME : exemple de la façon dont les messages des utilisateurs +sont traités.

+
+
    +
  • set source_from_exchange - set source_from_exchange

  • +
  • set mirror False to get date/source tree prepended

  • +
  • valider que la somme de contrôle fonctionne……

  • +
+
+

autre chose ?

+
+
+

Nettoyage

+

Ce sont des exemples, la mise en œuvre du nettoyage n’est pas couverte par +Sarracenia. Étant donné qu’un arbre raisonnablement petit comme donné +ci-dessus, il peut être pratique de scanner l’arbre et d’élaguer les anciens +fichiers à partir de celui-ci. Un travail de cron comme ça:

+
root@boule:/etc/cron.d# more sarra_clean
+# remove files one hour after they show up.
+# for weather production, 37 minutes passed the hour is a good time.
+# remove directories the day after the last time they were touched.
+37 4 * * *  root find /var/www/html -mindepth 1 -maxdepth 1 -type d -mtime +0  | xargs rm -rf
+
+
+

Cela peut sembler un peu agressif, mais ce fichier se trouvait sur un +très petit serveur virtuel qui n’était qu’un petit serveur virtuel. +pour le transfert de données en temps réel afin de conserver les données +pendant des périodes prolongées a rempli le disque et arrêté tous les +transferts. Dans les transferts à grande échelle, il y a toujours un échange +entre l’aspect pratique de conserver les données pour toujours et le besoin +de performance ce qui nous oblige à tailler régulièrement les arborescences +de répertoires. Les performances du système de fichiers sont optimales avec +les arbres de taille raisonnable, et quand les arbres deviennent trop grands, +le processus de “find” pour les traverser peut deviennent trop onéreux.

+

On peut plus facilement maintenir de plus petits arbres de répertoires en +les faisant rouler régulièrement. Si vous avoir assez d’espace disque pour +durer un ou plusieurs jours, puis une seule tâche cron logique qui +fonctionnerait sur les arbres quotidiens sans encourir la pénalité d’une +découverte, est une bonne approche.

+

Remplacer le contenu ci-dessus par:

+
34 4 * * * root find /var/www/html -mindepth 1 -maxdepth 1  -type d -regex '/var/www/html/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' -mtime +1 | xargs rm -rf
+
+
+

où le +1 peut être remplacé par le nombre de jours à conserver. (…) (….) +aurait préféré [0-9]{8}, mais il semblerait que la syntaxe de find +regex n’inclut pas les répétitions. )

+

Il est à noter que les logs se nettoieront par eux-mêmes, par défaut, après 5 rotations le log +le plus ancien sera enlevé à minuit, seulement si la configuration par défaut a été utilisée +depuis la première rotation. Il est possible de racourcir ce nombre en ajoutant logRotateCount 3 +à default.conf.

+
+
+

S’assurer que les choses sont en place

+

Les processus peuvent planter. On peut avoir un redémarrage automatisé +en exécutant sr3 sanity périodiquement:

+
root@boule:/etc/cron.d# more sanity
+# remove files one hour after they show up.
+# for weather production, 37 minutes passed the hour is a good time.
+# remove directories the day after the last time they were touched.
+7,14,21,28,35,42,49,56 * * * sr3 sanity
+
+
+
+
+

Démarrage

+

Le paquet Debian installe une unité systemd, mais l’installation de python3 ne s’en occupe pas.

+
+
+

Sr_Poll

+

FIXME: alimenter la sarra à partir de la source configurée avec un sr_poll. configuré.

+
+
+

Sr_winnow

+

FIXME: exemple de configuration sr_winnow expliqué, avec quelques pelles aussi.

+
+
+

Sr_sender

+

Lorsque les pare-feu empêchent l’utilisation de sarra pour tirer d’une pompe comme le ferait un abonné, on peut inverser l’alimentation en ayant la commande +la pompe amont alimente explicitement la pompe aval.

+

FIXME : configuration élaborée de l’échantillon sr_sender.

+
+
+

Ajout manuel d’utilisateurs

+

Pour éviter l’utilisation de sr_admin, ou pour contourner les problèmes, +on peut ajuster les paramètres utilisateur manuellement:

+
cd /usr/local/bin
+wget -q https://boule.example.com:15671/cli/rabbitmqadmin
+chmod 755 rabbitmqadmin
+
+rabbitmqctl add_user Alice <password>
+rabbitmqctl set_permissions -p / Alice   "^q_Alice.*$" "^q_Alice.*$|^xs_Alice$" "^q_Alice.*$|^xl_Alice$|^xpublic$"
+
+rabbitmqadmin -u root -p ***** declare exchange name=xs_Alice type=topic auto_delete=false durable=true
+rabbitmqadmin -u root -p ***** declare exchange name=xl_Alice type=topic auto_delete=false durable=true
+
+
+

ou paramétré:

+
u=Alice
+rabbitmqctl add_user ${u} <password>
+rabbitmqctl set_permissions -p / ${u} "^q_${u}.$" "^q_${u}.*$|^xs_${u}$" "^q_${u}.*$|^xl_${u}$|^xpublic$"
+
+rabbitmqadmin -u root -p ***** declare exchange name=xs_${u} type=topic auto_delete=false durable=true
+rabbitmqadmin -u root -p ***** declare exchange name=xl_${u} type=topic auto_delete=false durable=true
+
+
+

Ensuite, vous devez effectuer le même travail pour les serveurs sftp et / ou apache si +nécessaire, car l’authentification requise par le protocole de transport de charge +utile (SFTP, FTP ou HTTP(S)) est gérée séparément.

+
+
+
+

Installations avancées

+

Sur certaines configurations (nous les appelons généralement bunny), +nous utilisons un rabbitmq clustered, comme ceci:

+
/var/lib/rabbitmq/.erlang.cookie  same on all nodes
+
+on each node restart  /etc/init.d/rabbitmq-server stop/start
+
+on one of the node
+
+rabbitmqctl stop_app
+rabbitmqctl join_cluster rabbit@"other node"
+rabbitmqctl start_app
+rabbitmqctl cluster_status
+
+
+# having high availability queue...
+# here all queues that starts with "cmc." will be highly available on all the cluster nodes
+
+rabbitmqctl set_policy ha-all "^(cmc|q_)\.*" '{"ha-mode":"all"}'
+
+
+
+

Configuration keepalived d’un courtier Clustered

+

Dans cet exemple, bunny-op est un vip qui migre entre bunny1-op et bunny2-op. +Keepalived déplace le vip entre les deux:

+
#=============================================
+# vip bunny-op 192.101.12.59 port 5672
+#=============================================
+
+vrrp_script chk_rabbitmq {
+        script "killall -0 rabbitmq-server"
+        interval 2
+}
+
+vrrp_instance bunny-op {
+        state BACKUP
+        interface eth0
+        virtual_router_id 247
+        priority 150
+        track_interface {
+                eth0
+        }
+        advert_int 1
+        preempt_delay 5
+        authentication {
+                auth_type PASS
+                auth_pass bunop
+        }
+        virtual_ipaddress {
+# bunny-op
+                192.101.12.59 dev eth0
+        }
+        track_script {
+                chk_rabbitmq
+        }
+}
+
+
+
+
+

Intégration LDAP

+

Pour activer l’authentification LDAP pour rabbitmq:

+
rabbitmq-plugins enable rabbitmq_auth_backend_ldap
+
+# replace username by ldap username
+# clear password (will be verified through the ldap one)
+rabbitmqctl add_user username aaa
+rabbitmqctl clear_password username
+rabbitmqctl set_permissions -p / username "^xpublic|^amq.gen.*$|^cmc.*$" "^amq.gen.*$|^cmc.*$" "^xpublic|^amq.gen.*$|^cmc.*$"
+
+
+

Et vous devez configurer les paramètres LDAP dans le fichier de configuration du courtier : +(cet exemple de configuration ldap-dev test config a fonctionné lorsque nous l’avons testé….):

+
cat /etc/rabbitmq/rabbitmq.config
+[ {rabbit, [{auth_backends, [ {rabbit_auth_backend_ldap,rabbit_auth_backend_internal}, rabbit_auth_backend_internal]}]},
+  {rabbitmq_auth_backend_ldap,
+   [ {servers,               ["ldap-dev.cmc.ec.gc.ca"]},
+     {user_dn_pattern,       "uid=${username},ou=People,ou=depot,dc=ec,dc=gc,dc=ca"},
+     {use_ssl,               false},
+     {port,                  389},
+     {log,                   true},
+     {network,               true},
+    {vhost_access_query,    {in_group,
+                             "ou=${vhost}-users,ou=vhosts,dc=ec,dc=gc,dc=ca"}},
+    {resource_access_query,
+     {for, [{permission, configure, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+            {permission, write,
+             {for, [{resource, queue,    {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, exchange, {constant, true}}]}},
+            {permission, read,
+             {for, [{resource, exchange, {in_group, "cn=admin,dc=ec,dc=gc,dc=ca"}},
+                    {resource, queue,    {constant, true}}]}}
+           ]
+     }},
+  {tag_queries,           [{administrator, {constant, false}},
+                           {management,    {constant, true}}]}
+ ]
+}
+].
+
+
+
+
+

Nécessite RABBITMQ > 3.3.3.x

+

Cherchait à savoir comment utiliser LDAP strictement pour l’authentification par mot de passe. +La réponse que j’ai eue des gourous de Rabbitmq:

+
On 07/08/14 20:51, michel.grenier@ec.gc.ca wrote:
+> I am trying to find a way to use our ldap server  only for
+> authentification...
+> The user's  permissions, vhost ... etc  would already be set directly
+> on the server
+> with rabbitmqctl...  The only thing ldap would be used for would be
+> logging.
+> Is that possible... ?   I am asking because our ldap schema is quite
+> different from
+> what rabbitmq-server requieres.
+
+Yes (as long as you're using at least 3.3.x).
+
+You need something like:
+
+{rabbit,[{auth_backends,
+           [{rabbit_auth_backend_ldap, rabbit_auth_backend_internal}]}]}
+
+See http://www.rabbitmq.com/ldap.html and in particular:
+
+"The list can contain names of modules (in which case the same module is used for both authentication and authorisation), *or 2-tuples like {ModN, ModZ} in which case ModN is used for authentication and ModZ is used for authorisation*."
+
+Here ModN is rabbit_auth_backend_ldap and ModZ is rabbit_auth_backend_internal.
+
+Cheers, Simon
+
+
+
+
+

Support

+

Il est maintenant possible d’activer MQTT dans Sarracenia via le plugin RabbitMQ MQTT. +Voici un guide pratique minimal pour notre support RabbitMQTT:

+
    +
  • Après que tout autre service MQTT écoutant le port 1883 a été désactivé, +activez le plugin RabbitMQ MQTT.:

    +
    rabbitmq-plugins enable rabbitmq_mqtt
    +cat >> /etc/rabbitmq/rabbitmq.config << EOF
    +[{rabbitmq_mqtt, [{default_user,     <<"anonymous">>},
    +                  {default_pass,     <<"anonymous">>},
    +                  {allow_anonymous,  true},
    +                  {vhost,            <<"/">>},
    +                  {exchange,         <<"xmqtt_public">>},
    +                  {ssl_listeners,    []},
    +                  {tcp_listeners,    [1883]},
    +                  {tcp_listen_options, [{backlog, 4096},
    +                                        {nodelay, true}]}]}
    +].
    +EOF
    +systemctl restart rabbitmq-server
    +
    +
    +
  • +
  • Modifier les autorisations d’utilisateur anonyme (rabbit_mqtt.default_user) +pour permettre à l’utilisateur partenaire de s’abonner à votre flux mqtt +(c’est-à-dire en utilisant mosquitto_sub):

    +
    rabbitmqctl set_permissions -p / anonymous "^q_anonymous.*|^mqtt-subscription" "^q_anonymous.*|^xs_anonymous$|^mqtt-subscription" "^q_anonymous.*|^x[lrs]_anonymous.*|^x.*public$"
    +
    +
    +
  • +
  • Écrivez vos configurations qui seront publiées sur rabbitmqtt exchange:

    +
    # Voici un shovel/myshovel.conf minimal
    +# S’abonner à partir d’un échange amqp source
    +broker amqp://${afeeder}@${abroker}
    +exchange ${from_exchange}
    +
    +# publication sur rabbitmqtt exchange
    +post_broker amqp://${afeeder}@${abroker}
    +post_exchange xmqtt_public
    +post_topicPrefix  v03.${from_exchange}
    +report False
    +
    +
    +
  • +
  • ou consommer à partir de rabbitmqtt échange:

    +
    # Voici un subscribe/mysub.conf minimal
    +broker amqp://${asub}@${abroker}/
    +exchange xmqtt_public
    +topicPrefix v03.${from_exchange}
    +
    +# Imprimer tous les msg reçus
    +accept .*
    +on_message msg_rawlog
    +download off
    +
    +
    +

    Notez que nous utilisons xmqtt_public comme (post_)échange qui est défini comme +le rabbitmq_mqtt.exchange dans le fichier rabbitmq.config. Nous ajoutons également +l’échange de sources au (post_)topicPrefix, qui mappe l’échange source et pourrait +être utile si nous mappons plusieurs échanges à mqtt.

    +
  • +
  • Démarrez et testez votre configuration:

    +
    sr_shovel start myshovel.conf
    +sr_subscribe foreground mysub.conf
    +
    +
    +
  • +
  • Sur une autre machine, vous pouvez maintenant exécuter:

    +
    mosquitto_sub -h ${abroker} -t '#' -d
    +
    +
    +

    Les messages reçus de sr_subscribe et de mosquitto_sub doivent être les mêmes.

    +
  • +
+
+
+
+

Crochets de Sundew

+

Cette information n’est très probablement pas pertinente pour presque tous les utilisateurs. Sundew est un autre module de MetPX qui est essentiellement en cours de développement. +remplacé par Sarracénie. Cette information n’est utile qu’à ceux qui ont une base installée de Sundew souhaitant faire le pont +à la sarracénie. Les premiers travaux sur la sarracénie n’ont utilisé que le client d’abonnement comme téléchargeur, et le module de commutation de l’OMM existant. +de MetPX comme source de données. Il n’y avait pas de concept d’utilisateurs multiples, car le commutateur fonctionne comme une diffusion unique. +et outil de routage. Cette section décrit les types de colle utilisés pour nourrir les abonnés à la sarracénie à partir d’une source Sundew. +Il suppose une compréhension profonde de MetPX-Sundew. Actuellement, le script dd_notify.py crée des messages pour le fichier +protocole exp., v00. et v02 (dernière version du protocole de sarracénie)

+
+

Notifications sur DD

+

En remplacement des flux Atom/RSS qui indiquent aux abonnés quand de nouvelles données sont disponibles, nous mettons un courtier en ligne +sur notre serveur de diffusion de données (dd.weather.gc.ca.) Les clients peuvent s’y abonner. Pour créer les notifications, nous avons +un Sundew Sender (nommé wxo-b1-oper-dd.conf) avec un script d’envoi:

+
type script
+send_script sftp_amqp.py
+
+# connection info
+protocol    ftp
+host        wxo-b1.cmc.ec.gc.ca
+user        wxofeed
+password    **********
+ftp_mode    active
+
+noduplicates false
+
+# no filename validation (pds format)
+validation  False
+
+# delivery method
+lock  umask
+chmod 775
+batch 100
+
+
+

Nous voyons toutes les informations de configuration pour un expéditeur à fichier unique, mais le script send_script remplace le paramètre +expéditeur normal avec quelque chose qui construit aussi des messages AMQP. Cette configuration de l’expéditeur Sundew +invoque sftp_amqp.py comme un script pour faire l’envoi proprement dit, mais aussi pour placer la charge utile d’un fichier +Message AMQP dans le fichier /apps/px/txq/dd-notify-wxo-b1/, le mettant en fil d’attente pour un expéditeur AMQP Sundew. +Cette configuration sender´s c’est:

+
type amqp
+
+validation False
+noduplicates False
+
+protocol amqp
+host wxo-b1.cmc.ec.gc.ca
+user feeder
+password ********
+
+exchange_name cmc
+exchange_key  v02.post.${0}
+exchange_type topic
+
+reject ^ensemble.naefs.grib2.raw.*
+
+accept ^(.*)\+\+.*
+
+
+

La clé du sujet comprend une substitution. L’arborescence ${0} contient l’arborescence des répertoires dans laquelle la balise +a été placé sur dd (avec le / remplacé par .) Par exemple, voici une entrée de fichier journal:

+
2013-06-06 14:47:11,368 [INFO] (86 Bytes) Message radar.24_HR_ACCUM.GIF.XSS++201306061440_XSS_24_HR_ACCUM_MM.gif:URP:XSS:RADAR:GIF::20130606144709  delivered (lat=1.368449,speed=168950.887119)
+
+
+ +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/Docker.html b/fr/CommentFaire/Docker.html new file mode 100644 index 000000000..4a2b3f221 --- /dev/null +++ b/fr/CommentFaire/Docker.html @@ -0,0 +1,192 @@ + + + + + + + Exécution de MetPX via Docker — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Exécution de MetPX via Docker

+
+

Introduction

+

Alors que Sarracenia peut être installé via pip, debian / Ubuntu, une capacité Docker +est également fourni à l’appui de la conteneurisation et du cloud « run anywhere » +d’environnements natifs.

+

Pour offrir une flexibilité maximale, l’image par défaut de Sarracenia n’inclut pas +un courtier ou un point d’entrée. C’est à l’utilisateur d’orchestrer davantage son +déploiement en conséquence. Docker Compose est l’une de ces capacités d’orchestration.

+

Vous trouverez ci-dessous différents flux de travail pour créer, orchestrer et exécuter sarracenia via Docker:

+
# clone repo
+git clone https://github.com/MetPX/sarracenia.git
+cd sarracenia
+
+# build image
+docker build -t metpx-sr3:latest .
+
+# sarracenia connected to a rabbitmq setup
+# start
+docker-compose -f docker/compose/pump/docker-compose.yml up
+# stop
+docker-compose -f docker/compose/pump/docker-compose.yml down
+
+
+
+
+

Journalisation

+

Les normes dans le monde docker sont d’envoyer des messages à la sortie standard, donc +l’option logStdout doit être utilisée dans toutes les configurations d’un conteneur docker. +Cela permettra aux journaux docker de fonctionner comme prévu dans un environnement docker, en imprimant +à la sortie standard.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/Exemples_Hydro.html b/fr/CommentFaire/Exemples_Hydro.html new file mode 100644 index 000000000..36ac1c932 --- /dev/null +++ b/fr/CommentFaire/Exemples_Hydro.html @@ -0,0 +1,377 @@ + + + + + + + Utilisation de plugins pour récupérer des données hydrométriques — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Utilisation de plugins pour récupérer des données hydrométriques

+

Plusieurs sites Web de données environnementales utilisent des API pour communiquer des données. Afin de faire de la publicité pour la +disponibilité de nouveaux fichiers et les intégrer de manière transparente dans la pile Sarracenia, quelques plugins sont +nécessaire pour étendre la fonctionnalité de polling.

+
+

Protocoles de polling pris en charge en mode natif

+

Prêt à l’emploi, Sarracenia prend en charge l’interrogation des sources HTTP / HTTPS et SFTP / FTP où le nom de fichier +est ajouté à la fin de l’URL de base. Par exemple, si vous essayez d’accéder les données de niveau de l’eau +du réservoir du lac Ghost près de Cochrane en Alberta, auxquelles on peut accéder en naviguant jusqu’à +http://environment.alberta.ca/apps/Basins/data/figures/river/abrivers/stationdata/L_HG_05BE005_table.json, +l’URL de base dans ce cas serait considérée comme le +http://environment.alberta.ca/apps/Basins/data/figures/river/abrivers/stationdata/ +et la partie du nom de fichier L_HG_05BE005_table.json. Étant donné que l’URL de base ne +contient pas un bon répertoire avec tous les fichiers JSON, si vous vouliez vérifier si les nouvelles +données de niveau d’eau ont a été ajouté au localisateur ci-dessus, puisqu’il s’agit d’un fichier +JSON, vous pouvez vérifier l’en-tête modifié en dernier pour +voir s’il a été modifié depuis votre dernier poll. À partir de là, vous devrez définir le new_baseurl sur la +première partie, et le new_file réglé sur la seconde, et une instance sr_subscribe saurait assembler +pour localiser le fichier et le télécharger.

+
+

Extension des protocoles de poll

+

Si la source de données ne respecte pas cette convention (voir API NOAA CO-OPS et Service Web de valeurs instantanées USGS +pour des exemples de deux sources de données qui ne le font pas), un module registered_as +peut être inclus en bas d’un fichier plugin pour définir la liste des protocoles en cours +d’extension ou d’implémentation

+
def registered_as(self):
+        return ['http','https']
+
+
+

Ca surchargerait alors la méthode de transfert et utiliserait celle décrite dans le plugin.

+
+
+
+

Exemples d’intégration d’API dans des plugins

+
+

API NOAA CO-OPS

+

Le département des marées et des courants de National Oceanic and Atmospheric Administration publie son +programme coopératif données d’observations et de prédictions de stations via un service Web GET RESTful, +disponible à l’adresse the NOAA Tides et le site Web de Currents. +Par exemple, si vous souhaitez accéder aux données de température de l’eau de la dernière heure à Honolulu, +vous pouvez naviguer vers https://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv. +Une nouvelle observation est enregistrée toutes les six minutes, donc si vous vouliez annoncer uniquement de nouvelles données via +Sarracenia, vous configureriez une instance sr_poll pour vous connecter à l’API, mettre en veille toutes les heures et leur construire +une requête GET à annoncer à chaque fois qu’il se réveille (cela fonctionne sous l’hypothèse potentiellement erronée +que la source de données maintienne son accord). Pour télécharger ce nouveau fichier, vous devez vous connecter +à un sr_subscribe au même échange sur lequel il a été annoncé, et il récupérerait l’URL, qu’un do_download +plugin pourrait alors prendre et télécharger. Un exemple de plugin de poll qui saisit toute la température et le niveau d’eau +de la dernière heure de toutes les stations CO-OPS et les publient, sont incluses sous plugins comme +poll_noaa.py.

+

Un plugin do_download qui correspond à une une instance sarra pour télécharger ce fichier est inclus +comme download_noaa.py. +Des exemples de configurations pour sr_poll et sr_subscribe sont inclus sous +exemples, nommés pollnoaa.conf +et subnoaa.conf. +Pour exécuter, ajoutez à la fois des plugins et des configurations à l’aide de l’action add, modifiez les +variables appropriées dans la config (le flowbroker, et la pollingUrl entre autres. Si vous exécutez à partir +d’un serveur RabbitMQ local, de la documentation sous Contibution/Dev.rst +sur la façon de configurer le serveur peut être utile). Si tout a été configuré correctement, la sortie doit +ressemblez à quelque chose comme ceci:

+
[aspymap:~]$ sr_poll foreground pollnoaa.conf
+2018-09-26 15:26:57,704 [INFO] sr_poll pollnoaa startup
+2018-09-26 15:26:57,704 [INFO] log settings start for sr_poll (version: 2.18.07b3):
+2018-09-26 15:26:57,704 [INFO]  inflight=unspecified events=create|delete|link|modify use_pika=False
+2018-09-26 15:26:57,704 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=Nonems
+2018-09-26 15:26:57,704 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-09-26 15:26:57,705 [INFO]  heartbeat=300 default_mode=400 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-09-26 15:26:57,705 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-09-26 15:26:57,705 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-09-26 15:26:57,705 [INFO]  post_base_dir=None post_base_url=http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station={0:}&product={1:}&units=metric&time_zone=gmt&application=web_services&format=csv/ sum=z,d blocksize=209715200
+2018-09-26 15:26:57,705 [INFO]  Plugins configured:
+2018-09-26 15:26:57,705 [INFO]          on_line: Line_Mode
+2018-09-26 15:26:57,705 [INFO]          on_html_page: Html_parser
+2018-09-26 15:26:57,705 [INFO]          do_poll: NOAAPoller
+2018-09-26 15:26:57,705 [INFO]          on_message:
+2018-09-26 15:26:57,705 [INFO]          on_part:
+2018-09-26 15:26:57,705 [INFO]          on_file: File_Log
+2018-09-26 15:26:57,705 [INFO]          on_post: Post_Log
+2018-09-26 15:26:57,705 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse
+2018-09-26 15:26:57,705 [INFO]          on_report:
+2018-09-26 15:26:57,705 [INFO]          on_start:
+2018-09-26 15:26:57,706 [INFO]          on_stop:
+2018-09-26 15:26:57,706 [INFO] log_settings end.
+2018-09-26 15:26:57,709 [INFO] Output AMQP broker(localhost) user(tsource) vhost(/)
+2018-09-26 15:26:57,710 [INFO] Output AMQP exchange(xs_tsource)
+2018-09-26 15:26:57,710 [INFO] declaring exchange xs_tsource (tsource@localhost)
+2018-09-26 15:26:58,403 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv
+2018-09-26 15:26:58,403 [INFO] post_log notice=20180926192658.403634 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv CO-OPS__1611400__wt.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,554 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND
+2018-09-26 15:26:58,554 [INFO] post_log notice=20180926192658.554364 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1611400&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND CO-OPS__1611400__wl.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,691 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv
+2018-09-26 15:26:58,691 [INFO] post_log notice=20180926192658.691466 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_temperature&units=metric&time_zone=gmt&application=web_services&format=csv CO-OPS__1612340__wt.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+2018-09-26 15:26:58,833 [INFO] poll_noaa file posted: http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND
+2018-09-26 15:26:58,834 [INFO] post_log notice=20180926192658.833992 http://tidesandcurrents.noaa.gov/api/datagetter?range=1&station=1612340&product=water_level&units=metric&time_zone=gmt&application=web_services&format=csv&datum=STND CO-OPS__1612340__wl.csv headers={'source': 'noaa', 'to_clusters': 'ALL', 'sum': 'z,d', 'from_cluster': 'localhost'}
+^C2018-09-26 15:26:58,965 [INFO] signal stop (SIGINT)
+2018-09-26 15:26:58,965 [INFO] sr_poll stop
+
+
+

pour le polling et:

+
[aspymap:~]$ sr_subscribe foreground subnoaa.conf
+2018-09-26 15:26:53,473 [INFO] sr_subscribe subnoaa start
+2018-09-26 15:26:53,473 [INFO] log settings start for sr_subscribe (version: 2.18.07b3):
+2018-09-26 15:26:53,473 [INFO]  inflight=.tmp events=create|delete|link|modify use_pika=False
+2018-09-26 15:26:53,473 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=300000ms
+2018-09-26 15:26:53,473 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-09-26 15:26:53,473 [INFO]  heartbeat=300 default_mode=000 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-09-26 15:26:53,473 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-09-26 15:26:53,473 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=False
+2018-09-26 15:26:53,473 [INFO]  Plugins configured:
+2018-09-26 15:26:53,473 [INFO]          do_download: BaseURLDownloader
+2018-09-26 15:26:53,473 [INFO]          do_get     :
+2018-09-26 15:26:53,473 [INFO]          on_message:
+2018-09-26 15:26:53,474 [INFO]          on_part:
+2018-09-26 15:26:53,474 [INFO]          on_file: File_Log
+2018-09-26 15:26:53,474 [INFO]          on_post: Post_Log
+2018-09-26 15:26:53,474 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse RETRY
+2018-09-26 15:26:53,474 [INFO]          on_report:
+2018-09-26 15:26:53,474 [INFO]          on_start:
+2018-09-26 15:26:53,474 [INFO]          on_stop:
+2018-09-26 15:26:53,474 [INFO] log_settings end.
+2018-09-26 15:26:53,474 [INFO] sr_subscribe run
+2018-09-26 15:26:53,474 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-09-26 15:26:53,478 [INFO] Binding queue q_tsource.sr_subscribe.subnoaa.90449861.55888967 with key v02.post.# from exchange xs_tsource on broker amqp://tsource@localhost/
+2018-09-26 15:26:53,480 [INFO] reading from to tsource@localhost, exchange: xs_tsource
+2018-09-26 15:26:53,480 [INFO] report suppressed
+2018-09-26 15:26:53,480 [INFO] sr_retry on_heartbeat
+2018-09-26 15:26:53,486 [INFO] No retry in list
+2018-09-26 15:26:53,488 [INFO] sr_retry on_heartbeat elapse 0.007632
+2018-09-26 15:26:58,751 [INFO] download_noaa: file noaa_20180926_1926_1611400_TP.csv
+2018-09-26 15:26:58,751 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1611400__wt.csv
+2018-09-26 15:26:58,888 [INFO] download_noaa: file noaa_20180926_1926_1611400_WL.csv
+2018-09-26 15:26:58,889 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1611400__wl.csv
+2018-09-26 15:26:59,026 [INFO] download_noaa: file noaa_20180926_1926_1612340_TP.csv
+2018-09-26 15:26:59,027 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1612340__wt.csv
+2018-09-26 15:26:59,170 [INFO] download_noaa: file noaa_20180926_1926_1612340_WL.csv
+2018-09-26 15:26:59,171 [INFO] file_log downloaded to: /home/ib/dads/map/hydro_examples_sarra/fetch/noaa//CO-OPS__1612340__wl.csv
+^C2018-09-26 15:27:00,597 [INFO] signal stop (SIGINT)
+2018-09-26 15:27:00,597 [INFO] sr_subscribe stop
+
+
+

pour le téléchargement.

+
+
+

Service Web SHC SOAP

+

Un service Web SOAP (Simple Object Access Protocol) utilise un système de messagerie XML pour fournir les données demandées +données sur un réseau. Le client peut spécifier des paramètres pour une opération prise en charge (par exemple une recherche) sur +le service Web, noté avec une extension de fichier wdsl, et le serveur renverra une réponse SOAP au format XML. +Le Service Hydrographique du Canada (SHC) utilise ce service Web comme API pour obtenir des données hydrométriques. +données en fonction des paramètres envoyés. +Il ne prend en charge qu’une seule opération, la recherche, qui accepte les éléments suivants +paramètres : dataName, latitudeMin, latitudeMax, longitudeMin, longitudeMax, depthMin, depthMax, dateMin, +dateMax, start, end, tailleMax, metadata, metadataSelection, order. Par exemple, une recherche renverra toutes les +données sur le niveau d’eau disponibles à Acadia Cove au Nunavut le 1er septembre 2018 si votre recherche contient +les paramètres suivants : ‘wl’, 40.0, 85.0, -145.0, -50.0, 0.0, 0.0, ‘2018-09-01 00:00:00’, +‘2018-09-01 23:59:59’, 1, 1000, ‘true’, ‘station_id=4170, ‘asc’.

+

La réponse peut ensuite être convertie en un fichier et vidé, qui peut être publié, ou les paramètres peuvent +être annoncés eux-mêmes dans le rapport. Remarquez, qu’un plugin sarra do_download pourrait ensuite déchiffrer +et traiter les données dans un fichier côté utilisateur. Afin de ne publier que de nouvelles données à partir de SHC, une instance de poll peut être configurée pour se mettre en veille toutes les 30 minutes, +et un plugin do_poll pourrait définir la plage de début-fin sur la dernière demi-heure avant de former la demande. +Chaque demande est renvoyée avec un message d’état confirmant s’il s’agissait d’un appel de fonction valide. Le plugin pourrait +ensuite vérifier que le message d’état est correct avant de publier le message annonçant de nouvelles données sur l’échange. +Un plugin do_download prend ces paramètres passés dans le message, forme une requête SOAP avec eux, et +extrait les données/les enregistre dans un fichier. Des exemples de plugins qui effectuent ces deux étapes peuvent être trouvés sous +plugins, nommés poll_shc_soap.py +et download_shc_soap.py. +Des exemples de configurations pour l’exécution des deux sont inclus sous exemples, nommés +pollsoapshc.conf et +subsoapshc.conf.

+
+
+

Service Web de valeurs instantanées USGS

+

Le United States Geological Survey publie ses données sur l’eau par son Service Web des valeurs instantanées RESTful +, qui utilise des requêtes HTTP GET pour filtrer leurs données. Il renvoie les données dans les fichiers XML une fois +demandé, et peut prendre en charge plus d’un argument d’ID de station à la fois (téléchargement de données en bloc). Plus d’infos sur +le service peut être consulté sur le site Web des services de l’eau. +Ils ont une longue liste de paramètres à spécifier en fonction du type de données sur l’eau que vous souhaitez récupérer, +qui est passé par l’argument parameterCd. Par exemple, si vous vouliez aller chercher l’évacuation de l’eau, nivelez +les données de température des trois dernières heures de North Fork Vermilion River près de Bismarck, IL, vous utiliseriez +l’URL suivante : +https://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site=03338780&period=PT3H&parameterCd=00060,00065,00011. +Une liste de codes de paramètres à utiliser pour personnaliser vos résultats peut être trouvée +ici. +Les plugins pour n’importe quel service Web GET peuvent être généralisés pour utilisation, de sorte que les plugins +utilisés pour l’API NOAA CO-OPS peuvent également être réutilisés dans ce contexte. Par défaut, les ID de station +à transmettre sont différents, ainsi que le méthode de les passer, de sorte que le code de plug-in qui détermine les +ID de station à utiliser diffère, mais la méthode conceptuellement, c’est toujours la même chose. Vous transmettez +une version généralisée de l’URL comme pollingUrl dans la config, par exemple:

+
https://waterservices.usgs.gov/nwis/iv/?format=waterml,2.0&indent=on&site={0}&period=PT3H&parameterCd=00060,00065,00011
+
+
+

et dans le plugin, vous remplaceriez le ‘{0}’ (Python rend cela facile avec le formatage de chaîne) par les sites qui +vous intéressent, et si d’autres paramètres doivent être modifiés, ils peuvent être remplacés de la même manière. +Si un fichier d’ID de site de station n’a pas été transmis en tant qu’option de configuration de plug-in, +le plug-in saisit par défaut tout les identifiants de site enregistrés à partir de +the USGS website. +Le service Web IV prend en charge les requêtes avec plusieurs ID de site spécifiés (séparés par des virgules). +Si l’option plugin poll_usgs_nb_stn a été spécifié à la taille du bloc dans la configuration, il faudra des +groupes de données de stations en fonction du nombre passé (cela réduit les requêtes Web et accélère +la collecte de données en cas de collecte en bloc).

+

Pour exécuter cet exemple, les configs et les plugins se trouvent sous plugins +(poll_usgs.py +et download_usgs.py) +et examples (pollusgs.conf +et subusgs.conf).

+
+
+
+

Cas d’utilisation

+

Les plug-ins hydrométriques ont été développés pour le cas d’utilisation canhys d’Environnement Canada, où les fichiers +contenant les métadonnées de la station seraient utilisées comme données d’entrée pour recueillir les données +hydrométriques. Chaque plugin fonctionne également en générant tous les identifiants de station valides de l’autorité +de l’eau elle-même et le branchement de ces entrées. Cette option alternative peut être a basculé en omettant la +variable de configuration du plug-in qui spécifierait autrement le fichier de métadonnées de la station. +Les plugins de téléchargement renomment également le fichier selon la convention spécifique de ce cas d’utilisation.

+

La plupart de ces sources ont des avertissements que ces données ne sont pas de qualité assurée, mais elles sont rassemblées en soft +en temps réel (annoncées a la secondes/minutes à partir du moment où elles ont été enregistré).

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/FlowCallbacks.html b/fr/CommentFaire/FlowCallbacks.html new file mode 100644 index 000000000..57136a709 --- /dev/null +++ b/fr/CommentFaire/FlowCallbacks.html @@ -0,0 +1,642 @@ + + + + + + + Écriture de plugins FlowCallback — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Écriture de plugins FlowCallback

+

Tous les composants de Sarracenia implémentent l’algorithme Flow. +La classe principale de Sarracenia est sarracenia.flow et +la fonctionnalité de base est implémentée à l’aide de la classe créée pour ajouter un +traitement personnalisé à un flux, la classe flowcb (flow callback).

+

Pour une discussion détaillée de l’algorithme de flux elle-même, jetez un coup d’œil +sur le manuel Concepts. Pour tout flux, on peut +ajouter un traitement personnalisé à divers moments au cours du traitement en sous-classant +la classe sarracenia.flowcb.

+

En bref, l’algorithme comporte les étapes suivantes :

+
    +
  • gather – collecter passivement les messages de notification à traiter.

  • +
  • poll – collecter activement les messages de notification à traiter.

  • +
  • filter – appliquer des correspondances d’expression régulière accept/reject à la liste des messages de notification.

    +
      +
    • after_accept point d’entré de callback

    • +
    +
  • +
  • work – effectuer un transfert ou une transformation sur un fichier.

    +
      +
    • after_work point d’entrée de callback

    • +
    +
  • +
  • post – publier le résultat du travail effectué pour l’étape suivante.

  • +
+

Un flowcallback, est une classe python construite avec des routines nommées pour +indiquer quand le programmeur veut qu’elles soient appelés. +Il y a plusieurs exemples de class python flowcallback inclus avec Sarracenia, +qu’on peut voir (en anglais) dans Flowcallback Reference +qui peuvent servir comment modèle pour partir de nouvelles classes.

+

Ce guide décrit les éléments nécessaire pour façonner des classes flowcb à partir de zéro.

+
+

Entrées de fichier de configuration pour utiliser Flow_Callbacks

+

Pour ajouter un callback à un flux, une ligne est ajoutée au fichier de configuration

+
flowcb sarracenia.flowcb.log.Log
+
+
+

Si vous suivez la convention et que le nom de la classe est une +version en majuscules (Log) du nom de fichier (log), un raccourci est disponible

+
callback log
+
+
+

Le constructeur de classe accepte un objet de classe sarracenia.config.Config, +appelées options, qui stockent tous les paramètres à utiliser par le flux en cours d’exécution. +Options est utilisé pour remplacer le comportement par défaut des flux et des callbacks. +L’argument du flowcb est une classe python standard qui doit être +dans le chemin python normal pour l’import python, et le dernier élément +est le nom de la classe dans le fichier qui doit être instancié +en tant qu’instance flowcb.

+

un paramètre pour un callback est déclaré comme suit

+
set sarracenia.flowcb.filter.log.Log.logLevel debug
+
+
+

(le préfixe du paramètre correspond à la hiérarchie de types dans flowCallback)

+

lorsque le constructeur du callback est appelé, son argument options contiendra:

+
options.logLevel = 'debug'
+
+
+

Si aucune substitution spécifique au module n’est présente, le paramètre le plus global est utilisé.

+
+
+

Worklists

+

Outre l’option, l’autre argument principal pour les routines de callbacks after_accept et after_work +est worklist. Worklist est donnée aux points d’entrée qui se produisent pendant le traitement de message de +notification, et est un certain nombre de worklist de messages de notification:

+
worklist.incoming --> messages de notification à traiter (nouveaux ou nouvelles tentatives).
+worklist.ok       --> traité avec succès
+worklist.rejected --> les messages de notification ne doivent pas être traités ultérieurement.
+worklist.failed   --> messages de notification pour lesquels le traitement a échoué.
+                      les messages de notification ayant échoué seront réessayés.
+
+
+

Initialement, tous les messages de notification sont placés dans worklists.incoming. +si un plugin décide :

+
    +
  • un message de notification n’est pas pertinent, déplacé vers la worklist rejetée.

  • +
  • aucun traitement supplémentaire du message de notification n’est nécessaire, déplacez-le vers worklist ok.

  • +
  • une opération a échoué et elle doit être réessayée plus tard, passez à la worklist échouée.

  • +
+

Ne supprimez pas de toutes les listes, déplacez uniquement les messages de notification entre les worklists. +Il est nécessaire de placer les messages de notification rejetés dans la worklist appropriée +afin qu’ils soient reconnus comme reçus. Les messages ne peuvent être supprimés qu’une +fois que l’accusé de réception a été pris en charge.

+
+
+

Journalisation

+

Python a une excellente journalisation intégrée, et il faut simplement utiliser le module +d’une manière normale, pythonique, avec:

+
import logging
+
+
+

Après toutes les importations dans votre fichier source python, définissez un enregistreur +pour le fichier source

+
logger = logging.getLogger(__name__)
+
+
+

Comme c’est normal avec le module de journalisation Python, les messages de notification peuvent alors +être affiché dans le journal

+
logger.debug('got here')
+
+
+

Chaque message de notification dans le journal sera précédé de la classe et de la +routine émettant le message de notification du journal, ainsi que de la date/heure.

+

On peut également implémenter un remplacement par module pour les niveaux de journalisation. +Voir sarracenia/moth/amqp.py comme exemple. Pour ce module, +le niveau de journalisation des messages de notification est porté à l’avertissement par défaut. +On peut le remplacer par un paramètre de fichier de configuration:

+
set sarracenia.moth.amqp.AMQP.logLevel info
+
+
+

dans la fonction __init__(self,options) du callback, +inclure les lignes:

+
me = "%s.%s" % ( __class__.__module__ , __class__.__name__ )
+if 'logLevel' in self.o['settings'][me]:
+             logger.setLevel( self.o['logLevel'].upper() )
+
+
+
+
+

Initialisation et paramètres

+

L’étape suivante consiste à déclarer une classe

+
class Myclass(FlowCB):
+
+
+

en tant que sous-classe de FlowCB. Les principales routines de la classe sont les points d’entrée +qui seront appelés au moment où leur nom l’indique. S’il manque un point d’entrée donné à votre classe, +elle ne sera tout simplement pas appelée. La classe __init__() est utilisée pour +initialiser des éléments pour la classe de callback:

+
def __init__(self, options):
+
+    super().__init__(options,logger)
+
+    self.o.add_option( 'myoption', 'str', 'usuallythis')
+
+
+

Les lignes de configuration de la journalisation dans __init__ permettent de définir un niveau de journalisation spécifique +pour cette classe flowCallback. Une fois la journalisation passe-partout est terminée, +la routine add_option pour définir les paramètres de la classe. +les utilisateurs peuvent les inclure dans les fichiers de configuration, tout comme les options intégrées

+
myoption IsReallyNeeded
+
+
+

Le résultat d’un tel réglage est que le self.o.myoption = ‘IsReallyNeeded’. +Si aucune valeur n’est définie dans la configuration, self.o.myoption sera par défaut ‘usuallyThis’ +Il existe différents types d’options, où le type déclaré modifie l’analyse:

+
'count'    type de nombre entier.
+'duration' un nombre à virgule flottante indiquant une quantité de secondes (0,001 est 1 miliseconde)
+           modifié par un suffixe unitaire ( m-minute, h-heure, w-semaine )
+'flag'     option booléen (True/False).
+'list'     une liste de valeurs de chaîne, chaque occurrence suivante se caténates au total.
+           toutes les options de plugin v2 sont déclarées de type list.
+'size'     taille entière. Suffixes k, m et g pour les multiplicateurs kilo, mega et giga (base 2).
+'str'      une valeur de chaîne arbitraire, comme tous les types ci-dessus,
+           chaque occurrence suivante remplace la précédente.
+
+
+
+
+

Points d’entrée

+

Autres entry_points, extraits de sarracenia/flowcb/__init__.py

+
def name(self):
+    Task: return the name of a plugin for reference purposes. (automatically there)
+
+def ack(self,messagelist):
+    Task: acknowledge notification messages from a gather source.
+
+def gather(self):
+    Task: gather notification messages from a source... return a list of notification messages.
+          can also return tuple (keep_going, new_messages) where keep_going is a flag
+          that when False stops processing of further gather routines.
+    return []
+
+"""
+  application of the accept/reject clauses happens here, so after_accept callbacks
+  run on a filtered set of notification messages.
+
+"""
+
+def after_accept(self,worklist):
+    """
+     Task: just after notification messages go through accept/reject masks,
+           operate on worklist.incoming to help decide which notification messages to process further.
+           and move notification messages to worklist.rejected to prevent further processing.
+           do not delete any notification messages, only move between worklists.
+    """
+def do_poll(self):
+    Task: build worklist.incoming, a form of gather()
+
+def on_data(self,data):
+    Task:  return data transformed in some way.
+
+    return new_data
+
+def after_work(self,worklist):
+    Task: operate on worklist.ok (files which have arrived.)
+
+def post(self,worklist):
+     Task: operate on worklist.ok, and worklist.failed. modifies them appropriately.
+           notification message acknowledgement has already occurred before they are called.
+
+def on_housekeeping(self):
+     do periodic processing.
+
+def on_html_page(self,page):
+     Task: modify an html page.
+
+def on_line(self,line):
+     used in FTP polls, because servers have different formats, modify to canonical use.
+
+     Task: return modified line.
+
+def on_start(self):
+     After the connection is established with the broker and things are instantiated, but
+     before any notification message transfer occurs.
+
+def on_stop(self):
+
+
+
+
+

new_* Champs

+

Pendant le traitement des messages de notification, les valeurs des champs standard d’origine restent généralement inchangées (telles que lues). +Pour modifier les champs des messages de notification transmis aux consommateurs en aval, on modifie plutôt new_field +de celui du message, car l’original est nécessaire pour une récupération réussie en amont +:

+
    +
  • msg[‘new_baseUrl’] … baseUrl à transmettre aux consommateurs en aval.

  • +
  • msg[‘new_dir’] … le répertoire dans lequel un fichier sera téléchargé ou envoyé.

  • +
  • msg[‘new_file’] …. nom final du fichier à écrire.

  • +
  • msg[‘new_inflight_path’] … nom calculé du fichier temporaire à écrire avant de le renommer en msg[‘new_file’] … ne pas modifier manuellement.

  • +
  • msg[‘new_relPath’] … calculé à partir de ‘new_baseUrl’, ‘post_baseDir’, ‘new_dir’, ‘new_file’… ne pas modifier manuellement.

  • +
  • msg[‘post_version’] … le format d’encodage du message à poster (à partir des paramètres)

  • +
  • msg[‘new_subtopic’] … la hiérarchie des sous-thèmes qui sera codée dans le message de notification destiné aux consommateurs en aval.

  • +
+
+
+

Les champs override

+

Pour modifier le traitement des messages, on peut définir des remplacements pour modifier le fonctionnement des algorithmes intégrés. +Par exemple:

+
    +
  • msg[‘nodupe_override’] = { ‘key’: …, ‘path’: … } modifie le fonctionnement de la détection des doublons.

  • +
  • msg[‘topic’] … définit le sujet d’un message publié (au lieu d’être calculé à partir d’autres champs.)

  • +
  • msg[‘exchangeSplitOverride’] = int … change la façon dont post_ExchangeSplit choisit parmi plusieurs postExchanges

  • +
+
+
+

Personnalisation de la suppression des doublons

+

Le traitement intégré des doublons consiste à utiliser le champ d’identité comme clé et à stocker le chemin (path) comme valeur. +Ainsi, si un fichier est reçu avec la même clé et que le path est déjà présent, il est alors considéré comme un doublon. +et laissé tomber.

+

Dans certains cas, nous pouvons souhaiter que seul le nom du fichier soit utilisé, donc si un fichier portant le même nom est reçu deux fois, +quel que soit le contenu, il doit alors être considéré comme un doublon et supprimé. Ceci est utile lorsque plusieurs systèmes +produisent les mêmes produits, mais ils ne sont pas identiques au niveau des bits. Le flowcb intégré qui implémente +cette fonctionnalité est ci-dessous :

+
import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+class Name(FlowCB):
+    """
+      Remplacez la comparaison afin que les fichiers portant le même nom, quel que soit
+      le répertoire dans lequel ils se trouvent, sont considérés comme identiques.
+      Ceci est utile lors de la réception de données provenant de deux sources différentes
+      (deux arbres différents) et vanner entre eux.
+    """
+    def after_accept(self, worklist):
+        for m in worklist.incoming:
+            if not 'nodupe_override' in m:
+                m['_deleteOnPost'] \|= set(['nodupe_override'])
+                m['nodupe_override'] = {}
+
+            m['nodupe_override']['path'] = m['relPath'].split('/')[-1]
+            m['nodupe_override']['key'] = m['relPath'].split('/')[-1]
+
+
+
+
+

Personnalisation de post_exchangeSplit

+

La fonction ExchangeSplit permet à un seul flux d’envoyer des sorties à différents échanges, +numérotés 1…n pour assurer la répartition de la charge. Le traitement intégré le fait de manière +manière fixe basée sur le hachage du champ d’identification. Le but d’exchangeSplit est de +permettre à un ensemble commun de chemins en aval de recevoir un sous-ensemble du flux total, et pour +les produits avec un « routage » similaire atterrissent sur le même nœud en aval. Par exemple, un fichier +avec une somme de contrôle donnée, pour que le vannage fonctionne, il doit atterrir sur le même nœud en aval.

+

Il se pourrait que, plutôt que d’utiliser une somme de contrôle, on préfère utiliser une autre somme de contrôle. +méthode pour décider quel échange est utilisé:

+
import logging
+from sarracenia.flowcb import FlowCB
+import hashlib
+logger = logging.getLogger(__name__)
+
+
+class Distbydir(FlowCB):
+  """
+    Remplacer l'utilisation du champ d'identité afin que les produits puissent
+    être regroupés par répertoire dans le relPath. Cela garantit que tous les produits
+    reçus du même répertoire sont publiés dans le même exchange lorsque post_exchangeSplit est actif.
+  """
+  def after_accept(self, worklist):
+      for m in worklist.incoming:
+          m['_deleteOnPost'] |= set(['exchangeSplitOverride'])
+          m['exchangeSplitOverride'] = int(hashlib.md5(m['relPath'].split(os.sep)[-2]).hexdigest()[0])
+
+
+
+
+

Exemple de sous-classe Flowcb

+

Il s’agit d’un exemple de fichier de classe de callback (gts2wis2.py) qui accepte les fichiers dont +les noms commencent par ceux d’AHL et renomme l’arborescence des répertoires selon une norme différente, +celui en évolution pour le WIS 2.0 de WMO (pour plus d’informations sur ce module : +https://github.com/wmo-im/GTStoWIS2)

+
import json
+import logging
+import os.path
+
+from sarracenia.flowcb import FlowCB
+import GTStoWIS2
+
+logger = logging.getLogger(__name__)
+
+
+class GTS2WIS2(FlowCB):
+
+  def __init__(self, options):
+
+      if hasattr(options, 'logLevel'):
+          logger.setLevel(getattr(logging, options.logLevel.upper()))
+      else:
+          logger.setLevel(logging.INFO)
+      self.topic_builder=GTStoWIS2.GTStoWIS2()
+      self.o = options
+
+
+  def after_accept(self, worklist):
+
+      new_incoming=[]
+
+      for msg in worklist.incoming:
+
+          # fix file name suffix.
+          type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )
+          tpfx=msg['subtopic']
+
+          # input has relpath=/YYYYMMDD/... + pubTime
+          # need to move the date from relPath to BaseDir, adding the T hour from pubTime.
+          try:
+              new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]
+              t='.'.join(tpfx[0:2])+'.'+new_baseSubDir
+              new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir
+              new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])
+              msg['new_dir'] = new_baseDir + os.sep + new_relDir
+              msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, msg['new_file'] )
+
+          except Exception as ex:
+              logger.error( "skipped" , exc_info=True )
+              worklist.failed.append(msg)
+              continue
+
+          msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )
+          new_incoming.append(msg)
+
+      worklist.incoming=new_incoming
+
+
+

La routine after_accept est l’une des deux plus courantes en cours d’utilisation.La routine after_accept est l’une des deux plus courantes en cours d’utilisation.

+

La routine after_accept a une boucle externe qui parcourt l’ensemble de la +liste des messages de notification entrants. Le traitement normal est qu’il construit une nouvelle liste de +messages de notification entrants, en ajoutant tous les messages rejetés à worklist.failed. La +liste est juste une liste de messages de notification, où chaque message de notification est un dictionnaire python avec +tous les champs stockés dans un message de notification au format v03. Dans le message de notification, il y a, +par exemple, les champs baseURL et relPath :

+
    +
  • baseURL - baseURL de la ressource à partir duquel un fichier serait obtenu.

  • +
  • relPath - le chemin d’accès relatif à ajouter à baseURL pour obtenir l’URL de téléchargement complet.

  • +
+

Cela se produit avant que le transfert (téléchargement ou envoi, ou traitement) du fichier +se soit produit, de sorte que l’on peut changer le comportement en modifiant les champs dans le message de notification. +Normalement, les chemins de téléchargement (appelés new_dir et new_file) refléteront l’intention +pour faire un mirroir à l’arborescence de source d’origine. Donc si vous avez a/b/c.txt sur l’arborescence source, et +vous téléchargez dans le répertoire mine sur le système local, la new_dir serait +mine/a/b et new_file serait c.txt.

+

Le plugin ci-dessus modifie la mise en page des fichiers à télécharger, en fonction de la classe +GTStoWIS, qui prescrit une arborescence de répertoires +différente en sortie. Il y a beaucoup de champs à mettre à jour lors de la modification de placement de fichier, +il est donc préférable d’utiliser:

+
msg.updatePaths( self.o, new_dir, new_file )
+
+
+

pour mettre à jour correctement tous les champs nécessaires dans le message de notification. Cela mettra à jour +‘new_baseURL’, ‘new_relPath’, ‘new_subtopic’ à utiliser lors de l’affichage.

+

La partie try/except de la routine traite le cas ou un fichier pourrait arriver +avec un nom à partir duquel une arborescence de topic ne peut pas être générée, puis une exception +peut se produire et le message de notification est ajouté à la liste de travail ayant échoué et ne sera pas +traité par des plugins ultérieurs.

+
+
+

Autres exemples

+

La sous-classification de Sarracenia.flowcb est utilisée en interne pour effectuer beaucoup de travail de base. +C’est une bonne idée de regarder le code source de sarracenia lui-même. Par exemple:

+
    +
  • sr3 list fcb est une commande pour répertorier toutes les classes de rappel incluses dans le package metpx-sr3.

  • +
  • sarracenia.flowcb jetez un coup d’œil dans le fichier __init__.py qui s’y trouve, +qui fournit ces informations sur un format plus succinct.

  • +
  • sarracenia.flowcb.gather.file.File est une classe qui implémente la publication de fichiers +et la surveillance de répertoires, dans le sens d’un callback qui implémente le point d’entrée +gather, en lisant un système de fichiers et en créant une liste de messages de notification à traiter.

  • +
  • sarracenia.flowcb.gather.message.Message est une classe qui implémente la réception des messages +de notification à partir des flux de protocole de fil d’attente de messages.

  • +
  • sarracenia.flowcb.nodupe.NoDupe Ce module supprime les doublons des flux de messages en fonction +des sommes de contrôle d’intégrité.

  • +
  • sarracenia.flowcb.post.message.Message est une classe qui implémente la publication de messages +de notification dans les flux de protocole de fil d’attente de messages

  • +
  • sarracenia.flowcb.retry.Retry lorsque le transfert d’un fichier échoue, Sarracenia doit conserver +le message de notification correspondant dans un fichier d’état pour une période ultérieure lorsqu’il +pourra être réessayé. Cette classe implémente cette fonctionnalité.

  • +
+
+
+

Modification de fichiers en transit

+

La classe sarracenia.transfer inclu un point d’entrée on_data qui permet de transformer +des données durant un transfer:

+
 def on_data(self, chunk) -> bytes:
+     """
+         transform data as it is being read.
+         Given a buffer, return the transformed buffer.
+         Checksum calculation is based on pre transformation... likely need
+         a post transformation value as well.
+     """
+     # modify the chunk in this body...
+     return chunk
+
+def registered_as():
+     return ['scr' ]
+
+# copied from sarracenia.transfer.https
+
+def connect(self):
+
+     if self.connected: self.close()
+
+     self.connected = False
+     self.sendTo = self.o.sendTo.replace('scr', 'https', 1)
+     self.timeout = self.o.timeout
+
+     if not self.credentials(): return False
+
+     return True
+
+
+

Pour effectuer la modification des données en vol, on peut sous-classer la classe de transfert pertinente. +Une telle classe (scr - strip retour chariot) peut être ajoutée en mettant un import dans la configuration +dossier:

+
import scr.py
+
+
+

alors les messages où l’url de récupération est définie pour utiliser le schéma de récupération scr utiliseront ce +protocole de transfert personnalisé.

+
+
+

flots modifiés

+

Si aucun des composants intégrés ( poll, post, sarra, shovel, subscribe, watch, winnow ) n’a le +comportement souhaité, on peut créer un composant personnalisé pour faire ce qu’il faut en sous-classant le flux.

+

Copiez l’une des sous-classes de sarracenia.flow à partir du code source et modifiez-la à votre goût. Dans la configuration +fichier, ajoutez la ligne

+
flowMain MaComposant
+
+
+

pour que le flux utilise le nouveau composant.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html b/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html new file mode 100644 index 000000000..83fb07ae5 --- /dev/null +++ b/fr/CommentFaire/Ingestion_de_email_avec_Sarracenia.html @@ -0,0 +1,347 @@ + + + + + + + Ingestion par e-mail avec Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Ingestion par e-mail avec Sarracenia

+

Le courrier électronique est un moyen facile d’acheminer les données entre les serveurs. Utilisation du protocole POP3 (Post Office Protocol) et +Protocole IMAP (Internet Message Access Protocol), les fichiers e-mail peuvent être diffusés via Sarracenia +en étendant les fonctions du poll et de téléchargement.

+
+

Polling

+
+

Extending Polling Protocols

+

De série, Sarracenia support les pollUrl pour sonder des serveurs avec les protocoles HTTP/HTTPS et SFTP/FTP. +D’autres protocoles peuvent être pris en charge en sous-classant la classe sarracenia.flowcb.poll.Poll. +Heureusement, il existe un plugin de poll par courrier, qui invoque un plugin. +commencez par énumérer les exemples disponibles

+
fractal% sr3 list examples | grep poll
+cpump/cno_trouble_f00.inc        poll/airnow.conf
+poll/aws-nexrad.conf             poll/mail.conf
+poll/nasa-mls-nrt.conf           poll/noaa.conf
+poll/soapshc.conf                poll/usgs.conf
+fractal%
+
+
+

Ajouter la configuration:

+
fractal% sr3 add poll/mail.conf
+add: 2022-03-10 15:59:48,266 2785187 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/poll/mail.conf to /home/peter/.config/sr3/poll/mail.conf
+fractal%
+
+
+

Qu’avons-nous obtenu?:

+
fractal% cat ~/.config/sr3/poll/mail.conf
+#
+# Sample poll config, used to advertise availability of new emails using either POP3/IMAP protocols.
+# To use, make sure rabbitmq is running as described in the Dev.rst documentation,
+# and a tsource user/xs_tsource exchange exist, with FLOWBROKER set to the hostname
+# rabbitmq is running on (e.g. export FLOWBROKER='localhost')
+#
+# The pollUrl is in RFC 1738 format, e.g. <scheme>://<user>@<host>:<port>/ where your full credentials,
+# <scheme>://<user>:<password>@<host>:<port>/ would be contained in your ~/.config/sarra/credentials.conf.
+# Valid schemes are pop/pops/imap/imaps, where the s denotes an SSL connection. If a port isn't
+# specified, the default port associated with the scheme will be used (IMAPS -> 993, POPS -> 995,
+# IMAP -> 143, POP -> 110).
+#
+
+post_broker amqp://tsource@${FLOWBROKER}
+post_exchange xs_tsource
+
+scheduled_interval 60
+
+pollUrl <scheme>://<user>@<host>:<port>/
+
+callback poll.mail
+
+fractal%
+
+
+

Maintenant, lorsque l’instance de poll est démarrée avec ce plugin,

+
+
+

Mise en œuvre de POP/IMAP

+

Avec les modules poplib et imaplib de Python, la pollUrl peut être analysée et le serveur de messagerie +peut être connecté selon le schéma spécifié. Sarracenia peut extraire les informations d’identification de la pollUrl +grâce à ses classes intégrées, aucun mot de passe n’a donc besoin d’être stocké dans le fichier de configuration pour se connecter. Pop3 +utilise un indicateur de lecture interne pour déterminer si un message a été vu ou non. Si un message n’est pas lu, après +en le récupérant avec POP3, il sera marqué comme lu et il ne sera pas repris sur d’autres poll. +POP3 offre d’autres options comme la suppression du fichier après sa lecture, mais IMAP offre plus d’options de gestion +de courrier telles que le déplacement entre les dossiers et la génération de requêtes plus complexes. IMAP permet également +à plusieurs clients de se connecter à une boîte aux lettres en même temps, et prend en charge les indicateurs de message de suivi tels que +si le message est lu/non lu, répondu/pas encore répondu, ou supprimé/toujours dans la boîte de réception. L’exemple +de plugin de poll, +poll_email_ingest.py +récupère uniquement les e-mails non lus dans la boîte de réception et les marque comme non lus après les avoir récupérés, dans les deux cas suivants : +Versions POP et IMAP. Ce paramètre peut être facilement modifié selon les intentions de l’utilisateur final. S’il y a +s’il y a de nouveaux messages de la dernière fois qu’un client POP/IMAP s’est connecté, il publiera alors le fichier +en fonction de l’objet et d’un horodatage, où une instance sr_subscribe peut recevoir le message publié, +connectez-vous individuellement au serveur et téléchargez le message pour le sortir dans un fichier localement. Un exemple +de configuration qui a été inclus sous exemples en tant que +pollingest.conf. +Une fois que vous avez modifié/fourni les variables d’environnement requises pour le +config pour travailler, ouvrir un nouveau terminal et exécuter

+
[aspymap:~]$ sr_poll foreground pollingest.conf
+
+
+

Si les informations d’identification ont été incluses correctement et que toutes les variables ont été définies, la sortie doit ressembler à ceci

+
[aspymap:~/sarra_test_output]$ sr_poll foreground pollingest.conf
+2018-10-03 15:24:58,611 [INFO] poll_email_ingest init
+2018-10-03 15:24:58,617 [INFO] sr_poll pollingest startup
+2018-10-03 15:24:58,617 [INFO] log settings start for sr_poll (version: 2.18.07b3):
+2018-10-03 15:24:58,617 [INFO]  inflight=unspecified events=create|delete|link|modify use_pika=False
+2018-10-03 15:24:58,617 [INFO]  suppress_duplicates=1200 retry_mode=True retry_ttl=Nonems
+2018-10-03 15:24:58,617 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-10-03 15:24:58,617 [INFO]  heartbeat=300 default_mode=400 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-10-03 15:24:58,617 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-10-03 15:24:58,617 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-10-03 15:24:58,617 [INFO]  post_base_dir=None post_base_url=pops://dfsghfgsdfg24@hotmail.com@outlook.office365.com:995/ sum=z,d blocksize=209715200
+2018-10-03 15:24:58,617 [INFO]  Plugins configured:
+2018-10-03 15:24:58,617 [INFO]          on_line: Line_Mode
+2018-10-03 15:24:58,617 [INFO]          on_html_page: Html_parser
+2018-10-03 15:24:58,617 [INFO]          do_poll: Fetcher
+2018-10-03 15:24:58,617 [INFO]          on_message:
+2018-10-03 15:24:58,617 [INFO]          on_part:
+2018-10-03 15:24:58,618 [INFO]          on_file: File_Log
+2018-10-03 15:24:58,618 [INFO]          on_post: Post_Log
+2018-10-03 15:24:58,618 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse
+2018-10-03 15:24:58,618 [INFO]          on_report:
+2018-10-03 15:24:58,618 [INFO]          on_start:
+2018-10-03 15:24:58,618 [INFO]          on_stop:
+2018-10-03 15:24:58,618 [INFO] log_settings end.
+2018-10-03 15:24:58,621 [INFO] Output AMQP broker(localhost) user(tsource) vhost(/)
+2018-10-03 15:24:58,621 [INFO] Output AMQP exchange(xs_tsource)
+2018-10-03 15:24:58,621 [INFO] declaring exchange xs_tsource (tsource@localhost)
+2018-10-03 15:24:59,452 [INFO] post_log notice=20181003192459.452392 pops://dfsghfgsdfg24@hotmail.com@outlook.office365.com:995/ sarra%20demo20181003_15241538594699_452125 headers={'parts': '1,1,1,0,0', 'sum': 'z,d', 'from_cluster': 'localhost', 'to_clusters': 'ALL'}
+^C2018-10-03 15:25:00,355 [INFO] signal stop (SIGINT)
+2018-10-03 15:25:00,355 [INFO] sr_poll stop
+
+
+
+
+
+

Téléchargement

+

Les messages électroniques, une fois récupérés, sont formatés au format MIME (Multipurpose Internet Mail Extensions) 1.0 brut, +comme indiqué dans le premier en-tête du fichier. Les métadonnées de l’e-mail sont transmises dans une série d’en-têtes, dont une +par ligne, dans le format nom:valeur. Cela peut être analysé pour les pièces jointes, les corps de message, les méthodes de codage, etc. Un +do_download plugin peut implémenter la récupération du message à sortir dans un fichier en enregistrant le protocole +dans un module séparé, comme dans le plugin do_poll. Une fois qu’un message est reçu avec l’utilisateur/hôte +publié, il peut ensuite se connecter au serveur de messagerie en utilisant la destination et les informations d’identification spécifiées +dans ~/.config/sarra/credentials.conf et récupére le message localement. Un exemple de plugin qui fait cela +peut être trouvé sous plugins comme +download_email_ingest.py.

+
+

Décodage du contenu

+

Une fois le message électronique téléchargé, un plug-in on_file peut analyser le fichier au format MIME et extraire +la pièce jointe, généralement indiquée par l’en-tête Content-Disposition ou les champs corps/objet/adresse du message, à enregistrer en tant que +nouveau fichier pour affiner davantage les données. Un exemple de plugin qui fait cela peut être trouvé sous plugins comme +file_email_decode.py. +Un exemple de configuration incorporant ce type de traitement de fichiers est inclus sous exemples en tant que +downloademail.conf. +Une fois que les variables d’environnement ont été fournies et que le serveur rabbitmq est correctement configuré, ouvrez un nouveau +terminal et exécutez:

+
[aspymap~]$ sr_subscribe foreground downloademail.conf
+
+
+

Si tout a été fourni correctement, la sortie devrait ressembler à ceci:

+
[aspymap:~/sarra_output_test]$ sr_subscribe foreground downloademail.conf
+2018-10-03 15:24:57,153 [INFO] download_email_ingest init
+2018-10-03 15:24:57,159 [INFO] sr_subscribe downloademail start
+2018-10-03 15:24:57,159 [INFO] log settings start for sr_subscribe (version: 2.18.07b3):
+2018-10-03 15:24:57,159 [INFO]  inflight=.tmp events=create|delete|link|modify use_pika=False
+2018-10-03 15:24:57,159 [INFO]  suppress_duplicates=False retry_mode=True retry_ttl=300000ms
+2018-10-03 15:24:57,159 [INFO]  expire=300000ms reset=False message_ttl=None prefetch=25 accept_unmatch=False delete=False
+2018-10-03 15:24:57,159 [INFO]  heartbeat=300 default_mode=000 default_mode_dir=775 default_mode_log=600 discard=False durable=True
+2018-10-03 15:24:57,159 [INFO]  preserve_mode=True preserve_time=True realpathPost=False base_dir=None follow_symlinks=False
+2018-10-03 15:24:57,159 [INFO]  mirror=False flatten=/ realpathPost=False strip=0 base_dir=None report=True
+2018-10-03 15:24:57,159 [INFO]  Plugins configured:
+2018-10-03 15:24:57,159 [INFO]          do_download: Fetcher
+2018-10-03 15:24:57,159 [INFO]          do_get     :
+2018-10-03 15:24:57,159 [INFO]          on_message:
+2018-10-03 15:24:57,159 [INFO]          on_part:
+2018-10-03 15:24:57,159 [INFO]          on_file: File_Log Decoder
+2018-10-03 15:24:57,159 [INFO]          on_post: Post_Log
+2018-10-03 15:24:57,159 [INFO]          on_heartbeat: Hb_Log Hb_Memory Hb_Pulse RETRY
+2018-10-03 15:24:57,159 [INFO]          on_report:
+2018-10-03 15:24:57,159 [INFO]          on_start:
+2018-10-03 15:24:57,159 [INFO]          on_stop:
+2018-10-03 15:24:57,159 [INFO] log_settings end.
+2018-10-03 15:24:57,159 [INFO] sr_subscribe run
+2018-10-03 15:24:57,160 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-10-03 15:24:57,164 [INFO] Binding queue q_tsource.sr_subscribe.downloademail.64168876.31529683 with key v02.post.# from exchange xs_tsource on broker amqp://tsource@localhost/
+2018-10-03 15:24:57,166 [INFO] reading from to tsource@localhost, exchange: xs_tsource
+2018-10-03 15:24:57,167 [INFO] report to tsource@localhost, exchange: xs_tsource
+2018-10-03 15:24:57,167 [INFO] sr_retry on_heartbeat
+2018-10-03 15:24:57,172 [INFO] No retry in list
+2018-10-03 15:24:57,173 [INFO] sr_retry on_heartbeat elapse 0.006333
+2018-10-03 15:25:00,497 [INFO] download_email_ingest downloaded file: /home/ib/dads/map/.cache/sarra/sarra_doc_test/sarra demo20181003_15241538594699_452125
+2018-10-03 15:25:00,500 [INFO] file_log downloaded to: /home/ib/dads/map/.cache/sarra/sarra_doc_test/sarra demo20181003_15241538594699_452125
+^C2018-10-03 15:25:03,675 [INFO] signal stop (SIGINT)
+2018-10-03 15:25:03,675 [INFO] sr_subscribe stop
+
+
+
+
+
+

Cas d’utilisation

+

Les plugins d’ingestion d’e-mails ont été développés pour le cas d’utilisation des données en rafale courte, où l’information +arrive dans les pièces jointes des messages. Auparavant, les e-mails étaient téléchargés avec un script fetchmail et un +cronjob s’exécuterait de temps en temps pour détecter et décoder les nouveaux fichiers et leurs pièces jointes, +à utiliser pour un traitement ultérieur des données. Sarracenia prend désormais en charge toutes les étapes du routage des données, +et permet à ce processus d’être plus parallélisable.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/MiseANiveau.html b/fr/CommentFaire/MiseANiveau.html new file mode 100644 index 000000000..f730a2883 --- /dev/null +++ b/fr/CommentFaire/MiseANiveau.html @@ -0,0 +1,771 @@ + + + + + + + GUIDE DE MISE A NIVEAU — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

GUIDE DE MISE A NIVEAU

+

Ce dossier documente les changements de comportement afin de fournir des conseils aux personnes qui effectuent la mise à niveau +à partir d’une version précédente. Les sections sont intitulées pour indiquer les changements nécessaires lorsqu’il y a +une mise à niveau vers cette version. Pour effectuer une mise à niveau sur plusieurs versions, il faut démarrer +à la version après celle installée, et tenir compte de toutes les notifications pour la version intérim. +Bien que la stabilité du langage de configuration soit importante, à l’occasion, les changements ne peuvent +être évités. Ce fichier ne documente pas les nouveaux, mais uniquement les modifications qui posent problème lors des +mises à niveau. Les avis prennent la forme :

+
+
CHANGEMENT

Indique où les fichiers de configuration doivent être modifiés pour obtenir le même comportement qu’avant la publication.

+
+
ACTION

Indique une activité de maintenance requise dans le cadre d’un processus de mise à niveau.

+
+
BUG

Indique un bug grave pour indiquer que le déploiement de cette version n’est pas recommandé.

+
+
NOTICE

Un changement de comportement qui sera perceptible lors de la mise à niveau, mais qui n’est pas préoccupant.

+
+
SHOULD

Indique les interventions recommandées qui sont recommandées, mais pas obligatoires. Si l’activité prescrite n’est pas effectuée, +la conséquence est soit une ligne de configuration qui n’a aucun effet (gaspillage), soit l’application +qui peut générer des messages.

+
+
+

Les sections sont intitulées par les changements intervenus au niveau en question.

+
+

Instructions d’installation

+

Installation Guide

+
+
+

git

+
+
+

3.0.54

+

CHANGEMENT : sr3 sanity redémarre uniquement les instances manquantes, pas celles arrêtées. +cela est considéré comme plus conforme aux attentes des analystes.

+
+
+

3.0.53

+

CHANGEMENT : l’option directory dans le sondage ne sera plus convertie en path silencieusement. +Utilisez path explicitement à la place. Il est toujours converti lors de la mise à niveau depuis la v2 avec +sr3 convert, mais dans les configurations v3, directory agit désormais comme dans tous les autres +composants comme spécificateur de chemin de destination de téléchargement.

+
+
+

3.0.52

+

CHANGEMENT : argument messageCountMax supplémentaire au point d’entrée flowcb.gather(). +lors de la mise en œuvre de rappels de flow pour les flux cédulés ou de remplacements d’poll, le +le point d’entrée de gather prend désormais un argument supplémentaire indiquant le maximum +nombre de messages que la routine doit retourner.

+

Pour être compatible avec les versions précédentes, on peut établir une valeur par défaut +sur le rassemblement

+
def gather(self, messageMaxCount=None) :
+
+
+

Avec la valeur par défaut, les plugins sont compatible avec les version précédentes.

+
+
+

3.0.51

+

CHANGEMENT : nouveau paramètre obligatoire action pour la routine sarracenia.config.one_config() +indiquant comment la configuration sera utilisée. Lorsqu’il est utilisé pour des opérations en lecture +seule (statut, show, dump) la configuration doit éviter de remplir des valeurs qui ne +devraient être définit lorsqu’il est utilisé. Les exemples ont été mis à jour en conséquence.

+

CHANGEMENT : paramètre action désormais obligatoire pour sarracenia.config.finalize().

+
+
+

3.0.47

+

CHANGEMENT: les options de substitution temporelles, qui reflète +les options des routines strftime de python ont un élément rajouté: +un décalage (anglais: Offset?) pour decaler le moment d´un étampe temporelle +dans le temps. en v2 on exprimait, par exemple:

+
    +
  • ${YYYYMMDD-70m}

  • +
+

Pour donner l´année, mois jour, il y 70 minutes dans le passé. +En 3.0.47, on exprime cela avec:

+
    +
  • %{%o-70m%Y%m%d}

  • +
+

CHANGEMENT: le valeur de défaut pour l´option filename est maintenant +None, au lieu de ‘WHATFN’. Ceci réduit la compatibilité avec Sundew +mais l´augmente avec Sarra v2, et minimise l´étonnement de ceux qui ne +connaissent pas Sundew (réduction de comportement inattendu)

+

CHANGEMENT:

+
+
+

3.0.45

+
+
CHANGEMENT: l´unité dans l´option logRotateInterval est rendu

secondes, comme toute autre intervalle dans la configuration. +Dans les versions antérieurs, c´était une quantité de jours.

+
+
+
+
+

3.0.41

+

CHANGEMENT: champs de message v03 renommé: “integrity” est devenu “identity”

+
+
    +
  • version actuel va accepter est convertir les anciens messages.

  • +
  • version actuel va publier le nouveau champ et est donc incompatible avec toute version antérieur.

  • +
  • https://github.com/MetPX/sarracenia/issues/703

  • +
  • metpx-sr3c >= v3.23.06 (versio compatible de l´implantation en C)

  • +
  • metpx-sarracenia >= v2.23.06 (version v2 (ancien) compatible.)

  • +
+
+
+
+

3.0.40

+
+
CHANGEMENT: l´option post_format v02 est nécessaire pour que sr3 émet des

messages en format v02. Avant cette version, l´option post_topicPrefix v02.post +était suffisant. Avec la version actuel, les deux options doivent être spécifiés.

+
+
+

CHANGEMENT: l’interface de programmation (API) python a subit un changement de rupture

+
+

pour la classe sarracenia.moth, il faut maintenant specifier l’options[‘broker’] au lieu +de le fournir commen argument séparé.

+

avant:

+
    +
  • Moth(broker: url, options: dict, is_subsubscriber: bool) -> Config

  • +
  • pubFactory( broker, options ) -> Config

  • +
  • subFactory( broker, options ) -> Config

  • +
+

après:

+
    +
  • Moth( options: dict, is_subscribe: bool) -> Config

  • +
  • pubFactory( options ) -> Config

  • +
  • subFactory( options ) -> Config

  • +
+

sarracenia.config API:

+
+

Il est recommandé d´appeller sarracenia.config.finalize() +après avoir fourni les options. Celui-ci interpole et complète +les valeurs d´options pou qu´ils soient utilisable par les +composantes.

+
+
+
+
+

3.0.26

+
+
CHANGEMENTles options d’événement (logEvents et fileEvents) remplacent désormais la valeur précédente.

Avant ils etaient être uni (ou’d) avec la valeur précédente. Peut maintenant préfacer +les éléments set avec + pour obtenir le comportement précédent. +Également - est disponible pour supprimer un élément d’une option définie. +(la conversion sr3 préfixe maintenant les valeurs v2 avec +)

+
+
CHANGEMENTfileEvents, nouveaux événements présents mkdir, et rmdir, quelques ajustements

les paramètres fileEvents peuvent maintenant être requis.

+
+
+
+
+

3.0.25

+
+
CHANGEMENTla valeur par défaut pour acceptUnmatched est maintenant True pour tous les composants.

avant cette version, la valeur par défaut était False dans le composant subscribe +mais Vrai pour tous les autres.

+
+
+
+
+

3.0.22

+

CHANGEMENTMENT: option destination dans une configuration de poll est remplacé par pollUrl

+

CHANGEMENTMENT: option destination dans une configuration de sender est remplacé par sendTo

+

ACTION: remplacer les destination dans les configurations affectés (traité automatiquement +dans le cas de conversion à partir de v2.)

+
+
+

3.0.17

+
+
CHANGEMENT: La chaine de charactères “Vendor” est changé de “science.gc.ca” à “MetPX”.

Ce changement modifie le placement des fichiers sur la platteforme Windows.

+
+
+

CHANGEMENT: l´encodage des messages d´annonce v03 est changé: Identity est rendu optionnel.

+
+
CHANGEMENT: l’encodage des messages d’annonce v03 est changé: le champs fileOp est rajouté

pour séparer les operations sur des fichiers qui ne comprennent pas des transmissions +de données: créations de liens symboliques, renommage de fichier, suppression de fichiers. +Le champs Identity est maintenant dédié au sommes de contrôle pour les données.

+
+
+
+
+

3.0.15

+
+
NOTICE: rétablir les paquets Debian et Windows en supprimant les exigences matérielles pour les modules python

qui sont difficiles à satisfaire. À partir de la version 3.0.15, les dépendances sont modulaires.

+
+
+

CHANGEMENT: il y a maintenant quatre “extras” configurés pour les paquets pip pour metpx-sr3.

+
+
    +
  • amqp - capacité de communiquer avec les courtiers AMQP (rabbitmq)

  • +
  • mqtt - capacité à communiquer avec les courtiers MQTT

  • +
  • ftppoll - possibilité d’interroger les serveurs FTP

  • +
  • vip - activez les paramètres vip (Virtual IP) pour implémenter le traitement singleton pour la prise en charge de la haute disponibilité.

  • +
+

avec l’installation de pip, on peut inclure tous les extras via:

+
pip install metpx-sr3[amqp,mqtt,ftppoll,vip]
+
+
+

avec les paquets Linux, installez les paquets natifs correspondants pour activer les fonctionnalités correspondantes

+

sur Ubuntu, respectivement:

+
apt install python3-amqp
+apt install python3-paho-mqtt
+apt install python3-dateparser python3-tz
+apt install python3-netifaces
+
+
+

sr3 recherche les modules pertinents au démarrage et active automatiquement la prise en charge des fonctionnalités pertinentes.

+
+
+
CHANGEMENT: le placement des fichiers pour indiquer des configurations désactivées.

Avant, l’action disable ajoutait un .off au nom de fichier de configuration. +Maintenant, on crée à la place un fichier disabled dans le répertoire d’état. +Les fichiers de configuration ne sont plus modifié par la gestion routinière +d’activités.

+
+
+
+
+

3.0.14

+

bêta initiale.

+
+
NOTICEseuls les paquets pip fonctionnent actuellement. Pas de paquets Debian sur launchpad.net

ni aucun package Windows.

+
+
+
+
+

V2 to Sr3

+
+
NOTICE: Sr3 est un refacteur très profond de Sarracenia. Pour plus de détails sur la nature

des changements, allez ici Brièvement, où v2 +est une application écrite en python qui avait une petite installation d’extension, +Sr3 est une boîte à outils qui fournit naturellement une API et est beaucoup plus +pythonique. Sr3 est construit avec moins de code, plus de code maintenable, et +prend en charge plus de fonctionnalités, et plus naturellement.

+
+
CHANGEMENTles messages de journal sont complètement différents. Toute analyse des journaux devra être examinée.

Le nouveau format de journal inclut un préfixe avec un process-id et la routine générant le message.

+
+
NOTICELors de la migration de la v2 vers la sr3, les configurations simples “fonctionneront simplement”.

Cependant, les cas reposant sur des plugins construits par l’utilisateur nécessiteront des efforts de portage. +Les plugins intégrés fournis avec Sarracenia ont été portés comme des exemples.

+
+
CHANGEMENT: placement du fichier. Sous Linux~/.cache/sarra -> ~/.cache/sr3

~/.config/sarra -> ~/.config/sr3 +Changement similaire sur d’autres plateformes. Les différents placements +permettent d’exécuter v2 et sr3 en même temps sur le même serveur.

+
+
CHANGEMENT: L’interface de ligne de commande (CLI) est différente. Il n’y a qu’un seul entry_point principalsr3.

donc la plupart des invocations sont différentes dans un modèle comme ci-dessous:

+
sr_subscribe start config -> sr3 start subscribe/config
+
+
+

dans sr3, on peut spécifier une série de configurations sur lesquelles fonctionner avec une seule commande:

+
sr3 start poll/airnow subscribe/airnow sender/cmqb
+
+
+
+
CHANGEMENT: dans sr3, utilisez – pour les options de mots complets, comme –config ou –broker. Dans la v2, vous

pouvez utiliser -config et -broker, mais un tiret unique est réservé aux options à caractère unique. +Ceci est le résultat de sr3 utilisant la classe ArgParse standard python:

+
-config hoho.conf  -> in v2 refers to loading the hoho.conf file as a configuration.
+
+
+

Dans sr3, il sera interprété comme -c (config) charger le fichier config.conf, et hoho.conf +fait partie d’une option ultérieure. dans sr3:

+
--config hoho.conf
+
+
+

le fait comme prévu.

+
+
+

CHANGEMENT: En général, les traits de soulignement dans les options sont remplacés par camelCase. p. ex. :

+
+

v2 loglevel -> sr3 logLevel

+

Les options v2 qui sont renommées seront comprises, mais un message d’information sera produit au +démarrage. Le trait de soulignement est toujours utilisé à des fins de regroupement. Options qui ont changé :

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Option v2

Option v3

accel_scp_threshold

accelThreshold

accel_wget_threshold

accelThreshold

accept_unmatch

acceptUnmatched

accept_unmatched

acceptUnmatched

base_dir

baseDir

basedir

baseDir

baseurl

baseUrl

bind_queue

queueBind

cache

nodupe_ttl

cache_basis

nodupe_basis

caching

nodupe_ttl

chmod

permDefault

chmod_dir

permDirDefault

chmod_log

permLog

declare_exchange

exchangeDeclare

declare_queue

queueDeclare

default_dir_mode

permDirDefault

default_log_mode

permLog

default_mode

permDefault

document_root

documentRoot

e

fileEvents

events

fileEvents

exchange_split

exchangeSplit

file_time_limit

fileAgeMax

hb_memory_baseline_file

MemoryBaseLineFile

hb_memory_max

MemoryMax

hb_memory_multiplier

MemoryMultiplier

heartbeat

housekeeping

instance

instances

ll

logLevel

logRotate

logRotateCount

logRotate_interval

logRotateInterval

log_format

logFormat

log_reject

logReject

logdays

logRotateCount

loglevel

logLevel

no_duplicates

nodupe_ttl

post_base_dir

post_baseDir

post_base_url

post_baseUrl

post_basedir

post_baseDir

post_baseurl

post_baseUrl

post_document_root

post_documentRoot

post_exchange_split

post_exchangeSplit

post_rate_limit

messageRateMax

post_topic_prefix

post_topicPrefix

preserve_mode

permCopy

preserve_time

timeCopy

queue_name

queueName

report_back

report

source_from_exchange

sourceFromExchange

sum

identity

suppress_duplicates

nodupe_ttl

suppress_duplicates_basis

nodupe_basis

topic_prefix

topicPrefix

+
+
+
CHANGEMENTtopic_prefix v02.post par défaut -> topicPrefix v03

peut avoir besoin de modifier les configurations pour remplacer la valeur par défaut pour obtenir des +configurations compatibles.

+
+
CHANGEMENT: v2mirror a la valeur false sur tous les composants à l’exception de sarra.

sr3 : la valeur par défaut de mirror est True sur tous les composants, à l’exception de subscribe.

+
+
NOTICELes plugins v2 les plus courants sont on_message, et on_file

(selon les directives plugin et on_ dans les fichiers de configuration v2) qui peuvent +être honoré via la classe de plugin v2wrapper sr3 plugin class +De nombreux autres plugins ont été portés, et le module de configuration +reconnaît les anciens paramètres de configuration et ils sont interprétés +dans le nouveau style. les conversions connues peuvent être visualisées en démarrant +un interpréteur python

+
Python 3.8.10 (default, Nov 26 2021, 20:14:08)
+[GCC 9.3.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import sarracenia.config,pprint
+>>> pp=pprint.PrettyPrinter()
+>>> pp.pprint(sarracenia.config.convert_to_v3)
+{
+ 'do_send':   {
+                'file_email':           ['flowCallback',
+                                         'sarracenia.flowcb.send.email.Email']
+              },
+ 'ls_file_index':                       ['continue'],
+ 'no_download':                         ['download',
+                                         'False'],
+ 'notify_only':                         ['download',
+                                         'False'],
+
+ 'on_message':{
+                'msg_2http':            ['flow_callback',
+                                         'sarracenia.flowcb.accept.tohttp.ToHttp'],
+                'msg_2local':           ['flow_callback',
+                                         'sarracenia.flowcb.accept.tolocal.ToLocal'],
+                'msg_2localfile':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.tolocalfile.ToLocalFile'],
+                'msg_WMO_type_suffix':  ['flow_callback',
+                                         'sarracenia.flowcb.accept.wmotypesuffix.WmoTypeSuffix'],
+                'msg_by_source':        ['continue'],
+                'msg_by_user':          ['continue'],
+                'msg_delay':            ['flow_callback',
+                                         'sarracenia.flowcb.accept.messagedelay.MessageDelay'],
+                'msg_delete':           ['flow_callback',
+                                         'sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles'],
+                'msg_download':         ['continue'],
+                'msg_download_baseurl': ['flow_callback',
+                                         'sarracenia.flowcb.accept.downloadbaseurl.DownloadBaseUrl'],
+                'msg_dump':             ['continue'],
+                'msg_fdelay':           ['continue'],
+                'msg_from_cluster':     ['continue'],
+                'msg_gts2wistopic':     ['continue'],
+                'msg_hour_tree':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.hourtree.HourTree'],
+                'msg_http_to_https':    ['flow_callback',
+                                         'sarracenia.flowcb.accept.httptohttps.HttpToHttps'],
+                'msg_log':              ['logEvents',
+                                         'after_accept'],
+                'msg_overwrite_sum':    ['continue'],
+                'msg_print_lag':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.printlag.PrintLag'],
+                'msg_rawlog':           ['logEvents', 'after_accept'],
+                'msg_rename4jicc':      ['flow_callback',
+                                         'sarracenia.flowcb.accept.rename4jicc.Rename4Jicc'],
+                'msg_rename_dmf':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamedmf.RenameDMF'],
+                'msg_rename_whatfn':    ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamewhatfn.RenameWhatFn'],
+                'msg_renamer':          ['flow_callback',
+                                         'sarracenia.flowcb.accept.renamer.Renamer'],
+                'msg_save':             ['flow_callback',
+                                         'sarracenia.flowcb.accept.save.Save'],
+                'msg_skip_old':         ['flow_callback',
+                                         'sarracenia.flowcb.accept.skipold.SkipOld'],
+                'msg_speedo':           ['flow_callback',
+                                         'sarracenia.flowcb.accept.speedo.Speedo'],
+                'msg_stdfiles':         ['continue'],
+                'msg_stopper':          ['continue'],
+                'msg_sundew_pxroute':   ['flow_callback',
+                                         'sarracenia.flowcb.accept.sundewpxroute.SundewPxRoute'],
+                'msg_test_retry':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.testretry.TestRetry'],
+                'msg_to_clusters':      ['flow_callback',
+                                         'sarracenia.flowcb.accept.toclusters.ToClusters'],
+                'msg_total':            ['continue'],
+                'msg_total_save':       ['continue'],
+                'post_hour_tree':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.posthourtree.PostHourTree'],
+                'post_long_flow':       ['flow_callback',
+                                         'sarracenia.flowcb.accept.longflow.LongFLow'],
+                'post_override':        ['flow_callback',
+                                         'sarracenia.flowcb.accept.postoverride.PostOverride'],
+                'post_total':           ['continue'],
+                'post_total_save':      ['continue'],
+                'wmo2msc':              ['flow_callback',
+                                         'sarracenia.flowcb.filter.wmo2msc.Wmo2Msc']
+               },
+ 'on_post':    {
+                'post_log':             ['logEvents', 'after_work']
+               },
+ 'plugin':     {
+                'accel_scp':            ['continue'],
+                'accel_wget':           ['continue'],
+                'msg_fdelay':           ['flowCallback',
+                                         'sarracenia.flowcb.filter.fdelay.FDelay'],
+                'msg_pclean_f90':       ['flowCallback',
+                                         'sarracenia.flowcb.filter.pclean_f90.PClean_F90'],
+                'msg_pclean_f92':       ['flowCallback',
+                                         'sarracenia.flowcb.filter.pclean_f92.PClean_F92']
+               },
+ 'windows_run':                         ['continue'],
+ 'xattr_disable':                       ['continue']
+}
+>>>
+
+
+

Les options répertoriées comme “continuer” sont obsolètes, remplacées par le traitement par défaut ou rendues +inutile par des changements dans la mise en œuvre.

+
+
NOTICEpour les utilisateurs d’API et les rédacteurs de plugins, le format de plugin v2 est entièrement remplacé par

la classe Flow Callback. La nouvelle fonctionnalité de plugin +peut principalement être implémenté sous forme de plugins.

+
+
CHANGEMENT: les plugins do_poll v2 doivent être remplacés par une sous-classification pour poll

Exemple dans plugin porting

+
+
+

CHANGEMENT: Les plugins on_html_page v2 sont également remplacés par la sous-classification poll

+

CHANGEMENT: v2 do_send remplacé par send entrypoint dans un plugin Flowcb plugin portage

+
+
NOTICEles plugins d’accélérateur v2 sont remplacés par l’accélérateur intégré.

accel_wget_command, accel_scp_command, accel_ftpget_command, accel_ftpput_command, +accel_scp_command, sont maintenant des options intégrées utilisées par la classe +Transfer. +L’ajout de nouveaux protocoles de transfert se fait en sous-classant Transfer.

+
+
+

SHOULD: v2 on_message -> after_accept doit être réécrit portage de plugin

+

SHOULD: v2 on_file -> after_work devrait être réécrit portage de plugin

+
+
SHOULDles plugins v2 doivent être réécrits. portage de plugin

il existe de nombreux plugins intégrés qui sont portés et automatiquement +convertis, mais les externes doivent être réécrits.

+

Cependant, cette compatibilité a des conséquences sur les performances, de sorte qu’un trafic élevé +de flux s’exécuteront avec moins de charge cpu et mémoire si les plugins sont portés sur sr3. +Pour créer des plugins sr3 natifs, il faut étudier la classe flowCallback (flowcb).

+
+
+

CHANGEMENT: on_watch plugins devient entry_point un point d’entrée after_accept sr3 dans un flowcb dans un watch.

+
+
ACTIONLe composant sr_audit a disparu. Remplacé par l’exécution de sr sanity en tant que cron

(ou tâche planifiée sous Windows) pour s’assurer que les processus nécessaires continuent de s’exécuter.

+
+
CHANGEMENTparamètres obsolètesuse_amqplib, use_pika. Le nouveau sarracenia.moth.amqp

utilise la bibliothèque amqp. Pour utiliser d’autres bibliothèques, il faut créer de nouvelles sous-classes de sarracenia.moth.

+
+
CHANGEMENT: statehost est maintenant un indicateur booléen, l’option fqdn n’est plus implémentée.

s’il s’agit d’un problème, soumettez un problème. Ce n’est tout simplement pas considéré comme intéressant pour l’instant.

+
+
CHANGEMENTMENT: sr_retry est devenu retry.py.

Tous les plugins accédant aux structures internes de sr_retry.py doivent être réécrits. +Cet accès n’est plus nécessaire, car l’API définit comment mettre des messages sur +la fil d’attente de nouvelle tentative (déplacer les messages vers worklist.failed. )

+
+
CHANGEMENTle watch sr3, avec l’option force_polling, est beaucoup moins efficace

sur sr3 que v2 pour les grandes arborescences de répertoires (voir numéro #403 ) +Idéalement, on n’utilise pas du tout force_polling.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/index.html b/fr/CommentFaire/index.html new file mode 100644 index 000000000..083da9329 --- /dev/null +++ b/fr/CommentFaire/index.html @@ -0,0 +1,261 @@ + + + + + + + Comment Faire — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Comment Faire

+
+

Contents:

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/source.html b/fr/CommentFaire/source.html new file mode 100644 index 000000000..39ebb06c6 --- /dev/null +++ b/fr/CommentFaire/source.html @@ -0,0 +1,644 @@ + + + + + + + Sources de données — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sources de données

+
+

Injection de données dans un réseau de pompes MetPX-Sarracenia

+
+

Warning

+

FIXME : Les sections manquantes sont mises en surbrillance par FIXME. +Pas vraiment prêt à l’emploi, il en manque trop pour l’instant.

+
+
+

Note

+

FIXME: éléments manquants connus: bonne discussion sur le choix de la somme de contrôle. +Mise en garde sur les stratégies de mise à jour des fichiers. Cas d’utilisation d’un fichier constamment mis à jour, +plutôt que d’émettre de nouveaux fichiers.)

+
+
+

Enregistrement de révision

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+

Une pompe de données Sarracenia est un serveur Web (ou sftp) avec des notifications pour que les +abonnés sachent rapidement quand de nouvelles données sont arrivées. Pour savoir quelles données sont déjà disponibles +sur une pompe, affichez l’arborescence avec un navigateur Web. Pour des besoins immédiats simples, on peut +télécharger des données à l’aide du navigateur lui-même ou d’un outil standard tel que wget. +L’intention habituelle est que sr_subscribe télécharge automatiquement les données +voulu d’un répertoire sur une machine d’abonné où d’autres logiciels +peuvent les traiter. Notez que ce manuel utilise des abonnements pour tester +l’injection de données, de sorte que le guide de l’abonné doit probablement être lu avant +celui-ci.

+

Quelle que soit la façon dont cela est fait, injecter des données signifie indiquer à la pompe où les données sont +de sorte qu’elles puissent être acheminé vers/par la pompe. Cela peut être fait par l’un ou l’autre +à l’aide de la commande active et explicite sr_post, ou simplement à l’aide de sr3_watch sur un répertoire. +Lorsqu’il y a un grand nombre de fichiers et/ou des contraintes de rapidité serrées, l’appel +de sr3_post directement par le producteur du dossier est optimale, par ce que sr3_watch peut fournir de +performances décevantes. Une autre approche explicite, mais à basse fréquence, est la +commande sr_poll, qui permet d’interroger des systèmes distants pour extraire des données +dans le réseau efficacement.

+

Alors que sr3_watch est écrit comme un système de surveillance de répertoire optimal, il n’y a tout simplement pas de +moyen rapide de regarder des arborescences de répertoires volumineuses (disons, plus de 100 000 fichiers). Sur +dd.weather.gc.ca, par exemple, il y a 60 millions de fichiers dans environ un million de répertoires. +Parcourir cette arborescence de répertoires une fois prend plusieurs heures. Pour rechercher de nouveaux fichiers, +la meilleure résolution temporelle est toutes les quelques heures (disons 3). Donc en moyenne une notification +se produira 1,5 heure après l’affichage du fichier. En utilisant I_NOTIFY (sur Linux), cela +prend plusieurs heures pour démarrer, car il doit effectuer une promenade initiale dans l’arborescence des fichiers jusqu’à +configurer toutes les watches. Après cela, ce sera instantané, mais s’il y a trop de fichiers, +(et 60 millions, c’est très probablement trop) il va simplement planter et refuser de fonctionner. +Ce sont des limites inhérentes à la surveillance des répertoires, quelle que soit la façon dont cela se fait. +S’il est vraiment nécessaire de le faire, il y a de l’espoir. S’il vous plaît +consultez Quickly Announcing Very Large Trees On Linux

+

Avec sr_post, le programme qui place le fichier n’importe où dans l’arborescence arbitrairement profond[1]_, cela indique +à la pompe (qui indiquera aux abonnés) exactement où chercher. Il n’y a pas de limites de +système à craindre. C’est ainsi que fonctionne dd.weather.gc.ca, et les notifications sont inférieures à la seconde, avec +60 millions de fichiers sur le disque. Il est beaucoup plus efficace, en général, de faire des +notifications directe plutôt que de passer par l’indirection du système de fichiers, mais des cas plus +petite et plus simples, cela fait peu de différence pratique.

+

Dans le cas le plus simple, la pompe prend les données de votre compte, où que vous les ayez, +à condition que vous lui donniez la permission. Nous décrivons d’abord ce cas.

+
+

Note

+

Bien que l’arborescence de fichiers elle-même n’ait pas de limites en profondeur ou en nombre, la possibilité de +filtrer basé sur topics est limité par AMQP à 255 caractères. Donc la configuration de l’item subtopic +est limité à un peu moins que cela. Il n’y a pas delimite fixe +car les topics sont codées en utf8 qui est de longueur variable. Notez que le +directive subtopic vise à fournir une classification grossière, et +l’utilisation de accept/reject est destinée à un travail plus détaillé. Les clauses accept/reject +ne s’appuient pas sur les en-têtes AMQP, en utilisant des noms de chemin stockés dans le corps du +message de notifications, et ne sont donc pas affectés par cette limite.

+
+
+
+

Injection SFTP

+

L’utilisation directe de la commande sr_post(1) est le moyen le plus simple d’injecter des données +dans le réseau de pompes. Pour utiliser sr_post, vous devez savoir:

+
    +
  • le nom du courtier local: ( disons: ddsr.cmc.ec.gc.ca )

  • +
  • vos informations d’authentification pour ce broker ( disons: user=rnd : password=rndpw )

  • +
  • votre propre nom de serveur (disons: grumpy.cmc.ec.gc.ca )

  • +
  • votre propre nom d’utilisateur sur votre serveur (disons: peter)

  • +
+

Supposons que l’objectif soit que la pompe accède au compte de Peter via SFTP. Alors vous avez besoin +de prendre la clé publique de la pompe et la placer dans le fichier .ssh/authorized_keys de peter. +Sur le serveur que vous utilisez (grumpy), il faut faire quelque chose comme ceci:

+
cat pump.pub >>~peter/.ssh/authorized_keys
+
+
+

Cela permettra à la pompe d’accéder au compte de Peter sur grumpy en utilisant sa clé privée. +Donc, en supposant que l’on est connecté au compte de Peter sur grumpy, on peut stocker les +informations d’identification du courtier en toute sécurité

+
echo 'amqps://rnd:rndpw@ddsr.cmc.ec.gc.ca' >> ~/.config/sarra/credentials.conf:
+
+
+
+

Note

+

Les mots de passe sont toujours stockés dans le fichier credentials.conf. +pour éviter cela, il est préférable d’utiliser des clés, que Sarracenia peut trouver en +en regardant les fichiers de configuration ssh. Configurez ssh pour qu’il fonctionne, et Sarracenia +fonctionnera aussi.

+
+

Donc, maintenant, la ligne de commande pour sr3_post n’est que l’URL pour que ddsr récupère le +fichier sur grumpy:

+
sr3_post -post_broker amqp://guest:guest@localhost/ -post_base_dir /var/www/posts/ \
+-post_base_url http://localhost:81/frog.dna
+
+2016-01-20 14:53:49,014 [INFO] Output AMQP  broker(localhost) user(guest) vhost(/)
+2016-01-20 14:53:49,019 [INFO] message published :
+2016-01-20 14:53:49,019 [INFO] exchange xs_guest topic v02.post.frog.dna
+2016-01-20 14:53:49,019 [INFO] notice   20160120145349.19 http://localhost:81/ frog.dna
+2016-01-20 14:53:49,020 [INFO] headers  parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c to_clusters=localhost
+
+
+

Il y a un sr_subscribe qui s’abonne à tous les messages *.dna indiqués dans le journal d’abonnement. +Voici le fichier de configuration

+
broker amqp://guest:guest@localhost
+directory /var/www/subscribed
+subtopic #
+accept .*dna*
+
+
+

et voici la sortie associée du fichier journal d’abonnement:

+
2016-01-20 14:53:49,418 [INFO] Received notice  20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna
+2016-01-20 14:53:49,419 [INFO] downloading/copying into /var/www/subscribed/frog.dna
+2016-01-20 14:53:49,420 [INFO] Downloads: http://grumpy:80/20160120/guest/frog.dna  into /var/www/subscribed/frog.dna 0-16
+2016-01-20 14:53:49,424 [INFO] 201 Downloaded : v02.report.20160120.guest.frog.dna 20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna 201 sarra-server-trusty guest 0.404653 parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c from_cluster=test_cluster source=guest to_clusters=test_cluster rename=/var/www/subscribed/frog.dna message=Downloaded
+
+
+

Ou bien, voici le journal d’une instance sr_sarra

+
2016-01-20 14:53:49,376 [INFO] Received v02.post.frog.dna '20160120145349.19 http://grumpy:81/ frog.dna' parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c to_cluster=ddsr.cmc.ec.gc.ca
+2016-01-20 14:53:49,377 [INFO] downloading/copying into /var/www/test/20160120/guest/frog.dna
+2016-01-20 14:53:49,377 [INFO] Downloads: http://grumpy:81/frog.dna  into /var/www/test/20160120/guest/frog.dna 0-16
+2016-01-20 14:53:49,380 [INFO] 201 Downloaded : v02.report.frog.dna 20160120145349.19 http://grumpy:81/ frog.dna 201 sarra-server-trusty guest 0.360282 parts=1,16,1,0,0 sum=d,d108dcff28200e8d26d15d1b3dfeac1c from_cluster=test_cluster source=guest to_clusters=test_cluster message=Downloaded
+2016-01-20 14:53:49,381 [INFO] message published :
+2016-01-20 14:53:49,381 [INFO] exchange xpublic topic v02.post.20160120.guest.frog.dna
+2016-01-20 14:53:49,381 [INFO] notice   20160120145349.19 http://grumpy:80/ 20160120/guest/frog.dna
+@
+
+
+

La commande demande à ddsr de récupérer le fichier treefrog/frog.dna en se connectant +dans grumpy en tant que peter (en utilisant la clé privée de la pompe) pour le récupérer, et le poster +sur la pompe, pour l’acheminement vers les autres destinations de la pompe.

+

Semblable à sr_subscribe, on peut également placer des fichiers de configuration dans un répertoire spécifique sr_post:

+
blacklab% sr3_post edit dissem.conf
+
+post_broker amqps://rnd@ddsr.cmc.ec.gc.ca/
+post_base_url sftp://peter@grumpy
+
+
+

Et puis:

+
sr3_post -c dissem -url treefrog/frog.dna
+
+
+

S’il existe différentes variétés de publication utilisées, les configurations peuvent être enregistrées pour chacune d’elles.

+
+

Warning

+

FIXME: Besoin de faire un exemple réel. ce truc inventé n’est pas suffisamment utile.

+

FIXME: sr3_post n’accepte pas les fichiers de configuration pour le moment, indique la page de manuel. Vrai/Faux ?

+

sr3_post lignes de commande peuvent être beaucoup plus simples si c’était le cas.

+
+

sr_post revient généralement immédiatement car son seul travail est d’informer la pompe de la disponibilité +de fichiers. Les fichiers ne sont pas transférés lorsque sr3_post revient, il ne faut donc pas supprimer les fichiers +après avoir posté sans être sûr que la pompe les a réellement ramassés.

+
+

Note

+

sftp est peut-être le plus simple à implémenter et à comprendre pour l’utilisateur, mais il est aussi +le plus coûteux en termes de CPU sur le serveur. Tout le travail de transfert de données est +fait au niveau de l’application python lorsque l’acquisition sftp est terminée, ce qui n’est pas génial.

+

Une version cpu inférieure serait pour le client d’envoyer d’une manière ou d’une autre (sftp?) et puis juste +indiquer où se trouve le fichier sur la pompe (essentiellement la version sr_sender2).

+
+

Notez que cet exemple utilise sftp, mais si le fichier est disponible sur un site Web local, +alors http fonctionnerait, ou si la pompe de données et le serveur source partagent un système de fichiers, +alors même une URL de fichier pourrait fonctionner.

+
+
+

Injection HTTP

+

Si nous prenons un cas similaire, mais dans ce cas, il y a un espace accessible http, +les étapes sont les mêmes ou même plus simples si aucune authentification n’est requise pour la pompe +pour acquérir les données. Il faut installer un serveur Web d’une sorte ou d’une autre.

+

Supposons une configuration qui affiche tous les fichiers sous /var/www sous forme de dossiers, s’exécutant sous +les utilisateurs de www-data. Les données publiées dans ces répertoires doivent être lisibles pour l’utilisateur www-data +pour permettre au serveur Web de le lire. Le serveur exécutant le serveur Web +s’appelle blacklab, et l’utilisateur sur le serveur est peter s’exécutant comme peter sur blacklab, +un répertoire est créé sous /var/www/project/outgoing, accessible en écriture par peter, +ce qui se traduit par une configuration comme celle-ci

+
sr3_watch edit project.conf
+
+broker amqp://feeder@localhost/
+url http://blacklab/
+post_base_dir /var/www/project/outgoing
+
+
+

Ensuite, une watch est démarrée:

+
sr3_watch start project
+
+
+
+

Warning

+

FIXME : exemple réel.

+
+
FIXMEsr3_watch était censé prendre les fichiers de configuration, mais qui cela n’a peut-être pas

été modifié à cet effet.

+
+
+
+

Pendant l’exécution de sr_watch, chaque fois qu’un fichier est créé dans le répertoire document_root, +il sera annoncé à la pompe (sur localhost, c’est-à-dire le serveur blacklab lui-même).:

+
cp frog.dna  /var/www/project/outgoing
+
+
+
+

Warning

+

FIXME : exemple réel.

+
+

Cela déclenche un message à la pompe. Tous les abonnés pourront alors télécharger +le fichier.

+
+

Warning

+

FIXME. trop cassé pour l’instant pour vraiment l’éxécuter aussi facilement… +donc la création d’une vraie démo est différée.

+
+
+
+

Interrogation de sources externes

+

Certaines sources sont intrinsèquement éloignées, et nous sommes incapables de les intéresser ou de les affecter. +On peut configurer sr_poll pour extraire des données de sources externes, généralement des sites Web. +La commande sr_poll s’exécute généralement comme un singleton qui suit les nouveautés dans une arborescence de source +et crée des messages de notification de source à traiter par le réseau de pompes.

+

Les serveurs externes, en particulier les serveurs Web, ont souvent différentes façons de publier leur +listes de produits, de sorte que le traitement personnalisé de la liste est souvent nécessaire. C’est pourquoi sr_poll +a le paramètre do_poll, ce qui signifie que l’utilisation d’un script de plug-in est pratiquement requise +pour l’utiliser.

+
+

Note

+

voir les poll_script inclus dans le répertoire des plugins de package pour un exemple. +FIXME:

+
+
+
+

Messages de rapport

+

Si le sr3_post a fonctionné, cela signifie que la pompe a accepté de jeter un coup d’œil sur votre dossier. +Pour savoir où vont vos données par la suite, il faut examiner le fichiers de journalisation de la source. +Il est également important de noter que la pompe initiale, ou toute autre pompe +en aval, peut refuser de transmettre vos données pour diverses raisons, qui ne seront que +signalés à la source dans ces messages de rapport.

+

Pour afficher les messages du rapport source, la commande sr_report n’est qu’une version de sr_subscribe, avec +les mêmes options là où elles ont du sens. Si le fichier de configuration (~/.config/sarra/default.conf) +est configuré, alors tout ce qui est nécessaire est:

+
sr_report
+
+
+

Pour afficher les messages de rapport indiquant ce qui est arrivé aux éléments insérés dans le +réseau à partir de la même pompe utilisant ce compte (rnd, dans l’exemple). On peut déclencher +post-traitement arbitraire des messages de rapport à l’aide de plugins on_message.

+
+

Warning

+

FIXME: besoin de quelques exemples.

+
+
+
+

Fichiers volumineux

+

Les fichiers plus volumineux ne sont pas envoyés en tant que bloc unique. Ils sont envoyés en pièces et chaque pièce +a une empreinte digitale, de sorte que lorsque les fichiers sont mis à jour, les parties inchangées +ne pas pas envoyé à nouveau. Il existe un seuil par défaut intégré dans les commandes sr_, +au-dessus de duquels les messages de notification partitionnés seront effectués par défaut. Ce seuil peut +être ajusté au goût à l’aide de l’option part_threshold.

+

Différentes pompes le long du parcours peuvent avoir des tailles de pièces maximales différentes. Pour +parcourir un chemin donné, la pièce ne doit pas être plus grande que le paramètre de seuil +de toutes les pompes intermédiaires. Une pompe enverra à la source un journal des erreurs +s’il refuse de transférer un fichier.

+

Comme chaque partie est annoncée, il y a donc un message de rapport correspondant pour +chaque partie. Cela permet aux expéditeurs de surveiller la progression de la livraison de grands +fichiers.

+
+
+

Fiabilité et sommes de contrôle

+

Chaque donnée injectée dans le réseau de pompage doit avoir une empreinte digitale unique (ou somme de contrôle). +Les données circuleront si elles sont nouvelles, et déterminer si les données sont nouvelles est basé sur l’empreinte digitale. +Pour obtenir de la fiabilité dans un réseau sarracenia, plusieurs sources indépendantes sont provisionnées. +Chaque source annonce ses produits, et s’ils ont le même nom et la même empreinte digitale, alors +les produits sont considérés comme identiques.

+

Le composant sr_winnow de sarracenia examine les messages de notification entrants et note quels produits +sont reçus (par nom de fichier et somme de contrôle). Si un produit est nouveau, il est transmis à d’autres composants +pour le traitement. Si un produit est un doublon, le message de notification n’est plus transféré. +De même, lorsqu’un composant sr_subscribe ou sr_sarra reçoit un message de notification pour un produit qui est déjà +présent sur le système local, ils examineront l’empreinte digitale et ne téléchargeront pas les données à moins qu’elles ne soient différentes. +Les méthodes de somme de contrôle doivent être connues sur un réseau, car les composants en aval les réappliqueront.

+

Différents algorithmes d’empreintes digitales sont appropriés pour différents types de données, de sorte que +l’algorithme à appliquer doit être choisi par la source de données et non imposé par le réseau. +Normalement, l’algorithme ‘d’ est utilisé, qui applique le célèbre Message-Digest 5 (md5sum) +aux données du fichier.

+

Lorsqu’il y a une origine pour les données, cet algorithme fonctionne bien. Pour une haute disponibilité, +les chaînes de production fonctionneront en parallèle, de préférence sans communication entre +eux. Les articles produits par des chaînes indépendantes peuvent naturellement avoir un temps de traitement différent +et numéros de série différent appliqués, de sorte que les mêmes données traitées par +différentes chaînes ne seront pas identiques au niveau binaire. Pour les produits fabriqués +par différentes chaînes de production pour être acceptées comme équivalentes, elles doivent avoir +la même empreinte digitale.

+

Une solution pour ce cas est, si les deux chaînes de traitement produisent des données avec +le même nom, appliquer la somme de contrôle sur le nom du fichier au lieu des données, cela s’appelle ‘n’. +Dans de nombreux cas, les noms eux-mêmes dépendent de la chaîne de production, de sorte qu’une +algorithme est nécessaire. Si un algorithme personnalisé est choisi, elle doit être publié sur +le réseau:

+
http://dd.cmc.ec.gc.ca/config/msc-radar/sums/
+
+   u.py
+
+
+

Ainsi, les clients en aval peuvent obtenir et appliquer la même algorithme pour comparer les messages de notification +provenant de sources multiples.

+
+

Warning

+

FIXME: science-fiction encore: aucun répertoire de configuration de ce type n’existe encore. aucun moyen de les mettre à jour. +chemin de recherche pour les algos de somme de contrôle? intégré, à l’échelle du système, par source?

+

De plus, si chaque source définit son propre algorithme, elle doit choisir le même +(avec le même nom) afin d’avoir une correspondance.

+

FIXME : vérifiez que la vérification des empreintes digitales inclut la correspondance entre l’algorithme et la valeur.

+

FIXME: pas nécessaire au début, mais probablement à un moment donné. +en attendant, nous parlons simplement aux gens et incluons leurs algorithmes dans le package.

+
+
+

Note

+

Méthodes d’empreintes digitales basées sur le nom, plutôt que sur les données réelles, +entraînera la réexpédition de l’intégralité du fichier lorsqu’ils seront mis à jour.

+
+
+
+

En-têtes d’utilisateur

+

Que se passe-t-il s’il y a un élément de métadonnées qu’une source de données a choisi pour une raison quelconque de ne pas +inclure dans la hiérarchie des noms de fichiers ? Comment les consommateurs de données peuvent-ils connaître ces informations sans avoir +à télécharger le fichier afin de déterminer qu’il n’est pas intéressant. Un exemple serait les +avertissements météorologiques. Les noms de fichiers peuvent inclure des avertissements météorologiques pour un pays entier. Si les consommateurs +ne sont intéressés que par le téléchargement d’avertissements qui leur sont locaux, alors, une source de données pourrait +utilisez le hook on_post afin d’ajouter des en-têtes supplémentaires au message de notification.

+
+

Note

+

Une grande flexibilité s’accompagne d’un grand potentiel de préjudice. Les noms de chemin doivent inclure autant d’informations +que possible car sarracenia est construit pour optimiser le routage en les utilisant. Des métadonnées supplémentaires doivent être utilisées +pour compléter, plutôt que remplacer, le routage intégré.

+

Pour ajouter des en-têtes aux messages de notification en cours de publication, vous pouvez utiliser l’option d’en-tête. +Dans une configuration, ajoutez les instructions suivantes

+
header CAP_province=Ontario
+header CAP_area-desc=Uxbridge%20-%20Beaverton%20-%20Northern%20Durham%20Region
+header CAP_polygon=43.9984,-79.2175 43.9988,-79.219 44.2212,-79.3158 44.4664,-79.2343 44.5121,-79.1451 44.5135,-79.1415 44.5136,-79.1411 44.5137,-79.1407 44.5138,-79.14 44.5169,-79.0917 44.517,-79.0879 44.5169,-79.0823 44.218,-78.7659 44.0832,-78.7047 43.9984,-79.2175
+
+
+
+

Ainsi, lorsqu’un message de notification de fichier est publié, il inclura les en-têtes avec les valeurs données. +Cet exemple est artificiel parce qu’il affecte statiquement les valeurs d’en-tête appropriées +aux cas simples. Dans ce cas précis, il est probablement plus approprié de mettre en œuvre un +plugin on_post pour les fichiers Common Alerting Protocol pour extraire les informations d’en-tête ci-dessus et +les placer dans les en-têtes de message de notification pour chaque alerte.

+
+

Considérations relatives à l’efficacité

+

Il n’est pas recommandé de mettre une logique trop complexe dans les scripts du plugin, car ils s’exécutent de manière synchrone avec +les opérations post et receive. Notez que l’utilisation des installations intégrées d’AMQP (en-têtes) est faite pour +être explicitement aussi efficace que possible. À titre d’exemple extrême, inclure du code XML codé dans les messages de notification +n’affectera pas légèrement les performances, cela ralentira le traitement par ordre de grandeur. On ne sera pas +en mesure de compenser avec plusieurs instances, car la pénalité est tout simplement trop importante pour être surmontée.

+

Considérons, par exemple, les messages du Protocole d’alerte commun (CAP) pour les alertes météorologiques. Ces alertes +dépassent souvent 100 Ko, alors qu’un message de notification sarracenia est de l’ordre de 200 octets. Les messages de notification sarracenia +vont à beaucoup plus de destinataires que l’alerte : toute personne envisageant de télécharger une alerte, par opposition à ceux qui intéressent réellement l’abonné, +et ces métadonnées seront également incluses dans les messages du rapport, +et donc répliqués dans de nombreux autres endroits où les données elles-mêmes ne seront pas présentes.

+

Inclure toutes les informations contenues dans la PAC signifierait, juste en termes de transport, 500 fois +plus de capacité utilisée pour un seul message de notification. Lorsqu’il y a plusieurs millions de messages +de notification à transférer, cela s’additionne. +Seules les informations minimales requises par l’abonné pour prendre la décision de télécharger ou non devraient être +ajouter au message de notification. Il convient également de noter qu’en plus de ce qui précède, il y a généralement +10x à 100x plus de pénalité de processeur de mémoire en analysant une structure de données XML par rapport à la représentation en texte brut, qui +affectera le taux de traitement.

+
+
+
+
+
+

Quickly Announcing Very Large Trees On Linux

+

Pour mettre en miroir de très grands arbres (millions de fichiers) en temps réel, il faut trop de temps pour des outils comme rsync +ou trouvez pour parcourir et générer des listes de fichiers à copier. Sous Linux, on peut intercepter les appels pour des +opérations de fichiers en utilisant la technique bien connue de la bibliothèque de shim. Cette technique fournit virtuellement des +messages de notification en temps réel des fichiers quelle que soit la taille de l’arborescence, avec une surcharge minimale vu que +cette technique impose beaucoup moins de charge que les mécanismes de traversée des arbres et utilise +l’implémentation C de Sarracenia, qui utilise très peu de mémoire ou de ressources de processeur.

+

Pour utiliser cette technique, il faut avoir l’implémentation C de Sarracenia installée. Les bibliothèque +Libsrshim fait partie de ce package et l’environnement doit être configuré pour intercepter les appels +de la bibliothèque C comme suit:

+
export SR_POST_CONFIG=somepost.conf
+export LD_PRELOAD=libsrshim.so.1.0.0
+
+
+

somepost.conf est une configuration valide qui peut être testée avec sr3_post pour publier manuellement un fichier. +Tout processus appelé à partir d’un shell avec ces paramètres aura tous les appels à des routines telles que close(2) +intercepté par libsrshim. Libsrshim vérifiera si le fichier est en cours d’écriture, puis appliquera la configuration +somepost (les clauses accept/reject) et publiera le fichier si cela est approprié. +Exemple:

+
blacklab% more pyiotest
+f=open("hoho", "w+" )
+f.write("hello")
+f.close()
+blacklab%
+
+blacklab% more test2.sh
+
+echo "called with: $* "
+if [ ! "${LD_PRELOAD}" ]; then
+   export SR_POST_CONFIG=`pwd`/test_post.conf
+   export LD_PRELOAD=`pwd`/libsrshim.so.1.0.0
+   exec $0
+   #the exec here makes the LD_PRELOAD affect this shell, as well as sub-processes.
+fi
+
+set -x
+
+echo "FIXME: exec above fixes ... builtin i/o like redirection not being posted!"
+bash -c 'echo "hoho" >>~/test/hoho'
+
+/usr/bin/python2.7 pyiotest
+cp libsrshim.c ~/test/hoho_my_darling.txt
+
+blacklab%
+
+lacklab% ./test2.sh
+called with:
+called with:
++++ echo 'FIXME: exec above fixes ... builtin i/o like redirection not being posted!'
+FIXME: exec above fixes ... builtin i/o like redirection not being posted!
++++ bash -c 'echo "hoho" >>~/test/hoho'
+2017-10-21 20:20:44,092 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,092 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,092 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,094 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,095 [DEBUG] isMatchingPattern: /home/peter/test/hoho matched mask: accept .*
+2017-10-21 20:20:44,096 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,096 [DEBUG] sr3_post file2message called with: /home/peter/test/hoho sb=0x7ffef2aae2f0 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,096 [INFO] published: 20171021202044.096 sftp://peter@localhost /home/peter/test/hoho topic=v02.post.home.peter.test sum=s,a0bcb70b771de1f614c724a86169288ee9dc749a6c0bbb9dd0f863c2b66531d21b65b81bd3d3ec4e345c2fea59032a1b4f3fe52317da3bf075374f7b699b10aa source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202002.304 atime=20171021202002.308 mode=0644 parts=1,2,1,0,0
++++ /usr/bin/python2.7 pyiotest
+2017-10-21 20:20:44,105 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,105 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,105 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,107 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,107 [DEBUG] isMatchingPattern: /home/peter/src/sarracenia/c/hoho matched mask: accept .*
+2017-10-21 20:20:44,108 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,108 [DEBUG] sr3_post file2message called with: /home/peter/src/sarracenia/c/hoho sb=0x7ffeb02838b0 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,108 [INFO] published: 20171021202044.108 sftp://peter@localhost /c/hoho topic=v02.post.c sum=s,9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043 source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202044.101 atime=20171021202002.320 mode=0644 parts=1,5,1,0,0
++++ cp libsrshim.c /home/peter/test/hoho_my_darling.txt
+2017-10-21 20:20:44,112 [INFO] sr3_post settings: action=foreground log_level=1 follow_symlinks=no sleep=0 heartbeat=300 cache=0 cache_file=off
+2017-10-21 20:20:44,112 [DEBUG] setting to_cluster: localhost
+2017-10-21 20:20:44,112 [DEBUG] post_broker: amqp://tsource:<pw>@localhost:5672
+2017-10-21 20:20:44,114 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,114 [DEBUG] isMatchingPattern: /home/peter/test/hoho_my_darling.txt matched mask: accept .*
+2017-10-21 20:20:44,115 [DEBUG] connected to post broker amqp://tsource@localhost:5672/#xs_tsource_cpost_watch
+2017-10-21 20:20:44,115 [DEBUG] sr3_post file2message called with: /home/peter/test/hoho_my_darling.txt sb=0x7ffc8250d950 islnk=0, isdir=0, isreg=1
+2017-10-21 20:20:44,116 [INFO] published: 20171021202044.115 sftp://peter@localhost /home/peter/test/hoho_my_darling.txt topic=v02.post.home.peter.test sum=s,f5595a47339197c9e03e7b3c374d4f13e53e819b44f7f47b67bf1112e4bd6e01f2af2122e85eda5da633469dbfb0eaf2367314c32736ae8aa7819743f1772935 source=tsource to_clusters=localhost from_cluster=localhost mtime=20171021202044.109 atime=20171021202002.328 mode=0644 parts=1,15117,1,0,0
+blacklab%
+
+
+
+
Remarque::
+

redirection de fichier du i/o résultant des shell intégrés (pas de processus spawn) dans le shell où +les variables d’environnement sont d’abord définies NE SERONT PAS PUBLIÉES. seuls les sub-shells sont affectés:

+
# ne sera pas publié...
+echo "hoho" > kk.conf
+
+# sera publié.
+bash -c 'echo "hoho" > kk.conf'
+
+
+

Il s’agit d’une limitation de la technique, car l’ordre de chargement de la bibliothèque dynamique est résolu par le +processus de démarrage et ne peut pas être modifié par la suite. Une solution de contournement

+
if [ ! "${LD_PRELOAD}" ]; then
+  export SR_POST_CONFIG=`pwd`/test_post.conf
+  export LD_PRELOAD=`pwd`/libsrshim.so.1.0.0
+  exec $*
+fi
+
+
+
+

Ce qui activera la bibliothèque shim pour l’environnement appelant en la redémarrant. +Ce code particulier peut avoir un impact sur les options de ligne de commande et peut ne pas être directement applicable.

+
+
+

À titre d’exemple, nous avons un arbre de 22 millions de fichiers qui est écrit en continu jour et nuit. +Nous devons copier cette arborescence dans un deuxième système de fichiers le plus rapidement possible, +avec un temps de copie maximal ambitieux d’environ cinq minutes.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/subscriber.html b/fr/CommentFaire/subscriber.html new file mode 100644 index 000000000..9e987482d --- /dev/null +++ b/fr/CommentFaire/subscriber.html @@ -0,0 +1,1144 @@ + + + + + + + Guide de l’abonné — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Guide de l’abonné

+
+

Réception de données à partir d’une pompe de données MetPX-Sarracenia

+
+

Enregistrement de révision

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+
+

Introduction

+

Une pompe de données Sarracenia est un serveur Web avec des notifications +pour que les abonnés sachent rapidement, quand de nouvelles données sont arrivées. +Pour savoir quelles données sont déjà disponibles sur une pompe, il faut +afficher l’arborescence avec un navigateur Web. +Pour des besoins immédiats simples, on peut télécharger des données en utilisant le +navigateur lui-même, ou un outil standard tel que wget. +L’intention habituelle est que sr3 subscribe +télécharge automatiquement les données souhaitées dans un répertoire sur une +machine d’un abonné où d’autres logiciels peuvent les traiter. Veuillez noter:

+
    +
  • l’outil est entièrement piloté par ligne de commande (il n’y a pas d’interface graphique). Plus précisément, +il est principalement piloté par fichier de configuration. +La plupart de l’interface implique l’utilisation d’un éditeur de texte pour modifier les fichiers de configuration.

  • +
  • lorsqu’il est écrit pour être compatible avec d’autres environnements, +l’accent est mis sur l’utilisation de Linux.

  • +
  • l’outil peut être utilisé soit comme un outil d’utilisateur final, soit comme un moteur de transfert à l’échelle du système. +Ce guide est axé sur le cas de l’utilisateur final.

  • +
  • Des documents de référence plus détaillés sont disponibles à l’adresse suivante : +page de manuel sr3(1) traditionnelle,

  • +
  • Toute la documentation du paquet est disponible à https://metpx.github.io/sarracenia

  • +
+

Alors que Sarracenia peut fonctionner avec n’importe quel arbre Web, ou n’importe quelle URL +que les sources choisissent de poster, il y a une format conventionnelle. un exemple:

+
+
+

Le serveur Web d’une pompe de données exposera simplement des dossiers accessibles sur le Web +et la racine de l’arbre est la date, dans le format AAAAMMJJ. +Ces dates ne représentent rien sur les données autres que +quand elles ont été mis dans le réseau de pompage, et puisque Sarracenia +utilise toujours le temps coordonné universel, les dates peuvent ne pas correspondre à +la date/heure actuelle à l’emplacement de l’abonné:

+
Index of /
+
+Name                    Last modified      Size  Description
+Parent Directory                             -
+20151105/               2015-11-27 06:44    -
+20151106/               2015-11-27 06:44    -
+20151107/               2015-11-27 06:44    -
+20151108/               2015-11-27 06:44    -
+20151109/               2015-11-27 06:44    -
+20151110/               2015-11-27 06:44    -
+
+
+

Un nombre variable de jours sont stockés sur chaque pompe de données, et pour ceux qui mettent +l’accent sur une livraison fiable en temps réel, le nombre de jours sera plus court. +Pour les autres pompes, où les pannes à long terme doivent être tolérées, plus de jours +seront conservés.

+

Sous le premier niveau d’arbres de date, il y a un répertoire +par source. Une source dans Sarracenia est un compte utilisé pour injecter des +données dans le réseau de pompage. Les données peuvent traverser de nombreuses pompes sur son +chemin vers celles qui sont visibles:

+
Index of /20151110
+
+Name                    Last modified      Size  Description
+Parent Directory                             -
+UNIDATA-UCAR/           2015-11-27 06:44    -
+NOAAPORT/               2015-11-27 06:44    -
+MSC-CMC/                2015-11-27 06:44    -
+UKMET-RMDCN/            2015-11-27 06:44    -
+UKMET-Internet/         2015-11-27 06:44    -
+NWS-OPSNET/             2015-11-27 06:44    -
+
+
+

Les données sous chacun de ces répertoires ont été obtenues à partir de la +source. Dans ces exemples, il est en fait injecté par DataInterchange +et les noms sont choisis pour représenter l’origine des données.

+

Le dépôt de données original d’Environnement et Changement climatique Canada est +une “source” dans ce sens, apparaissant sur hpfx en tant que WXO-DD, ou le même +tree étant disponible à la racine de

+
https://dd.weather.gc.ca
+
+
+

Le dépôt de données original d’Environnement et Changement climatique Canada est +une “source” dans ce sens, apparaissant sur hpfx en tant que WXO-DD, ou le même +tree étant disponible à la racine de

+
Icon  Name                    Last modified      Size  Description
+[TXT] about_dd_apropos.txt    2021-05-17 13:23  1.0K
+[DIR] air_quality/            2020-12-10 14:47    -
+[DIR] alerts/                 2022-07-13 12:00    -
+[DIR] analysis/               2022-07-13 13:17    -
+[DIR] barometry/              2022-03-22 22:00    -
+[DIR] bulletins/              2022-07-13 13:19    -
+[DIR] citypage_weather/       2022-07-13 13:21    -
+[DIR] climate/                2020-09-03 16:30    -
+[DIR] doc/                    2022-09-28 20:00    -
+[DIR] ensemble/               2022-07-13 13:34    -
+[DIR] hydrometric/            2021-01-14 14:12    -
+[DIR] marine_weather/         2020-12-15 14:51    -
+[DIR] meteocode/              2022-07-13 14:01    -
+[DIR] model_gdsps/            2021-12-01 21:41    -
+[DIR] model_gdwps/            2021-12-01 16:50    -
+
+
+

Les répertoires en dessous de ce niveau sont liés à la date recherchée.

+

On peut exécuter sr3 pour télécharger des produits sélectionnés à partir de pompes de données comme celles-ci. +Les fichiers de configuration sont quelques lignes de mots clés, et sr3 comprend quelques exemples.

+

Lister les échantillons de configurations disponibles avec sr3 list

+
$ sr3 list examples
+  Sample Configurations: (from: /usr/lib/python3/dist-packages/sarracenia/examples )
+  cpump/cno_trouble_f00.inc        poll/aws-nexrad.conf             poll/pollingest.conf             poll/pollnoaa.conf               poll/pollsoapshc.conf
+  poll/pollusgs.conf               poll/pulse.conf                  post/WMO_mesh_post.conf          sarra/wmo_mesh.conf              sender/ec2collab.conf
+  sender/pitcher_push.conf         shovel/no_trouble_f00.inc        subscribe/WMO_Sketch_2mqtt.conf  subscribe/WMO_Sketch_2v3.conf    subscribe/WMO_mesh_CMC.conf
+  subscribe/WMO_mesh_Peer.conf     subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf          subscribe/dd_all.conf            subscribe/dd_amis.conf
+  subscribe/dd_aqhi.conf           subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf       subscribe/dd_cmml.conf           subscribe/dd_gdps.conf
+  subscribe/dd_ping.conf           subscribe/dd_radar.conf          subscribe/dd_rdps.conf           subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+  subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf        subscribe/hpfx_amis.conf         subscribe/local_sub.conf
+  subscribe/pitcher_pull.conf      subscribe/sci2ec.conf            subscribe/subnoaa.conf           subscribe/subsoapshc.conf        subscribe/subusgs.conf
+  sender/ec2collab.conf            sender/pitcher_push.conf         watch/master.conf                watch/pitcher_client.conf        watch/pitcher_server.conf
+  watch/sci2ec.conf
+
+
+

AMIS, le AES (Service de l’environnement atmosphérique, en anglais) Meteorological Information Service, était un +système de diffusion des données météorologiques via satellite dans les années 1980. Il s’agit d’un flux continu de messages +texte (à l’origine à 4800 bps !) et chaque message est limité à 14000 octets. Le service est passé à un flux Internet en continu +au début des années 2000, et la version internet est toujours transmise à ceux qui s’intéressent à la navigation aérienne +et maritime à travers le pays.

+

On peut receivoir un flux continu de ces bulletins météorologiques traditionnels à partir du datamart d’origine en utilisant +le modèle de subscribe/dd_amis.conf:

+
$ sr3 add subscribe/dd_amis.conf
+  add: 2021-01-26 01:13:54,047 [INFO] sarracenia.sr add copying: /usr/lib/python3/dist-packages/sarracenia/examples/subscribe/dd_amis.conf to /home/peter/.config/sr3/subscribe/dd_amis.conf
+
+
+

Maintenant, les fichiers dans .config/ peut être utilisé directement:

+
$ sr3 list
+  User Configurations: (from: /home/peter/.config/sr3 )
+  subscribe/dd_amis.conf           admin.conf                       credentials.conf                 default.conf
+  logs are in: /home/peter/.cache/sr3/log
+
+
+

Pour afficher une configuration, donnez-la à sr3 list comme argument:

+
$ sr3 list subscribe/dd_amis.conf
+  # il s’agit d’un flux de bulletin wmo (un ensemble appelé AMIS dans les temps anciens)
+
+  broker amqps://dd.weather.gc.ca/
+  topicPrefix v02.post
+
+  # instances: nombre de processus de téléchargement à exécuter à la fois.  la valeur par défaut est 1. Pas assez pour ce cas
+  instances 5
+
+  # expire, en utilisation opérationnelle, devrait être plus longue que l’interruption prévue
+  expire 10m
+
+  subtopic bulletins.alphanumeric.#
+
+
+

Pour supprimer une configuration:

+
$ sr3 remove subscribe/dd_amis
+  2021-01-26 01:17:24,967 [INFO] root remove FIXME remove! ['subscribe/dd_amis']
+  2021-01-26 01:17:24,967 [INFO] root remove removing /home/peter/.config/sr3/subscribe/dd_amis.conf
+
+
+

pour plus d’informations:

+ +
+
+

Ressources côté serveur allouées aux abonnés

+

Chaque configuration entraîne la déclaration des ressources correspondantes sur le broker, +dont la durée de vie est contrôlée par le paramètre expire. La valeur par défaut expire est définie +à 300 secondes pour éviter d’encombrer les serveurs avec de petites expér +Chaque configuration entraîne la déclaration des ressources correspondantes sur le courtier.

+

Régler expire à la valeur qui a le plus de sens pour votre application (suffisamment longue pour traverser +pannes que vous rencontrer.) Dans un fichier de configuration, quelque chose comme

+
expire 3h
+
+
+

Lors de la modification des paramètres subtopic ou queue, ou lorsque l’on s’attend à ne pas utiliser +une configuration pour une période prolongée, il est préférable de:

+
sr3 cleanup subscribe/swob.conf
+
+
+

qui désallouera la fil d’attente (et ses liaisons) sur le serveur.

+

Pourquoi? Chaque fois qu’un abonné est démarré, une fil d’attente est créée sur la pompe de données, avec +les liaisons de rubrique définies par le fichier de configuration. Si l’abonné est arrêté, +la fil d’attente continue de recevoir des messages de notification tels que définis par la sélection de subtopic, et lorsque le +l’abonné redémarre, les messages de notification en fil d’attente sont transférés au client. +Ainsi, lorsque l’option subtopic est modifiée, puisqu’elle est déjà définie sur le +serveur, on finit par ajouter une liaison plutôt que de la remplacer. Par exemple +si l’un d’eux a un subtopic qui contient SATELLITE, puis arrête l’abonné, +modifier le fichier et maintenant le topic ne contient que RADAR, lorsque l’abonné est +redémarré, non seulement tous les fichiers satellites en fil d’attente seront envoyés au consommateur, +mais le RADAR est ajouté aux fixations, plutôt que de les remplacer, de sorte que l’abonné +obtiendra à la fois les données SATELLITE et RADAR même si la configuration +ne contient plus l’ancien.

+

De plus, si l’on expérimente et qu’une fil d’attente doit être arrêtée pendant très longtemps +elle peut accumuler un grand nombre de messages de notification. Le nombre total de messages de notification +sur une pompe de données a un effet sur les performances de la pompe pour tous les utilisateurs. C’est donc +conseillé de demander à la pompe de désaffecter les ressources lorsqu’elles ne seront pas nécessaires +pendant de longues périodes ou lors de l’expérimentation de différents paramètres.

+
+
+

Utilisation de plusieurs configurations

+

Placez tous les fichiers de configuration, avec le suffixe .conf, dans un répertoire +standard : ~/.config/sr3/subscribe/. Par exemple, s’il y a deux fichiers dans +ce répertoire : dd_amis.conf et hpfx_amis.conf, on pourrait alors exécuter

+
fractal% sr3 start subscribe/dd_amis.conf
+starting:.( 5 ) Done
+
+fractal%
+
+
+

pour démarrer la configuration de téléchargement CMC. On peut utiliser +la commande sr3 pour démarrer/arrêter plusieurs configurations à la fois. +La commande sr3 passera par les répertoires par défaut et démarrera +toutes les configurations qu’y si trouve

+
fractal% sr3 status
+status:
+Component/Config                         State             Run  Miss   Exp Retry
+----------------                         -----             ---  ----   --- -----
+subscribe/dd_amis                        stopped             0     0     0     0
+subscribe/hpfx_amis                      stopped             0     0     0     0
+      total running configs:   0 ( processes: 0 missing: 0 stray: 0 )
+fractal% sr3 edit subscribe/hpfx_amis
+
+fractal% sr3 start
+starting:.( 10 ) Done
+
+fractal% sr3 status
+status:
+Component/Config                         State             Run  Miss   Exp Retry
+----------------                         -----             ---  ----   --- -----
+subscribe/dd_amis                        running             5     0     5     0
+subscribe/hpfx_amis                      running             5     0     5     0
+      total running configs:   2 ( processes: 10 missing: 0 stray: 0 )
+fractal%
+
+
+

démarrera certains processus sr3 tels que configurés par hpfx_amis.conf et d’autres +pour correspondre à dd_amis.conf. Sr3 stop fera également ce que vous attendez. Tout comme le sr3 status. +Notez qu’il existe 5 processus sr3 subscribe commencent par le CMC +et 3 NWS. Ce sont des instances et partagent les mêmes +fil d’attentes de téléchargement.

+

more:

+ +
+
+

Livraison hautement prioritaire

+

Bien que le protocole Sarracenia ne fournisse pas de hiérarchisation explicite, l’utilisation +de plusieurs files d’attentes offre des avantages similaires. Résultats de chaque configuration +dans une déclaration de fil d’attente côté serveur. Regroupez les produits à la même priorité dans +une fil d’attente en les sélectionnant à l’aide d’une configuration commune. Plus les regroupements sont petits, +plus le délai de traitement est faible. Alors que toutes les files d’attente sont traitées avec la même priorité, +les données passent plus rapidement dans des files d’attente plus courtes. On peut résumer par :

+
+

Utiliser plusieurs configurations pour établir la priorité

+
+

Pour rendre le conseil concret, prenons l’exemple des données d’Environnement Canada +( dd.weather.gc.ca ), qui distribue des binaires quadrillés, des images satellite GOES, +plusieurs milliers de prévisions urbaines, des observations, des produits RADAR, etc… +Pour la météo en temps réel, les avertissements et les données RADAR sont la priorité absolue. À certaines +heures de la journée, ou en cas d’arriérés, plusieurs centaines de milliers de produits +peut retarder la réception de produits hautement prioritaires si une seule fil d’attente est utilisée.

+

Pour assurer un traitement rapide des données dans ce cas, définissez une configuration pour vous abonner +aux avertissements météorologiques (qui sont un très petit nombre de produits), une seconde pour les RADARS +(un groupe plus grand mais encore relativement petit), et un troisième (groupe le plus important) pour toutes +les autres données. Chaque configuration utilisera une fil d’attente distincte. Les avertissements seront +traités le plus rapidement, les RADARS feront la queue les uns contre les autres et auront +plus de retard, et d’autres produits partageront une seule fil d’attente et seront soumis à plus de +retard dans les cas d’arriéré.

+

https://github.com/MetPX/sarracenia/blob/main/sarracenia/examples/subscribe/ddc_cap-xml.conf:

+
broker amqps://dd.weather.gc.ca/
+topicPrefix v02.post
+mirror
+directory ${HOME}/datamartclone
+subtopic alerts.cap.#
+
+
+

https://github.com/MetPX/sarracenia/blob/main/sarracenia/examples/subscribe/ddc_normal.conf:

+
broker amqps://dd.weather.gc.ca/
+topicPrefix v02.post
+subtopic #
+
+# rejeter les messages hautement prioritaire accepté par l´autre abonnement
+
+reject .*alerts/cap.*
+
+# la durée maximale de panne qu´on voudrait survivre sans perte de message
+# (on specifie une petite intervalle dans les cas de dévéloppement, mais plug long
+#  pour les cas opérationnels)
+expire 10m
+
+mirror
+directory ${HOME}/datamartclone
+
+
+

Là où vous voulez le miroir du data mart qui commence à ${HOME}/datamartclone (vraisemblablement il y a un +serveur web configuré pour afficher ce répertoire.) Probablement, la configuration ddc_normal +connaîtra beaucoup de files d’attente, car il y a beaucoup de données à télécharger. Le ddc_hipri.conf est +uniquement abonné aux avertissements météorologiques au format Common Alerting Protocol, il y aura donc +peu ou pas de fil d’attente pour ces données.

+
+
+

Affiner la sélection

+
+

Warning

+

FIXME: Faire une photo, avec un:

+
    +
  • courtier à une extrémité, et le subtopic s’y applique.

  • +
  • client à l’autre extrémité, et l’accept/reject s’appliquent là.

  • +
+
+

Choisissez subtopics (qui sont appliquées sur le broker sans téléchargement de message de notification) pour affiner +le nombre de messages de notification qui traversent le réseau pour accéder aux processus clients sarracenia. +Les options reject et accept sont évaluées par les processus sr3 subscribe eux-mêmes, +qui fourni un filtrage basé sur l’expression régulière des messages qui sont transférés. +accept fonctionne sur le chemin réel (enfin, l’URL), indiquant quels fichiers dans +le flux de notification reçu doit en fait être téléchargé. Regardez dans les Downloads +du fichier journal pour des exemples de ce chemin d’accès transformé.

+
+

Note

+

Brève introduction aux expressions régulières

+

Les expressions régulières sont un moyen très puissant d’exprimer les correspondances de motifs. +Elles offrent une flexibilité extrême, mais dans ces exemples, nous n’utiliserons qu’un +sous-ensemble très basique : le . est un caractère générique correspondant à n’importe quel caractère unique. Si c’est +suivi d’un nombre d’occurrences, il indique combien de lettres correspondront au motif. le caractère * (astérisque) signifie n’importe quel nombre d’occurrences. +ainsi:

+
    +
  • .* désigne toute séquence de caractères de n’importe quelle longueur. En d’autres termes, faites correspondre n’importe quoi.

  • +
  • cap.* désigne toute séquence de caractères commençant par cap.

  • +
  • .*CAP.* désigne toute séquence de caractères avec CAP quelque part dedans.

  • +
  • .*cap désigne toute séquence de caractères qui se termine par CAP. Dans le cas où plusieurs parties de la chaîne +peuvent correspondre, la plus longue est sélectionnée.

  • +
  • .*?cap comme ci-dessus, mais non-greedy, ce qui signifie que le match le plus court est choisi.

  • +
+

Veuillez consulter diverses ressources Internet pour plus d’informations sur l’ensemble +de variété de correspondance possible avec les expressions régulières :

+ +
+

retour aux exemples de fichiers de configuration :

+

Notez ce qui suit

+
$ sr3 edit subscribe/swob
+
+
+
+

broker amqps://anonymous@dd.weather.gc.ca +topicPrefix v02.post +accept .*/observations/swob-ml/.*

+

#écrire tous les SWOBS dans le répertoire de travail actuel +#MAUVAIS : CE N’EST PAS AUSSI BON QUE L’EXEMPLE PRÉCÉDENT +# NE PAS avoir de “subtopic” et filtrer avec “accept” SIGNIFIE QUE DES NOTIFICATIONS EXCESSIVES sont traitées.

+
+

Cette configuration, du point de vue de l’abonné, livrera probablement +les mêmes données que l’exemple précédent. Toutefois, le subtopic par défaut étant +un caractère générique signifie que le serveur transférera toutes les notifications pour le +serveur (probablement des millions d’entre eux) qui sera ignoré par le processus de l’abonné qui +applique la clause d’acceptation. Il consommera beaucoup plus de CPU et de +bande passante sur le serveur et le client. Il faut choisir les subtopics appropriés +pour minimiser les notifications qui seront transférées uniquement pour être ignorées. +Les modèles accept (et reject) sont utilisés pour affiner davantage subtopic plutôt +que de le remplacer.

+

Par défaut, les fichiers téléchargés seront placés dans le répertoire actuel +lors du démarrage de sr3 subscribe. Cela peut être remplacé à l’aide de +l’option directory.

+

Si vous téléchargez une arborescence de répertoires et que l’intention est de mettre en miroir l’arborescence, +alors l’option miroir doit être définie:

+
$ sr3 edit subscribe/swob
+
+
+
+

broker amqps://anonymous@dd.weather.gc.ca +topicPrefix v02.post +subtopic observations.swob-ml.# +directory /tmp +mirror True +accept .* +# +# au lieu d’écrire dans le répertoire de travail actuel, écrivez dans /tmp. +# dans /tmp. Mirror: créer une hiérarchie comme celle du serveur source.

+
+

On peut également intercaler les directives directory et accept/reject pour construire +une hiérarchie arbitrairement différente de ce qui se trouvait sur la pompe de données de source. +Le fichier de configuration est lu de haut en bas, alors sr3 subscribe +trouve un paramètre d’option ‘’directory’’, seulement les clauses ‘’accept’’ après +celles la entraîneront le placement de fichiers par rapport à ce répertoire

+
$ sr3 edit subscribe/ddi_ninjo_part1.conf
+
+
+
+

broker amqps://ddi.cmc.ec.gc.ca/ +topicPrefix v02.post +subtopic ec.ops.*.*.ninjo-a.#

+

directory /tmp/apps/ninjo/import/point/reports/in +accept .*ABFS_1.0.* +accept .*AQHI_1.0.* +accept .*AMDAR_1.0.*

+

directory /tmp/apps/ninjo/import/point/catalog_common/in +accept .*ninjo-station-catalogue.*

+

directory /tmp/apps/ninjo/import/point/scit_sac/in +accept .*~~SAC,SAC_MAXR.*

+

directory /tmp/apps/ninjo/import/point/scit_tracker/in +accept .*~~TRACKER,TRACK_MAXR.*

+
+

Dans l’exemple ci-dessus, les données du catalogue ninjo-station sont placées dans le +catalog_common/in, plutôt que dans l’hiérarchie des données ponctuelles +utilisée pour stocker les données qui correspondent aux trois premiers +clauses d’accept.

+
+

Note

+

Notez que .* dans la directive de subtopic, où +cela signifie “correspondre à un topic” (c’est-à-dire qu’aucun caractère de point n’est autorisé dans un nom +de sujet) a une signification différente de celle qui est dans une clause accept, +où cela signifie correspondre à n’importe quelle chaîne.

+

Oui, c’est déroutant. Non, on ne peut pas l’éviter.

+
+

Pour plus d´informations:

+ +
+
+

Perte de données

+
+
+

Panne trop longue

+

L’expire détermine combien de temps la pompe de données conservera votre abonnement et file d’attente +après une déconnexion. Le réglage doit être défini plus longtemps que la plus longue panne de votre +flux doit survivre sans perte de données.

+
+
+

File d´attente trop important

+

Les performances d’un flux +est important, car, au service d’Internet, le téléchargement lent d’un client affecte tous les autres, +et quelques clients lents peuvent submerger une pompe de données. Il existe souvent des politiques de serveur en place +pour éviter que des abonnements mal configurés (c’est-à-dire trop lents) n’entraînent de très longues files d’attente.

+

Lorsque la file d’attente devient trop longue, la pompe de données peut commencer à rejeter les messages, et +l’abonné percevra cela comme une perte de données.

+

Pour identifier les téléchargements lents, examinez le décalage dans le journal de téléchargement. Par exemple, créez +un exemple d’abonné comme ceci

+
fractal% sr3 list ie
+
+Sample Configurations: (from: /home/peter/Sarracenia/sr3/sarracenia/examples )
+cpump/cno_trouble_f00.inc        flow/amserver.conf               flow/poll.inc                    flow/post.inc                    flow/report.inc                  flow/sarra.inc
+flow/sender.inc                  flow/shovel.inc                  flow/subscribe.inc               flow/watch.inc                   flow/winnow.inc                  poll/airnow.conf
+poll/aws-nexrad.conf             poll/mail.conf                   poll/nasa-mls-nrt.conf           poll/noaa.conf                   poll/soapshc.conf                poll/usgs.conf
+post/WMO_mesh_post.conf          sarra/wmo_mesh.conf              sender/am_send.conf              sender/ec2collab.conf            sender/pitcher_push.conf         shovel/no_trouble_f00.inc
+subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf          subscribe/dd_all.conf            subscribe/dd_amis.conf           subscribe/dd_aqhi.conf           subscribe/dd_cacn_bulletins.conf
+subscribe/dd_citypage.conf       subscribe/dd_cmml.conf           subscribe/dd_gdps.conf           subscribe/dd_radar.conf          subscribe/dd_rdps.conf           subscribe/dd_swob.conf
+subscribe/ddc_cap-xml.conf       subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf        subscribe/hpfxWIS2DownloadAll.conf subscribe/hpfx_amis.conf
+subscribe/hpfx_citypage.conf     subscribe/local_sub.conf         subscribe/ping.conf              subscribe/pitcher_pull.conf      subscribe/sci2ec.conf            subscribe/subnoaa.conf
+subscribe/subsoapshc.conf        subscribe/subusgs.conf           sender/am_send.conf              sender/ec2collab.conf            sender/pitcher_push.conf         watch/master.conf
+watch/pitcher_client.conf        watch/pitcher_server.conf        watch/sci2ec.conf
+fractal%
+
+
+

choisissez-en un et ajoutez-le configuration locale

+
fractal% sr3 foreground subscribe/hpfx_amis
+.2022-12-07 12:39:37,977 [INFO] 3286919 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup queue declared q_anonymous_subscribe.hpfx_amis.67711727.37906289 (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,194 [INFO] 3286919 sarracenia.moth.amqp __getSetup binding q_anonymous_subscribe.hpfx_amis.67711727.37906289 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flowcb.log __init__ subscribe initialized with: {'post', 'on_housekeeping', 'after_accept', 'after_work', 'after_post'}
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2022-12-07 12:39:38,226 [INFO] 3286919 sarracenia.flow run pid: 3286919 subscribe/hpfx_amis instance: 0
+2022-12-07 12:39:38,241 [INFO] 3286919 sarracenia.flow run now active on vip None
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.20 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 3.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 2.17 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRMN20_KWAL_071739___19368
+2022-12-07 12:39:42,564 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 1.19 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SX/KWAL/17/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRWA20_KWAL_071739___7440
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN70_KWAL_071739___39755
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___132
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMN20_KWAL_071739___19368
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:42,957 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXAK50_KWAL_071739___15077
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___40860
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SA/KNKA/17/SAAK41_KNKA_071739___36105
+2022-12-07 12:39:43,227 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.71 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___19641
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___40860
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SAAK41_KNKA_071739___36105
+2022-12-07 12:39:43,457 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___19641
+2022-12-07 12:39:43,924 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.40 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/SR/KWAL/17/SRCN40_KWAL_071739___44806
+2022-12-07 12:39:43,924 [INFO] 3286919 sarracenia.flowcb.log after_accept accepted: (lag: 0.40 ) https://hpfx.collab.science.gc.ca /20221207/WXO-DD/bulletins/alphanumeric/20221207/UA/CWAO/17/UANT01_CWAO_071739___24012
+2022-12-07 12:39:44,098 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_071739___44806
+2022-12-07 12:39:44,098 [INFO] 3286919 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/UANT01_CWAO_071739___24012
+
+
+

Les nombres lag : signalés dans la session foreground indiquent l’ancienneté des données (en secondes, +en fonction de l’heure à laquelle elles ont été ajoutées au réseau par la source. Si vous voyez ce décalage +augmenter de manière déraisonnable, votre abonnement a un problème de performances.

+
+
+

Performance

+

Il y a de nombreux aspects de la performance que nous n’aborderons pas ici.

+

plus :

+ +

Dans les cas de haut-débit, comment faire remarquer rapidement les modifications de fichiers, +filtrer les réécritures fréquentes de fichiers, planifier des copies :

+ +

Mais Le but le plus courant lorsque les performances sont augmentées est d’accélérer leurs téléchargements. +Si les transferts vont trop lentement, les étapes sont les suivantes:

+
+

Optimiser la sélection des fichiers par processus

+
    +
  • Souvent, les utilisateurs spécifient # comme subtopic, ce qui signifie que les accept/reject +font tout le travail. Dans de nombreux cas, les utilisateurs ne sont intéressés que par une petite fraction des +fichiers publiés. Pour de meilleures performances, Rendez *suntopic* aussi spécifique que possible pour +minimiser l’envoi de messages de notification envoyés par le courtier et qui arrivent sur l’abonné uniquement pour +se faire rejetés. (utilisez l’option log_reject pour trouver de tels produits.)

  • +
  • Placez les instructions *reject* le plus tôt possible dans la configuration. Comme le rejet enregistre +le traitement de tous les regex ultérieurs dans la configuration.

  • +
  • Avoir peu de clauses d’acceptation/rejet : parce qu’il s’agit d’une expression régulière +les clauses de correspondance, accept/reject sont coûteuses, mais l’évaluation d’un +regex n’est pas beaucoup plus cher qu’un seul, il est donc préférable d’en avoir +quelques un plus compliqués que beaucoup de simples. Exemple:

    +
    accept .*/SR/KWAL.*
    +accept .*/SO/KWAL.*
    +
    +
    +

    fonctionnera à la moitié de la vitesse (ou le double de la surcharge du processeur) par rapport à

    +
    accept .*/S[OR]/KWAL.*
    +
    +
    +
  • +
  • Utilisez suppress_duplicates. Dans certains cas, il y a un risque que le même fichier +se fassent annoncer plus d’une fois. Habituellement, les clients ne veulent pas de copies redondantes +des fichiers transférés. L’option suppress_duplicates configure un cache de +les sommes de contrôle des fichiers qui sont passés et empêche leur traitement +encore.

  • +
  • Si vous transférez de petits fichiers, le traitement de transfert intégré est tout à fait +bon, mais s’il y a des fichiers volumineux dans le mélange, alors un chargement sur un binaire en C +va aller plus vite. Utilisez des plugins tels que accel_wget, accel_sftp, +accel_cp (pour les fichiers locaux.) Ces plugins ont des paramètres de seuil de sorte que +les méthodes optimal python transfer sont toujours utilisées pour les fichiers plus petits que le +seuil.

  • +
  • l’augmentation du prefetch peut réduire la latence moyenne (amortie sur +le nombre de messages de notification prélus.) Les performances peuvent être amélioré sur une longue +distances ou taux de messages de notification élevés au sein d’un centre de données.

  • +
  • Si vous contrôlez l’origine d’un flux de produits, et les consommateurs voudront une +très grande proportion des produits annoncés, et les produits sont petits +(quelques K au plus), envisagez de combiner l’utilisation de v03 avec l’inlining pour un +transfert optimal de petits fichiers. Remarque, si vous avez une grande variété d’utilisateurs +qui veulent tous des ensembles de données différents, l’inlining peut être contre-productif. Ceci +entraînera également des messages de notification plus importants et signifiera une charge beaucoup plus élevée sur le courtier. +Ca peut optimiser quelques cas spécifiques, tout en ralentissant le courtier dans l’ensemble.

  • +
+
+
+

Utiliser des instances

+

Une fois que vous avez optimisé ce qu’un seul abonné peut faire, si ce n’est pas assez rapide, +utilisez l’option instances pour que davantage de processus participent au +traitement. Avoir 10 ou 20 instances n’est pas un problème du tout. Le maximum +nombre d’instances qui augmenteront les performances plafonnera à un moment donné +qui varie en fonction de la latence à négocier, de la vitesse de traitement des instances de +chaque fichier, la prélecture en cours d’utilisation, etc… Il faut expérimenter.

+

En examinant les journaux d’instance, s’ils semblent attendre les messages de notification pendant une longue période, +ne faisant aucun transfert, alors on aurait pu atteindre la saturation de la fil d’attente. +Cela se produit souvent à environ 40 à 75 instances. Rabbitmq gère une seule fil d’attente +avec un seul processeur, et il y a une limite au nombre de messages de notification qu’une fil d’attente peut traiter +dans une unité de temps donnée.

+

Si la fil d’attente devient saturée, nous devons partitionner les abonnements +dans plusieurs configurations. Chaque configuration aura une fil d’attente distincte, +et les files d’attente auront leurs propres processeurs (CPU). Avec un tel partitionnement, nous sommes allés +à une centaine d’instances et pas vu de saturation. Nous ne savons pas quand nous courons +hors performance.

+

Nous n’avons pas encore eu besoin de faire évoluer le courtier lui-même.

+
+
+

Suppression des doublons haute performance

+

Une mise en garde à l’utilisation de instances est que suppress_duplicates est inefficace +car les différentes occurrences d’un même fichier ne seront pas reçues par les même +instances, et donc avec n instances, environ n-1/n doublons passeront à travers.

+

Afin de supprimer correctement les messages de notification de fichiers en double dans les flux de données +qui ont besoin de plusieurs instances, on utilise le winnowing avec post_exchangeSplit. +Cette option envoie des données à plusieurs échanges post-exchange en fonction de la somme de contrôle des données, +de sorte que tous les fichiers en double seront acheminés vers le même processus winnow. +Chaque processus winnow exécute la suppression normale des doublons utilisée dans des instances uniques, +puisque tous les fichiers avec la même somme de contrôle se retrouvent avec le même winnow, cela fonctionne. +Les processus winnow sont ensuite postés sur l’échange utilisé par des piscines de traitement réels.

+

Pourquoi la suppression des doublons haute performance est-elle une bonne chose ? Parce que +le modèle de disponibilité de Sarracenia est d’avoir des piles d’applications individuelles +qui produisent aveuglément des copies réductrices de produits. Il ne nécessite aucun ajustement +d’application d’un seul nœud à la participation à un cluster. Sarracenia +sélectionne le premier résultat que nous recevons pour le transfert. Cela évite tout tri +du protocole de quorum, une source d’une grande complexité en haute disponibilité +et en mesurant en fonction de la production, minimise le potentiel des +systèmes à apparaître, lorsqu’ils ne sont pas complètement fonctionnels. Les +applications n’ont pas besoin de savoir qu’il existe une autre pile produisant le même +produit, ce qui les simplifie également.

+

pour plus:

+ +
+
+
+

Plugins

+

Le traitement des fichiers par défaut est souvent correct, mais il existe également des personnalisations prédéfinies qui +peuvent être utilisé pour modifier le traitement effectué par les composants. La liste des plugins prédéfinis est +dans un répertoire ‘plugins’ où que le paquet soit installé (consultable avec sr3 list) +exemple de sortie:

+
$ sr3 list help
+blacklab% sr3 list help
+Valid things to list: examples,eg,ie flow_callback,flowcb,fcb v2plugins,v2p
+
+$ sr3 list fcb
+
+
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/accept/delete.py          flowcb/accept/downloadbaseurl.py
+flowcb/accept/hourtree.py        flowcb/accept/httptohttps.py
+flowcb/accept/longflow.py        flowcb/accept/posthourtree.py
+flowcb/accept/postoverride.py    flowcb/accept/printlag.py
+flowcb/accept/rename4jicc.py     flowcb/accept/renamedmf.py
+flowcb/accept/renamewhatfn.py    flowcb/accept/save.py
+flowcb/accept/speedo.py          flowcb/accept/sundewpxroute.py
+flowcb/accept/testretry.py       flowcb/accept/toclusters.py
+flowcb/accept/tohttp.py          flowcb/accept/tolocal.py
+flowcb/accept/tolocalfile.py     flowcb/accept/wmotypesuffix.py
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py
+flowcb/filter/pclean_f90.py      flowcb/filter/pclean_f92.py
+flowcb/filter/wmo2msc.py         flowcb/gather/file.py
+flowcb/gather/message.py         flowcb/housekeeping/hk_police_queues.py
+flowcb/housekeeping/resources.py flowcb/line_log.py
+flowcb/log.py                    flowcb/mdelaylatest.py
+flowcb/nodupe/data.py            flowcb/nodupe/name.py
+flowcb/pclean.py                 flowcb/poll/airnow.py
+flowcb/poll/mail.py              flowcb/poll/nasa_mls_nrt.py
+flowcb/poll/nexrad.py            flowcb/poll/noaa_hydrometric.py
+flowcb/poll/usgs.py              flowcb/post/message.py
+flowcb/retry.py                  flowcb/sample.py
+flowcb/script.py                 flowcb/send/email.py
+flowcb/shiftdir2baseurl.py       flowcb/v2wrapper.py
+flowcb/wistree.py                flowcb/work/delete.py
+flowcb/work/rxpipe.py
+$
+
+
+

On peut également voir les flowcallback inclus avec Sarracenia en consultant +la Référence flowcallback (anglais) +Les pluguns sont écrites en python et les auteurs peuvent les mettre dans ~/.config/sr3/plugins ou +bien n´importe ou dans le répertoire de configuration. On peut également consulter le code source +de n´importe lequel plugin avec la concatenation du répertoire afficher au début de sr3 list +et le module dans le listing. e.g.:

+
vi /home/peter/Sarracenia/sr3/sarracenia/flowcb/nodupe/name.py
+
+
+

On peut également consulter la documentations d´une module en utilisant les méchanismes de pythjon:

+
fractal% python3
+Python 3.10.6 (main, Nov  2 2022, 18:53:38) [GCC 11.3.0] on linux
+Type "help", "copyright", "credits" or "license" for more information.
+>>> import sarracenia.flowcb.run
+>>> help(sarracenia.flowcb.run)
+
+
+

Les plugins peuvent être inclus dans les configurations en ajoutant des lignes ‘flow_callback’ comme:

+
callback work.rxpipe
+
+
+

qui ajoute le rappel donné à la liste des rappels à appeler. +Il y a aussi:

+
callback_prepend work.rxpipe
+
+
+

qui ajoutera ce rappel à la liste, de sorte qu’il est appelé avant les autres.

+

Les plugins sont tous écrits en python, et les utilisateurs peuvent créer les leurs et les placer dans ~/.config/sr3/plugins. +Pour plus d’informations sur la création de nouveaux plug-ins personnalisés, reportez-vous à la section Implanter des Flowcallback

+

Pour récapituler :

+
    +
  • Pour voir les plugins actuellement disponibles sur le système sr3 list fcb

  • +
  • Pour afficher le contenu d’un plugin: FlowCallback Reference (anglais) <../../Reference/flowcb.rst>

  • +
  • Les plugins peuvent avoir des paramètres d’option, tout comme ceux intégrés

  • +
  • Pour les définir, placez les options dans le fichier de configuration avant que le plugin ne s’appelle lui-même

  • +
  • Pour créer vos propres plugins, créez-les dans ~/.config/sr3/plugins, ou dans le chemin PYTHONPATH configurer +pour acceder a vos modules Python.

  • +
+

plus:

+ +

encore davantage:

+ +
+
+

file_rxpipe

+

Le plugin file_rxpipe pour sr3 permet à toutes les instances d’écrire les noms +des fichiers téléchargés sur un canal nommé (¨named pipe¨ ). La configuration de cette configuration nécessite deux lignes dans +un fichier de configuration sr3

+
$ mknod /home/peter/test/.rxpipe p
+$ sr3 edit subscribe/swob
+
+broker amqps://anonymous@dd.weather.gc.ca
+topicPrefix v02.post
+subtopic observations.swob-ml.#
+
+rxpipe_name /home/peter/test/.rxpipe
+
+callback work/rxpipe
+
+directory /tmp
+mirror True
+accept .*
+# rxpipe est un plugin on_file intégré qui écrit le nom du fichier reçu dans
+# un canal nommé '.rxpipe' dans le répertoire de travail actuel.
+
+
+

Avec rxpipe, chaque fois qu’un transfert de fichiers est terminé et est prêt pour +post-traitement, son nom est écrit sur le canal linux (nommé .rxpipe.)

+
+

Note

+

Dans le cas où un grand nombre d’instances d’abonnement fonctionnent +Sur la même configuration, il y a une légère probabilité que les notifications +peuvent se corrompre mutuellement dans le canal nommé.

+

FIXME Nous devrions probablement vérifier si cette probabilité est négligeable ou non.

+
+
+
+

Analyse d’antivirus

+

Un autre exemple d’utilisation facile d’un plugin est de réaliser une analyse antivirus. +En supposant que ClamAV-daemon est installé, ainsi que le python3-pyclamd +package, alors on peut ajouter ce qui suit à un +fichier de configuration d’un abonné:

+
broker amqps://dd.weather.gc.ca
+topicPrefix v02.post
+batch 1
+callback clamav
+subtopic observations.swob-ml.#
+
+
+

Pour que chaque fichier téléchargé soit analysé av. Exemple d’exécution

+
 $ sr3 foreground subscribe//dd_swob.conf
+
+ blacklab% sr3 foreground subscribe/dd_swob
+ 2022-03-12 18:47:18,137 [INFO] 29823 sarracenia.flow loadCallbacks plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.clamav.Clamav', 'sarracenia.flowcb.log.Log']
+ clam_scan on_part plugin initialized
+ 2022-03-12 18:47:22,865 [INFO] 29823 sarracenia.flowcb.log __init__ subscribe initialized with: {'after_work', 'on_housekeeping', 'after_accept'}
+ 2022-03-12 18:47:22,866 [INFO] 29823 sarracenia.flow run options:
+ _Config__admin=amqp://bunnymaster:Easter1@localhost/ None True True False False None None, _Config__broker=amqps://anonymous:anonymous@dd.weather.gc.ca/ None True True False False None None,
+ _Config__post_broker=None, accel_threshold=0, acceptSizeWrong=False, acceptUnmatched=False, action='foreground', attempts=3, auto_delete=False, baseDir=None, baseUrl_relPath=False, batch=100, bind=True,
+ bindings=[('xpublic', ['v02', 'post'], ['observations.swob-ml.#'])], bufsize=1048576, bytes_per_second=None, bytes_ps=0, cfg_run_dir='/home/peter/.cache/sr3/subscribe/dd_swob', config='dd_swob',
+ configurations=['subscribe/dd_swob'], currentDir=None, dangerWillRobinson=False, debug=False, declare=True, declared_exchanges=['xpublic', 'xcvan01'],
+.
+.
+.
+ 022-03-12 18:47:22,867 [INFO] 29823 sarracenia.flow run pid: 29823 subscribe/dd_swob instance: 0
+ 2022-03-12 18:47:30,019 [INFO] 29823 sarracenia.flowcb.log after_accept accepted: (lag: 140.22 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/COGI/2022-03-12-2344-COGI-AUTO-minute-swob.xml
+.
+.
+.  # bonnes entrées...
+
+ 22-03-12 19:00:55,347 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2347-CVPX-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,353 [INFO] 30992 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.00579023 seconds, no viruses in /tmp/dd_swob/2022-03-12-2347-CVPX-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,385 [INFO] 30992 sarracenia.flowcb.log after_accept accepted: (lag: 695.46 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/COTR/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,571 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,596 [INFO] 30992 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.0243611 seconds, no viruses in /tmp/dd_swob/2022-03-12-2348-COTR-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,637 [INFO] 30992 sarracenia.flowcb.log after_accept accepted: (lag: 695.71 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/CWGD/2022-03-12-2348-CWGD-AUTO-minute-swob.xml
+ 2022-03-12 19:00:55,844 [INFO] 30992 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2348-CWGD-AUTO-minute-swob.xml
+
+ .
+ .
+ . # mauvaises entrées.
+
+ 2022-03-12 18:50:13,809 [INFO] 30070 sarracenia.flowcb.log after_work downloaded ok: /tmp/dd_swob/2022-03-12-2343-CWJX-AUTO-minute-swob.xml
+ 2022-03-12 18:50:13,930 [INFO] 30070 sarracenia.flowcb.log after_accept accepted: (lag: 360.72 ) https://dd4.weather.gc.ca /observations/swob-ml/20220312/CAJT/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+ 2022-03-12 18:50:14,104 [INFO] 30070 sarracenia.flowcb.clamav after_work scanning: /tmp/dd_swob/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+ 2022-03-12 18:50:14,105 [ERROR] 30070 sarracenia.flowcb.clamav avscan_hit part_clamav_scan took 0.0003829 not forwarding, virus detected in /tmp/dd_swob/2022-03-12-2343-CAJT-AUTO-minute-swob.xml
+
+ .
+ . # chaque intervalle de heartbeat, un petit résumé:
+ .
+ 2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.clamav on_housekeeping files scanned 121, hits: 5
+
+
+
+
+

Journalisation et débogage

+

Comme les composants sr3 s’exécutent généralement en tant que démon (sauf s’ils sont appelés en mode foreground) +on examine normalement son fichier journal pour savoir comment se déroule le traitement. Lorsque seulement +une seule instance est en cours d’exécution, on peut afficher le journal du processus en cours d’exécution comme suit:

+
sr3 log subscribe/*myconfig*
+
+
+

FIXME: pas implémenté correctement. normalement utiliser la commande “foreground” à la place.

+

myconfig est le nom de la configuration en cours d’exécution. Les fichiers journaux +sont placés conformément à la spécification XDG Open Directory. Il y aura un fichier journal +pour chaque instance (processus de téléchargement) d’un processus sr3 subscribe exécutant la configuration myflow

+
in linux: ~/.cache/sarra/log/sr_subscribe_myflow_01.log
+
+
+

On peut remplacer le placement sur Linux en définissant la variable d’environnement XDG_CACHE_HOME, comme +par: XDG Open Directory Specification +Les fichiers journaux peuvent être très volumineux pour les configurations à volume élevé, de sorte que la journalisation est très configurable.

+

Pour commencer, on peut sélectionner le niveau de journalisation dans l’ensemble de l’application en utilisant +logLevel et logReject :

+
    +
  • +
    debug

    Définir l’option de debug est identique à utiliser logLevel debug

    +
    +
    +
  • +
  • +
    logLevel ( par défaut: info )

    Niveau de journalisation exprimé par la journalisation de python. Les valeurs possibles sont les suivantes: critical, error, info, warning, debug.

    +
    +
    +
  • +
  • +
    log_reject <True|False> ( par défaut: False )

    imprimer un message de journal lors du rejet des messages de notification (en choisissant de ne pas télécharger les fichiers correspondants)

    +

    Les messages de rejet indiquent également la raison du rejet.

    +
    +
    +
  • +
+

À la fin de la journée (à minuit), ces fichiers de journalisations sont pivotées automatiquement par +les composants, et l’ancien journal obtient un suffixe de date. Le répertoire dans lequel +les journaux sont stockés peut être remplacé par l’option log, le nombre +de journaux pivotés à conserver sont définis par le paramètre logRotate. Le journal (et metriques) +le plus ancien est supprimé lorsque le nombre maximal de journaux (en messages et statistiques) a été +atteint et que cela poursuit pour chaque rotation. Un intervalle prend une durée de l’intervalle et +cela peut prendre un suffixe d’unité de temps, tel que ‘d|D’ pour les jours, ‘h|H’ pour les heures, +ou ‘m|M’ pour les minutes. Si aucune unité n’est fournie, les journaux tourneront à minuit. +Voici quelques paramètres pour la gestion des fichiers journaux :

+
    +
  • +
    log <dir> ( par défaut: ~/.cache/sarra/log ) (sur Linux)

    Répertoire dans lequel stocker les fichiers journaux.

    +
    +
    +
  • +
  • +
    statehost <False|True> ( par défaut: False )

    Dans les grands centres de données, l’annuaire de base peut être partagé entre des milliers de +nœuds. Statehost ajoute le nom du nœud après le répertoire de la cache pour le rendre +unique à chaque nœud. Ainsi, chaque nœud a ses propres fichiers d’état et journaux. +Par exemple, sur un nœud nommé goofy, ~/.cache/sarra/log/ devient ~/.cache/sarra/goofy/log.

    +
    +
    +
  • +
  • +
    logRotateCount <max_logs> ( par défaut: 5 , alias: lr_backupCount)

    Nombre maximal de journaux archivés.

    +
    +
    +
  • +
  • +
    logRotateInterval <duration>[<time_unit>] ( par défaut: 1, alias: lr_interval)

    La durée de l’intervalle avec une unité de temps optionnelle (ex. 5m, 2h, 3d)

    +
    +
    +
  • +
  • +
    permLog ( par défaut: 0600 )

    Bits d’autorisation à définir sur les fichiers journaux.

    +
    +
    +
  • +
+
+

Réglage du débogage flowcb/log.py

+

En plus des options d’application, il existe un flowcb qui est utilisé par défaut pour la journalisation, qui +a des options supplémentaires:

+
    +
  • logMessageDump (par défaut : off) indicateur booléen +S’ils sont définis, tous les champs d’un message de notification sont imprimés, à chaque événement, plutôt qu’une simple référence url/chemin.

  • +
  • +
    logEvents ( after_accept par défaut,after_work,on_housekeeping )

    émettre des messages de journal standard à certains points durant le traitement des messages. +autres valeurs : on_start, on_stop, post, gather, … etc…

    +
    +
    +
  • +
+

etc… On peut également modifier les plugins fournis, ou en écrire de nouveaux pour changer complètement la journalisation.

+

plus:

+ +
+
+

Réglage du débogage moth

+

L’activation de logLevel pour déboguer l’ensemble de l’application entraîne souvent des fichiers journaux excessivement volumineux. +Par défaut, la classe parent Messages Organized into Topic Hierarchies (Moth) pour les protocoles de messagerie, +ignore l’option de débogage à l’échelle de l’application. Pour activer le débogage de la sortie de ces classes, il y a +des paramètres supplémentaires.

+

On peut définir explicitement l’option de débogage spécifiquement pour la classe de protocole de messagerie:

+
set sarracenia.moth.amqp.AMQP.logLevel debug
+set sarracenia.moth.mqtt.MQTT.logLevel debug
+
+
+

cela va rendra la couche de messagerie très verbeuse. +Parfois, lors des tests d’interopérabilité, il faut voir les messages de notification bruts, avant de décoder par classes de Moth

+
messageDebugDump
+
+
+

L’une ou l’autre de ces options ou les deux feront de très gros journaux et sont mieux utilisées judicieusement.

+

plus:

+ +
+
+
+

Métrique Housekeeping

+

Les rappels de flux peuvent implémenter un point d’entrée on_housekeeping. Ce point d’entrée est généralement +une possibilité pour les rappels d’imprimer périodiquement des métriques. Le journal intégré et +les rappels de surveillance des ressources, par exemple, donnent des lignes dans le journal comme suit

+
2022-03-12 19:00:55,114 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=1.97 system=0.3
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Memory threshold set to: 161.2 MiB
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory usage: 53.7 MiB / 161.2 MiB = 33.33%
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.clamav on_housekeeping files scanned 121, hits: 0
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats messages received: 242, accepted: 121, rejected: 121  rate:    50%
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats files transferred: 0 bytes: 0 Bytes rate: 0 Bytes/sec
+2022-03-12 19:00:55,115 [INFO] 30992 sarracenia.flowcb.log housekeeping_stats lag: average: 778.91, maximum: 931.06
+
+
+

plus:

+ +
+
+

Réception de fichiers redondants

+

Dans les environnements où une grande fiabilité est requise, plusieurs serveurs +sont souvent configurés pour fournir des services. L’approche Sarracenia pour +la haute disponibilité est ´Active-Active´ en ce sens que toutes les sources sont en ligne +et la production de données en parallèle. Chaque source publie des données, +et les consommateurs les obtiennent de la première source qui les rend disponible, +en utilisant des sommes de contrôle pour déterminer si la référence donnée a été obtenue +ou pas.

+

Ce filtrage nécessite la mise en œuvre d’une pompe locale sans données avec +sr_winnow. Consultez le Guide de l’administrateur pour plus d’informations.

+
+
+

Proxys Web

+

La meilleure méthode pour travailler avec des proxys Web est de mettre ce qui suit +dans le fichier default.conf:

+
declare env HTTP_PROXY http://yourproxy.com
+declare env HTTPS_PROXY http://yourproxy.com
+
+
+

La mise en place de default.conf garantit que tous les abonnés utiliseront +le proxy, pas seulement une seule configuration.

+
+
+

Accès au niveau de l’API

+

Sarracenia version 3 propose également des modules python qui peuvent être appelés +à partir d’applications Python existantes.

+ +

L’API de flow apporte toutes les options de placement et d’analyse de +Sarracenia, c’est une manière pythonique de démarrer un flux à partir de python lui-même.

+

Ou on peut éviter le schéma de configuration de Sarracenia, peut-être que l’on veut +juste utiliser le support du protocole de message:

+ +
+
+

Plus d’informations

+

la page sr3(1) contient de l’informative définitive +La page principale: Sarracenia Documentation

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/CommentFaire/v2ASr3.html b/fr/CommentFaire/v2ASr3.html new file mode 100644 index 000000000..52ab02227 --- /dev/null +++ b/fr/CommentFaire/v2ASr3.html @@ -0,0 +1,1465 @@ + + + + + + + Portage des plugins V2 vers Sr3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Portage des plugins V2 vers Sr3

+

Ceci est un guide pour porter des plugins de Sarracenia version 2.X (metpx-sarracenia) vers +Sarracenia version 3.x (metpx-sr3)

+ +
+

Note

+

Si vous êtes nouveau sur Sarracenia, et que vous n’avez aucune expérience ou besoin de regarder les plugins v2, +ne lisez pas ceci. cela ne fera que vous confondre. Ce guide s’adresse à ceux qui ont besoin de prendre des +Plugins v2 et les porter à Sr3. Vous feriez mieux d’obtenir un nouveau regard en regardant le +jupyter notebook examples qui fournissent une introduction à la v3 sans +les références déroutantes à la v2.

+
+
+

Note

+

Même si vous avez réellement besoin de porter des plugins v2 vers sr3, vous devriez toujours être +familier avec les plugins sr3 avant d’essayer d’en porter un. Ressources pour cela :

+

Guide de Programmation

+
+

Les exemples de jupyter notebook sont probablement un bon pré-requis pour tout +le monde, pour comprendre comment fonctionnent les plugins Sr3, avant d’essayer de porter ceux de la v2.

+
+
+

Exemple de plugin Sr3

+

D’une manière générale, les plugins v2 ont été rajouté sur le code existant pour permettre certaines modifications +de comportement. Les plugins V2 de première génération n’avaient que des routines uniques déclarées +(par exemple on_message), alors que ceux de la deuxième génération utilisaient des classes entières +(par exemple plugin) ont été déclarés, mais toujours sur pilotis.

+

Les plugins Sr3 sont des éléments de conception de base, composés ensemble pour implémenter une partie de +Sarracenia. Les plugins V3 devraient être plus faciles à implémenté et à déboguer pour les programmeurs Python, +et sont plus flexibles et puissants que le mécanisme de v2.

+
+
    +
  • v3 utilise la syntaxe standard de python, pas l’étrange self.plugins, parent.logger, de la v2, +et oh gee pourquoi import ne fonctionne-t-il pas?

  • +
  • Importations python standard; Les erreurs de syntaxe sont détectées et signalées de la manière normale

  • +
  • Les classes v3 sont conçues pour être utilisables en dehors de l’interface de ligne de commande elle-même +(voir les exemples de jupyter notebook) +appelable par les programmeurs d’applications dans leur propre code, comme toute autre bibliothèque python.

  • +
  • Les classes v3 peuvent être sous-classées pour ajouter des fonctionnalités de base, comme un nouveau message +de notification ou un protocole de transport de fichier.

  • +
+
+
+

Note

+
+
Il existe également quelques vidéos pas à pas sur Youtube montrant des ports simples v2 -> v3:
+
+
+
+
+

Placement de Fichier

+

v2 place les fichier de configuration sous ~/.config/sarra, et les fichiers d’état sous ~/.cache/sarra

+

v3 place les fichier de configuration sous ~/.config/sr3, et les fichiers d’état sous ~/.cache/sr3

+

v2 a une implémentation C de sarra appelée sarrac. L’implémentation C pour v3, est appelée sr3c, +et est identique à celui de la v2, sauf qu’il utilise les emplacements de fichiers v3.

+
+
+

Différence de ligne de commande

+

En bref, le point d’entrée sr3 est utilisé pour démarrer / arrêter / évaluer les choses:

+
v2:  sr_*component* start config
+
+v3:  sr3 start *component*/config
+
+
+

Dans sr3, on peut également utiliser des spécifications de style de globbing de fichier pour demander qu’une commande +soit invoqué sur un groupe de configurations, alors que dans la v2, on ne pouvait fonctionner que sur une à la fois.

+
+

Note

+

sr3_post est une exception à ce changement parce qu’il fonctionne comme sr3_post de la v2, étant +un outil d’affichage interactif.

+
+
+
+

Ce qui fonctionnera sans changement

+

La première étape du portage d’une configuration subscribe/X vers v3, consiste simplement à copier le +fichier de configuration de ~/.config/sarra à l’emplacement correspondant dans ~/.config/sr3 et essayez:

+
sr3 show subscribe/X
+
+
+

La commande show est nouvelle dans sr3 et permet d’afficher la configuration après +avoir été analysé. La plupart d’entre eux devraient fonctionner, sauf si vous avez des plugins do_*.

+

Exemples de choses qui devraient fonctionner:

+
    +
  • tous les paramètres des fichiers de configuration v2 doivent être reconnus par l’analyseur d’options v3 et convertis +aux équivalents v3, c’est-à-dire :

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    Option v2

    Option v3

    accept_scp_threshold

    accel_threshold

    heartbeat

    housekeeping

    chmod_log

    permLog

    loglevel

    logLevel

    post_base_url

    post_baseUrl

    post_rate_limit

    messageRateMax

    cache, suppress_duplicates

    nodupe_ttl

    topic_prefix

    topicPrefix

    +

    Pour la liste complète, consultez le Release Notes

    +

    Le topic_prefix dans la v2 est ‘v02.post’ dans la v3, la valeur par défaut est ‘v03’. Si topic_prefix est omis +vous devrez ajouter la ligne topicPrefix v02.post pour obtenir le même comportement que la v2. Pourrais +être également placé dans ~/.config/sr3/default.conf si le cas est trop courant. +Il se peut que l’on doive remplacer de la même manière la valeur par défaut sr3 pour post_topicPrefix.

    +
  • +
  • toutes les routines on_message, on_file, on_post, on_heartbeat, fonctionneront, par sr3 en utilisant +le plugin flowcb/v2wrapper.py qui sera automatiquement appelé lorsque les plugins v2 sont +lu dans le fichier de configuration.

  • +
+
+

Note

+

Idéalement, v2wrapper est utilisé comme béquille pour permettre d’avoir une configuration fonctionnelle +rapidement. Il y a un succès de performance à l’utilisation de v2wrapper.

+
+
+
+

Ce qui ne fonctionnera pas sans changement

+
    +
  • do_* ils sont juste fondamentalement différents dans la v3.

  • +
+

Si vous avez une configuration avec un plugin do_*, vous avez besoin de ce guide, à partir du jour 1. +pour définir une configuration pour utiliser un plugin, dans la v2 on utilisait l’option plugin

+
plugin <pluginName>
+
+
+

L’équivalent de celui de la v3 est callback:

+
callback <pluginName>
+
+
+

Pour que ce raccourci fonctionne, il devrait y avoir un fichier nommé <pluginName>.py quelque part dans le +PYTHONPATH (~/.config/plugins est ajouté pour plus de commodité.) et ce fichier source python a besoin +qu’une classe <PluginName> y soit déclarée (identique au nom du fichier mais avec la première lettre en majuscule). +Si vous devez le nommer différemment, il existe un formulaire plus long qui permet de violer la +convention dans v3:

+
flowCallback <pluginName>.MyFavouriteClass
+
+
+

les déclarations de plugins de routine individuelles on_message, on_file, etc… ne sont pas un moyen de +faire les choses dans la v3. Vous déclarez des rappels et leur demandez de contenir les points d’entrée dont vous avez besoin.

+
    +
  • DESTFNSCRIPT fonctionne de manière similaire dans v3 à v2, mais l’API est faite pour correspondre v3 flowCallbacks, +les nouvelles routines, ou on renvoie le nouveau nom de fichier en sortie, au lieu de modifier un champ +dans le message de notification.

  • +
+
+
+

Différences de codage entre les plugins dans v2 vs Sr3

+

L’API pour ajouter ou personnaliser des fonctionnalités dans sr3 est très différente de la v2. +En général, les plugins v3:

+
    +
  • sont généralement sous-classés à partir de sarracenia.flowcb.FlowCB.

    +

    Dans la v2, on déclarerait:

    +
    class Msg_Log(object):
    +
    +
    +

    Les plugins v3 sont des fichiers sources python normaux (pas de magie à la fin.) +ils sont sous-classés à partir de sarracenia.flowcb:

    +
    from sarracenia.flowcb import FlowCB
    +
    +class MyPlugin(FlowCB):
    +  ...le reste de la classe de plugin..
    +
    +   def after_accept(self, worklist):
    +     ...code à exécuter dans callback...
    +
    +
    +

    Pour créer un plugin after_accept dans la classe MyPlugin, définissez une fonction +avec ce nom et la signature appropriée.

    +
  • +
  • Les plugins v3 sont pythoniques, pas bizarres : +Dans la v2, vous avez besoin que la dernière ligne pour inclure quelque chose comme

    +
    self.plugin = 'Msg_Delay'
    +
    +
    +

    ceux de la première génération à la fin avaient quelque chose comme ceci pour attribuer explicitement des points d’entrée:

    +
    msg_2localfile = Msg_2LocalFile(None)
    +self.on_message = msg_2localfile.on_message
    +
    +
    +

    Quoi qu’il en soit, une partie python naïve du fichier échouerait invariablement sans qu’une sorte de +harnais de test ne soit enroulée autour d’elle.

    +
    +

    Note

    +

    Dans la v3, supprimez ces lignes (généralement situées au bas du fichier)

    +
    +

    Dans la v2, il y avait des problèmes étranges avec les importations, ce qui a entraîné la mise en place +d’importer des instructions à l’intérieur des fonctions. Ce problème est résolu dans la v3, vous pouvez +vérifier votre syntaxe d’importation en faisant import X dans n’importe quel interpréteur python.

    +
    +

    Note

    +

    Placez les importations nécessaires au début du fichier, comme tout autre module python +et supprimez les importations situées dans les fonctions lors du portage.

    +
    +
  • +
  • Les plugins v3 peuvent être utilisés par les programmeurs d’applications. Les plugins ne sont pas +boulonné, mais un élément central, implémentant la suppression de doublon, réception et transmission de messages +de notification, surveillance de fichiers, etc.. comprendre les plugins v3 donne aux gens des indices +importants pour être capable de travailler sur sarracénia.

    +

    Les plugins v3 peuvent être importés dans des applications existantes pour ajouter la possibilité +d’interagir avec les pompes sarracenia sans utiliser l’interface de ligne de commande Sarracenia. +voir les tutoriels jupyter.

    +
  • +
  • Les plugins v3 utilisent maintenant la journalisation python standard

    +
    import logging
    +
    +
    +

    Assurez-vous que la déclaration d’enregistreur suivante se trouve après le last _import_ en haut du plugin v3

    +
    logger = logging.getLogger(__name__)
    +
    +# To log a notification message:
    +logger.debug( ... )
    +logger.info( ... )
    +logger.warning( ... )
    +logger.error( ... )
    +logger.critical( ... )
    +
    +
    +

    Lors du portage des plugins v2 -> v3 : logger.x remplace parent.logger.x. +Parfois, il y a aussi self.logger x… je ne sais pas pourquoi… ne demandez pas.

    +
    +

    Note

    +

    Dans vi, vous pouvez utiliser le remplacement global pour effectuer un travail rapide lors du portage:

    +
    :%s/parent.logger/logger/g
    +
    +
    +
    +
  • +
  • En v2, parent est un gâchis. L’objet self variait en fonction des points d’entrée +appelé. Par exemple, self dans __init__ n’est pas identique à self dans on_message. En conséquence, tous les +variables d´états doivent être stocké dans le parent. l’objet parent contient des options, des paramètres et +les variable d´instance de la classe qui appelle le plugin.

    +

    Pour les attributs réels, sr3 fonctionne désormais comme les programmeurs python s’y attendent : self, +est le même self, dans __init__() et tous les autres points d’entrée, donc on peut définir des variables +d’état pour le plugin en utilisant les attributs self.x dans le code du plugin.

    +
  • +
  • Les plugins v3 ont des options comme argument pour le __init__ (self, options): routine plutôt +que dans la v2 où ils se trouvaient dans l’objet parent. Par convention, dans la plupart des modules, la +fonction __init__ comprend un:

    +
    super().__init__(options,logger)
    +self.o.add_option('OptionName', Type, DefaultValue)
    +
    +
    +
    +

    Note

    +

    Dans VI, vous pouvez utiliser le remplacement global:

    +
    :%s/parent/self.o/g
    +
    +
    +
    +
  • +
  • vous pouvez voir quelles options sont actives en démarrant un composant avec la commande ‘show’

    +
    sr3 show subscribe/myconf
    +
    +
    +

    ces paramètres sont accessibles à partir de self.o

    +
  • +
  • Dans les paramètres sr3, recherchez le remplacement de nombreux traits de soulignement par le camelCase

    +
    +
    correspondre à l’intention. ainsi:
      +
    • custom_setting_thing -> customSettingThing

    • +
    • post_base_dir -> post_baseDir

    • +
    • post_broker est inchangé.

    • +
    • post_base_url -> post_baseUrl

    • +
    +
    +
    +
  • +
  • Dans la v2, parent.msg stockait les messages, avec certains champs comme attributs intégrés et d’autres comme en-têtes. +Dans la v3 les messages de notification sont maintenant des dictionnaires python , donc un msg.relpath v2 devient msg[‘relPath’] dans la v3.

    +

    plutôt que d’être transmis via le parent, il existe une option worklist transmise aux points d’entrée du plugin qui manipulent +messages. par exemple, un on_message(self,parent) dans un plugin v2 devient un after_accept(self,worklist) dans sr3. +la liste de travail.incoming contient tous les messages qui ont passé le filtrage d’acceptation/rejet et seront traités +(pour télécharger, envoyer ou publier) donc la logique ressemblera à

    +
    for msg in worklist.incoming:
    +    do the same logic as in the v2 plugin.
    +    for one message at a time in the loop.
    +
    +
    +

    Les mappages de tous les points d’entrée sont décrits dans Mappage des points d’entrée v2 aux Callbacks v3 +section plus loin dans ce document

    +

    Chaque message de notification v3 agit comme un dictionnaire python. Ci-dessous un mappage de table +champs de la représentation sarra v2 à celle de sr3 :

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    v2

    sr3

    Notes

    msg.pubtime

    msg[‘pubTime’]

    quand le message a été initialement publié

    msg.baseurl

    msg[‘baseUrl’]

    racine de l’arborescence url du fichier annoncé.

    msg.relpath

    msg[‘relPath’]

    chemin relatif concaténé à baseUrl pour le chemin canonique

    no equivalent

    msg[‘retrievePath’]

    chemin opaque pour remplacer le chemin canonique.

    msg.notice

    pas disponible

    calculé à partir d’un autre champ sur l’écriture v2

    msg.new_subtopic

    msg[‘new_subtopic’]

    à éviter en sr3, champ calculé à partir de relPath

    msg.new_dir

    msg[‘new_dir’]

    nom de répertoire où le fichier sera écrite.

    msg.new_file

    msg[‘new_file’]

    nom de fichier à écrire en new_dir.

    msg.headers

    msg

    pour les champs variables/optionnels.

    msg.headers[‘x’]

    msg[‘x’]

    un message est un dict python

    msg.message_ttl

    msg[‘message_ttl’]

    le même option de réglage.

    msg.exchange

    msg[‘exchange’]

    le canal sur lequel le message à été reçu.

    msg.logger

    logger

    les journeaux fonctionnent ¨normalement” pour python

    msg.parts

    msg[‘size’]

    oublie ca, utilise une constructeur de sarracenia.Message

    msg.sumflg

    msg[‘identity’]

    oublie ca, utilise une constructeur de sarracenia.Message

    msg.rename

    msg[‘rename’]

    En sr3, souvent mieux d’utiliser: retrievePath et relPath

    parent.msg

    worklist.incoming

    sr3 traite des groupe des messages, pas individuelement

    +
  • +
  • pubTime, baseUrl, relPath, retrievePath, size, identity, sont tous des champs de message standard +mieux décrit dans sr_post(7)

  • +
  • si l’on a besoin de stocker par état de message, alors on peut déclarer des champs temporaires dans le message, +qui ne seront pas transmis lors de la publication du message. Il y a un champ défini msg[‘_deleteOnPost’]

    +
    msg['my_new_field'] = my_new_value
    +msg['_deleteOnPost'] |= set(['my_new_field'])
    +
    +
    +

    Sarracenia supprimera le champ donné du message avant de le publier pour les consommateurs en aval.

    +
  • +
  • dans les anciennes versions de v2 (<2.17), il y avait msg.local_file, et msg.remote_file, certains anciens plugins peuvent contenir +ce. Ils représentaient la destination dans les cas d’abonnement et d’expéditeur, respectivement. +les deux ont été remplacés par new_dir concaténé avec new_file pour couvrir les deux cas. +la séparation du répertoire et du nom de fichier a été considérée comme une amélioration.

  • +
  • dans la v2 parent était l’objet sr_subscribe, qui avait toutes ses variables d’instance, dont aucune +étaient destinés à être utilisés par des plugins. Dans les fonctions du plugin __init__(), elles +peuvent être référencées en tant que soi plutôt que parent :

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

    v2

    sr3

    Notes

    parent.currentDir

    msg[‘new_dir’] ?

    répertoire courant… ca dépend…

    parent.masks

    none

    valeur interne de la class sr_subscribe

    parent.program_name

    self.o.program_name

    nom de la programme qui execute e.g. ‘sr_subscribe’

    parent.consumer

    none

    ivaleur interne de la class sr_consumer

    parent.publisher

    none

    instance de Publisher de sr_amqp.py

    parent.post_hc

    none

    instance de HostConnect class from sr_amqp.py

    parent.cache

    none

    cache pour mémoriser les fichiers traités.

    parent.retry

    none

    fil d´attente pour les ressais.

    +

    Il existe des dizaines (des centaines ?) de ces attributs qui étaient destinés à servir de données internes au +sr_subscribe et ne devrait pas vraiment être disponible pour les plugins. +La plupart d’entre eux n’apparaissent pas, mais si un développeur a trouvé quelque chose, il peut être présent. +Difficile de prédire ce qu’un développeur de plugin utilisant l’une de ces valeurs attendait.

    +
  • +
  • Dans la v3 les messages de notification sont maintenant des dictionnaires python , donc msg.relpath dans v2 +devient msg[‘relPath’] dans la v3. Les messages de notification v3, car les dictionnaires sont la +représentation interne par défaut.

  • +
  • Dans la v3 les plugins fonctionnent sur des lots de messages de notification. v2 on_message obtient parent +comme paramètre, et le message de notification se trouve dans parent.message. Dans la v3, after_accept a worklist +comme option, qui est la liste python des messages, la longueur maximale étant fixée par l’option +batch. Donc, l’organisation générale pour after_accept, et after_work est:

    +
    new_incoming=[]
    +for message in old_list:
    +    if good:
    +       new_incoming.append(message)
    +    if bad:
    +       worklist.rejected.append(message)
    +worklist.incoming=new_incoming
    +
    +
    +
    +

    Note

    +

    les plugins doivent être déplacés du répertoire /plugins vers le répertoire /flowcb, +et plus précisément, les plugins on_message qui se transforment en plugins after_accept devraient être +placé dans le répertoire flowcb/accept (afin que les plugins similaires puissent être regroupés).

    +
    +

    Dans after_work, le remplacement de on_file dans v2, les opérations sont sur :

    +
      +
    • worklist.ok (transfert réussi.)

    • +
    • worklist.failed (transferts ayant échoué.)

    • +
    +

    Dans le cas de la réception d’un fichier .tar et de l’extension à des fichiers individuels, +la routine after_work modifierait le fichier worklist.ok pour qu’il contienne des messages de notification pour +les fichiers individuels, plutôt que les .tar collectifs d’origine.

    +
    +

    Note

    +

    les plugins on_file qui deviennent des plugins after_work doivent être placés dans le +répertoire /flowcb/after_work

    +
    +
  • +
  • v3 a pas besoin de définir des champs de message de notification dans les plugins +dans la v2, il faudrait définir partstr, et sumstr pour les messages de notification v2 dans les plugins. +Cela nécessitait une compréhension excessive des formats de message de notification et signifiait que la +modification des formats de message de notification demande de modifier les plugins (le format de message de +notification v03 est non pris en charge par la plupart des plugins v2, par exemple).

  • +
+
+

La manipulation de ces champs manuellement est activement contre-productif. +La somme de contrôle est déjà effectuée lorsque le nouveau message de notification est généré, donc très probablement +tous les champs de message tels que sumalgo et d’autres champs algo peuvent être ignorés.

+
+

Pour créer un message de notification à partir d’un fichier local dans un plugin v3

+
import sarracenia
+
+m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )
+
+
+

juste a regarder do_poll -> poll

+
+
+
    +
  • les plugins v3 impliquent rarement la sous-classification des classes de Moth ou de Transfer. +La classe sarracenia.moth implémente un support pour les protocoles de mise en fil d’attente +des messages de notification qui prennent en charge les abonnements basés sur la hiérarchie des topics. +Il y a actuellement deux sous-classes de Moth: amqp (pour rabbitmq) et mqtt. Ce serait +idéal pour quelqu’un d’ajouter un amq1 (pour le support qpid amqp 1.0.)

    +

    Il peut être raisonnable d’y ajouter une classe SMTP pour l’envoi d’e-mails, +Pas sûr.

    +

    Les classes sarracenia.transfer incluent http, ftp et sftp aujourd’hui. +Elles sont utilisés pour interagir avec des services distants qui fournissent une interface de fichier +(prise en charge de choses comme la liste des fichiers, le téléchargement et / ou l’envoi.) +D’autres sous-classes telles que S3, IPFS ou webdav, seraient des ajouts excellents.

    +
  • +
+
+
+

Fichiers de configuration

+

Dans la v2, l’option de configuration principale pour déclarer un plugin est

+
plugin X
+
+
+

D’une manière générale, il devrait y avoir un fichier plugins/x.py +avec une classe X.py dans ce fichier dans ~/.config/plugins +ou dans le répertoire sarra/plugins dans le paquet lui-même. +Il s’agit déjà d’un style de déclaration de plugin de deuxième génération +dans Sarracenia. La version originale, une personne déclare des points d’entrée individuels

+
on_message, on_file, on_post, on_..., do_...
+
+
+

Dans Sr3, les entrées ci-dessus sont considérées comme des demandes pour des plugins de v2, +et doit être utilisé que pour des raisons de continuité. +Idéalement, on devrait appeler les plugins v3 comme suit:

+
callback x
+
+
+

Où x sera une sous-classe de sarracenia.flowcb, qui +contiendra une classe X (première lettre en majuscule) dans le +fichier x.py quelque part dans le chemin de recherche python, ou dans le répertoire +sarracenia/flowcb qui est inclus dans le package. +Il s’agit en fait d’une version abrégée de l’importation python. +Si vous devez déclarer un rappel qui n’obéit pas à cette +convention, on peut aussi utiliser un manière plus flexible mais plus longue:

+
flowcb sarracenia.flowcb.x.X
+
+
+

les deux ci-dessus sont équivalents. La version flowcb peut être utilisée pour importer des classes +qui ne correspondent pas à la convention du x.X (un fichier nommé x.py contenant une classe appelée X)

+
+
+

Mise à niveau de la configuration

+

Une fois qu’un plugin est porté, on peut également faire en sorte que l’analyseur d’options v3 reconnaisse une +invocation de plugin de v2 et la remplace par une invocation v3. En regardant dans /sarracenia/config.py#L144, +il existe une structure de données convert_to_v3. Voici un exemple d’entrée

+
.
+.
+.
+'on_message' : {
+         'msg_delete': [ 'flowCallback': 'sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles' ]
+.
+.
+.
+
+
+

Un fichier de configuration v2 contenant une ligne on_message msg_delete sera remplacé par l’analyseur avec

+
flowCallback sarracenia.flowcb.filter.deleteflowfiles.DeleteFlowFiles
+
+
+
+
+

Options

+

Dans la v2, on déclarerait les paramètres à utiliser par un plugin dans la routine __init__, avec +le declare_option.:

+
parent.declare_option('poll_usgs_stn_file')
+
+
+

Les valeurs sont toujours de type list, donc généralement, on utilise la valeur en choisissant la première valeur:

+
parent.poll_usgs_stn_file[0]
+
+
+

Dans la v3, cela serait remplacé par

+
self.o.add_option( option='poll_usgs_stn_file', kind='str', default_value='hoho' )
+
+
+

Dans la v3 il y a maintenant des types (comme on le voit dans le fichier sarracenia/config.py#L777) et le paramètre +de valeur par défaut est inclus sans code supplémentaire. Il serait mentionné dans d’autres routines comme celle-ci:

+
self.o.poll_usgs_stn_file
+
+
+
+
+

Mappage des points d’entrée v2 aux Callbacks v3

+

Pour un aperçu complet des points d’entrée v3, jetez un coup d’œil : +https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/__init__.py

+

pour plus de détails.

+
+

on_message, on_post –> after_accept

+

v2 : reçoit un message de notification, renvoie True/False

+
+
v3: reçoit worklist

modifie worklist.incoming +transfert des messages de notification rejetés vers worklist.rejected ou worklist.failed.

+
+
+

Flux d’échantillon:

+
def after_accept(self, worklist):
+
+   ...
+
+   new_incoming=[]
+   for m in worklist.incoming:
+
+        if message is useful to us:
+           new_incoming.append(m)
+        else
+           worklist.rejected.append(m)
+
+   worklist.incoming = new_incoming
+
+
+
+
exemples:

v2: plugins/msg_gts2wistopic.py +v3: flowcb/wistree.py

+
+
+
+
+

on_file –> after_work

+

v2 : reçoit un message de notification, renvoie True/False

+
+
v3: reçoit worklist

modifie worklist.ok (transfer has already happenned.) +transfert des messages de notification rejetés vers worklist.rejected ou worklist.failed.

+

peut également être utilisé pour travailler sur worklist.failed (la logique de retry le fait.)

+
+
+

exemples:

+
+

Danger

+

IL N’Y A PAS D’EXEMPLES?!?! +TODO: ajouter quelques exemples

+
+
+
+

on_heartbeat -> on_housekeeping

+
+
v2: reçoit le parent comme argument.

fonctionnera inchangé.

+
+
+

v3: ne reçoit que self (qui devrait avoir self.o qui remplaçe le parent)

+

exemples:

+
+
    +
  • v2: hb_cache.py – nettoie la cache (références sr_cache.)

  • +
  • v3: flowcb/nodupe.py – implémente toute la routine de mise en cache.

  • +
+
+
+
+

do_poll -> poll

+

v2: appelez do_poll à partir du plugin.

+
+
    +
  • +
    le protocole d’utilisation de la routine do_poll est identifié par le point d’entrée registered_as()

    qui est obligatoire à fournir.

    +
    +
    +
  • +
  • nécessite la construction manuelle de champs pour les messages de notification, est-ce que la vérification du message de notification est spécifique, +(ne prennent généralement pas en charge les messages de notification v03.)

  • +
  • appelle explicitement les points d’entrée du poll.

  • +
  • fonctionne, il faut s’inquiéter de savoir si on a le vip ou non pour décider quel traitement +à faire dans chaque plugin.

  • +
  • paramètre poll_without_vip disponible.

  • +
+
+

Dans plusieurs sondages v2, un do_poll est associé à un point d’entrée download_quelquechose +où un message partiel est construit avec le sondage et celui du téléchargement (ou do_download) est spécialisé +pour effectuer le téléchargement proprement dit. Souvent, dans SR3, on peut créer un message qui sera téléchargé avec succès +avec le traitement de téléchargement intégré.

+

Un exemple de traitement de téléchargement personnalisé consiste à créer l’arborescence de répertoires dans laquelle télécharger, combinée avec +l’utilisation d’un en-tête rename (en v2 parent.msg.rename) On peut désormais utiliser “retrievePath” pour définir l’url +pour soumettre au serveur, et “relPath” pour définir où il sera placé localment. RelPath inclut +toute l’arborescence des répertoires, où rename est uniquement pour le nom du fichier. La combinaison de relPath et +retrievePath fournit souvent suffisamment de fonctionnalités pour éviter le besoin d’un point d’entrée de téléchargement.

+

Il existe un autre modèle courant dans les sondages v2 où, plutôt que d’interroger un serveur distant pour le savoir +quels nouveaux produits sont disponibles, dans sr3 nous avons le concept de flux programmé, où il y a un délai fixe +liste des demandes effectuées périodiquement. Voir « Flux programmé » pour en savoir plus à ce sujet. Pour les sondages typiques, la migration +à sr3 suit :

+

v3: définir poll dans une classe flowcb.

+
+
    +
  • le sondage n’est exécuté que lorsque has_vip est true.

  • +
  • le point d’entrée registered_as() est inutile.

  • +
  • toujours rassembler les exécutions, et est utilisé pour s’abonner à post effectuée par le nœud qui a le vip, +permettant a la cache nodupe d’être maintenu à jour.

  • +
  • API définie pour créer des messages de notification à partir de données de fichier, quel que soit le format du message de notification.

  • +
  • get est disparu, les poll utilisent accept/reject comme les autres composants.

  • +
  • renvoie une liste de messages de notification à filtrer et à publier.

  • +
  • l´option download permet un sondage (poll ou flow) de télécharger les données dans une seul configuration. +En v2, il faullt combiner avec une autre configuration pour effectuer le téléchargement.

  • +
+
+

Pour créer un message de notification, sans fichier local, utilisez fromFileInfo sarracenia.message factory:

+
import dateparser
+import paramiko
+import sarracenia
+
+gathered_messages=[]
+
+m = sarracenia.Message.fromFileInfo(sample_fileName, cfg)
+
+
+

génère un message de notification à partir de zéro.

+

On peut également construire et fournir un enregistrement de statistiques simulé à partir de l’usine fromFileInfo, +en utilisant la classe paramiko.SfTPAttributes(). Par exemple, en utilisant +les routines dateparser pour convertir. Toutefois, le serveur distant répertorie également la date et l’heure, et +détermine la taille du fichier et les autorisations en vigueur

+
pollmtime = dateparser.parse( ... , settings={ ... TO_TIMEZONE='utc' } )
+mtimestamp = time.mktime( pollmtime.timetuple() )
+
+fsize = info_from_poll #about the size of the file to download
+st = paramiko.SFTPAttributes()
+st.st_mtime=mtimstamp
+st.st_atime=mtimestamp
+st.st_size=fsize
+st.st_mode=0o666
+m = sarracenia.Message.fromFileInfo(sample_fileName, cfg, st)
+
+
+

Il faut remplir l’enregistrement SFTPAttributes si possible, puisque le doublon +de cache utilise les métadonnées si elles sont disponibles. Plus les métadonnées sont bonnes, le mieux est la +détection des modifications apportées aux fichiers existants.

+

Une fois le message de notification généré, ajoutez-le à la liste

+
gathered_messages.append(m)
+
+
+

et à la fin:

+
return gathered_messages
+
+
+
+
+

Traitement IP virtuel dans le poll

+

Dans la v2, si vous avez une séléction de vIP, tous les nœuds participants pollent le serveur en amont +et maintiennent la liste des fichiers actuels, ils ne publient tout simplement pas le résultat. +Donc, si vous avez 8 serveurs partageant un vIP, les huit sont des poll, un peu triste. +Il y a aussi le paramètre poll_no_vip, et les plugins doivent souvent vérifier s’ils +ont le vIP ou non.

+

Dans la v3, seul le serveur avec le vIP peux poller. Les plugins n’ont pas besoin de vérifier. +Les autres serveurs participants s’abonnent à l’endroit où le sondage est publié, +pour mettre à jour leur cache recent_files.

+
+
exemples:
    +
  • flowcb/poll/airnow.py

  • +
+
+
+

Dans un sondage v2, les échanges de sortie étaient parfois des échanges assez populaires (par exemple xpublic) +ce qui rendrait les files d’attente duplicate_suppression dans un sondage sr3 plus grand que nécessaire.

+

Lorsque vous utilisez un sondage dans sr3, idéalement, le post_exchange est dédié à cet +sondage, afin que les participants VIP minimisent la taille de leur cache de suppression des doublons +en le chargeant uniquement des éléments publiés par le sondage.

+
+
+

Flux programmé

+

S’il existe un serveur WISKIS ( https://www.kisters.net/wiski ), il faut émettre +Les requêtes, souvent centrées sur le temps sont à intervalles réguliers. donc un point d’entrée gather() +est implémenté qui renvoie une liste de messages qu’un téléchargeur utilisera pour obtenir les données.

+ +

Comme un sondage, on peut utiliser l’option download pour consommer les messages en les +téléchargeant dans la même configuration, ou publier sur un échange pour téléchargement +par un subscribe ou sarra pour plus de performance.

+
+
+

on_html_page -> sous-classement de flowcb/poll

+

Voici un plugin v2 nsa_mls_nrt.py:

+
#!/usr/bin/env python3
+
+class Html_parser():
+
+    def __init__(self,parent):
+
+        parent.logger.debug("Html_parser __init__")
+        import html.parser
+
+        self.parent = parent
+        self.logger = parent.logger
+
+        self.parser = html.parser.HTMLParser()
+        self.parser.handle_starttag = self.handle_starttag
+        self.parser.handle_data     = self.handle_data
+
+
+    def handle_starttag(self, tag, attrs):
+        for attr in attrs:
+            c,n = attr
+            if c == "href" and n[-1] != '/':
+               self.myfname = n.strip().strip('\t')
+
+    def handle_data(self, data):
+        import time
+
+        if 'MLS-Aura' in data:
+               self.logger.debug("data %s" %data)
+               self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' +'_' + ' ' + 'Jan 1 00:01' + ' ' + data
+               self.logger.debug("(%s) = %s" % (self.myfname,self.entries[self.myfname]))
+        if self.myfname == None : return
+        if self.myfname == data : return
+        '''
+        # at this point data is a filename like
+        name = data.strip().strip('\t')
+
+        parts = name.split('_')
+        if len(parts) != 3 : return
+
+        words = parts[1].split('.')
+        sdate  = ' '.join(words[:4])
+        t      = time.strptime(sdate,'%Y %j %H %M')
+
+        # accept file if 1 month old in sec  60 sec* 60min * 24hr * 31days
+
+        epochf = time.mktime(t)
+        now    = time.time()
+        elapse = now - epochf
+
+        if elapse > self.month_in_secs : return
+
+        # build an ls line from date in file ... size set to 0  since not provided
+
+        mydate = time.strftime('%b %d %H:%M',t)
+
+        mysize = '_'
+
+        self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' + mysize + ' ' + mydate + ' ' + data
+        self.logger.debug("(%s) = %s" % (self.myfname,self.entries[self.myfname]))
+        '''
+
+    def parse(self,parent):
+        self.logger.debug("Html_parser parse")
+        self.entries = {}
+        self.myfname = None
+
+        self.logger.debug("data %s" % parent.data)
+        self.parser.feed(parent.data)
+        self.parser.close()
+
+        parent.entries = self.entries
+
+        return True
+
+html_parser = Html_parser(self)
+self.on_html_page = html_parser.parse
+
+
+

Le plugin a une routine principale “parse”, qui appelle la classe html.parser, où data_handler +est appelé pour chaque ligne, en construisant progressivement le dictionnaire self.entries où chaque entrée est +une chaîne construite pour ressembler à une ligne de sortie de commande ls.

+

Ce plugin est une copie presque exacte du plugin html_page.py utilisé par défaut. +Le point d’entrée on_html_page pour les plugins est remplacé par un mécanisme complètement différent. +La plus grande partie de la logique du poll de v2 dans sr3 est dans la nouvelle class sarracenia.FlowCB.Poll. +La logique des plugins/html_page.py v2, utilisés par défaut, fait désormais partie de cette +nouvelle classe Poll, sous-classée à partir de flowcb, de sorte que l’analyse HTML de base est intégrée.

+

Un autre changement par rapport à la v2 est qu’il y avait beaucoup plus de manipulation de chaînes dans l’ancienne +version. Dans les poll sr3, la plupart des maniupulations de chaînes ont été remplacées par le remplissage d’une +structure paramiko.SFTPAttribute dès que possible.

+

Donc, la façon de remplacer on_html_page dans sr3 est de sous-classer Poll. Voici une +version sr3 du même plugin (nasa_mls_nrt.py):

+
import logging
+import paramiko
+import sarracenia
+from sarracenia import nowflt, timestr2flt
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+class Nasa_mls_nrt(Poll):
+
+    def handle_data(self, data):
+
+        st = paramiko.SFTPAttributes()
+        st.st_mtime = 0
+        st.st_mode = 0o775
+        st.filename = data
+
+        if 'MLS-Aura' in data:
+               logger.debug("data %s" %data)
+               #self.entries[self.myfname] = '-rwxr-xr-x 1 101 10 ' +'_' + ' ' + 'Jan 1 00:01' + ' ' + data
+               self.entries[data]=st
+
+               logger.info("(%s) = %s" % (self.myfname,st))
+        if self.myfname == None : return
+        if self.myfname == data : return
+
+
+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/nasa_mls_nrt.py ) +et le fichier de configuration correspondant fourni ici : +( https://github.com/MetPX/sarracenia/blob/development/sarracenia/examples/poll/nasa-mls-nrt.conf )

+

La nouvelle classe est déclarée comme une sous-classe de Poll, et seule la classe nécessaire +de routine HTML (handle_data) doit être écrite pour remplacer le comportement +fourni par la classe parente.

+

Cette solution est inférieure à la moitié de la taille de celle de la v2 et permet +toutes sortes de flexibilité en permettant le remplacement de tout ou une seule partie des éléments +de la classe de poll.

+
+
+
+

on_line -> sous-classement de poll

+

Comme on_html_page ci-dessus, toutes les utilisations de on_line dans la version précédente +concernaient le reformatage des lignes pour qu’elles puissent être analysées. La routine on_line peut être +sous-classé de la même manière pour le remplacer. Il fallait modifier la chaîne parent.line +pour qu’elle soit analysable par l’analyse de ligne de style ls intégrée.

+

Dans sr3, on_line devrait renvoyer un champ paramiko.SFTPAttributes rempli, similaire +à la façon dont on_html_page fonctionne (mais seulement un seul au lieu d’un dictionnaire d’entre eux.) +Avec l’analyse de date plus flexible dans sr3, il n’y a pas le besoin d’identifié de on_line +sur lequel construire un exemple.

+
+
+

do_send -> send:

+

v2 : do_send peut être une routine autonome ou associée à un type de protocole

+
    +
  • basé sur registered_as() afin que la destination détermine si elle est utilisée ou non.

  • +
  • accepte parent comme argument.

  • +
  • renvoie True en cas de réussite, False en cas d’échec.

  • +
  • aura généralement un point d’entrée registered_as() pour indiquer les protocoles pour lesquels utiliser un sender.

  • +
+

v3: send(self,msg)

+
    +
  • utilisez le msg fourni pour effectuer l’envoi.

  • +
  • renvoie True en cas de réussite, False en cas d’échec.

  • +
  • registered_as n’est plus utilisé, peut être supprimé.

  • +
  • Le entry_point d’envoi remplace tous les envois et n’est pas spécifique au protocole. +Pour ajouter la prise en charge de nouveaux protocoles, il faut sous-classer sarracenia.transfer à la place.

  • +
+
+
exemples:
    +
  • flowcb/send/email.py

  • +
+
+
+
+
+

do_download -> download:

+

créer une classe flowCallback avec un point d’entrée download.

+
    +
  • accepte un seul message de notification comme argument.

  • +
  • renvoie la valeur True si le téléchargement réussit.

  • +
  • s’il renvoie False, la logique de nouvelle tentative s’applique (le téléchargement sera appelé à nouveau +puis placé dans la fil d’attente de nouvelles tentatives, retry queue.)

  • +
  • utiliser msg[‘new_dir’], msg[‘new_file’], msg[‘new_inflight_path’] +pour respecter les paramètres tels que inflight et placer le fichier correctement. +(à moins que changer cela soit la motivation du plugin.)

  • +
  • peut être une bonne idée de vérifier la somme de contrôle des données téléchargées. +Si la somme de contrôle du fichier téléchargé n’est pas en accord avec ce qui se trouve dans +le message de notification, la suppression des doublons échoue et ca boucle.

  • +
  • un cas de téléchargement est lorsque retrievalURL n’est pas un téléchargement de fichier normal. +Dans v03, il existe des champs retrievePath pour exactement ce cas. Cette nouvelle fonctionnalité +peut être utilisé pour éliminer le besoin de plugins de téléchargement. Exemple:

    +

    Dans la v2:

    +
    +
    +

    est porté sur sr3 :

    +
    +
    +

    Le résultat porté définit le nouveau champ retrievePath (chemin de récupération) au lieu de new_dir et new_file +et le traitement normal du champ retrievePath dans le message de notification fera un bon téléchargement, aucun +plugin est requis.

    +
  • +
  • Souvent, dans les sondages v2, plutôt que d’interroger un serveur distant pour savoir quels nouveaux produits +sont disponibles, dans sr3 nous avons le concept de flux planifié, où il y a une liste fixe de requêtes effectuées

    +
    +

    périodiquement. Voir « Flux programmé » pour en savoir plus à ce sujet. Pour les sondages typiques, la migration vers sr3 suit :

    +
    +
  • +
+
+

DESTFNSCRIPT

+

DESTFNSCRIPT est refondu en tant que point d’entrée flowcb, où la directive est maintenant formatée d’une manière +similaire au flowcallback dans la configuration

+

configuration V2:

+
accept .*${HOSTNAME}.*AWCN70_CWUL.*       DESTFNSCRIPT=sender_renamer_add_date.py
+
+
+

Code du plugin v2:

+
import sys, os, os.path, time, stat
+
+# this renamer takes file name like : AACN01_CWAO_160316___00009:cmcin:CWAO:AA:1:Direct:20170316031754
+# and returns :                       AACN01_CWAO_160316___00009_20170316031254
+
+class Renamer(object):
+
+  def __init__(self) :
+      pass
+
+  def perform(self,parent):
+
+      path = parent.new_file
+      tok=path.split(":")
+
+      datestr = time.strftime('%Y%m%d%H%M%S',time.gmtime())
+      #parent.logger.info('Valeur_path: %s' % datstr)
+
+      new_path=tok[0] + '_' + datestr
+      parent.new_file = new_path
+      return True
+
+renamer=Renamer()
+self.destfn_script=renamer.perform
+
+
+

Se transforme en sr3

+

configuration SR3:

+
accept .*${HOSTNAME}.*AWCN70_CWUL.*       DESTFNSCRIPT=sender_renamer_add_date.Sender_Renamer_Add_Date
+
+
+

In sr3, as for any flowcallback invocation, one needs to use a traditional python class invocation +and add to it the name of the class within the file. This notation is equivalent to python from +statement from sender_renamer_add_date import Sender_Renamer_Add_Date

+

Dans sr3, comme pour tout appel flowcallback, il faut utiliser un appel de classe python traditionnel +et ajouter le nom de la classe dans le fichier. Cette notation est équivalente à l’instruction python from, +from sender_renamer_add_date import Sender_Renamer_Add_Date

+

code du flow callback:

+
import logging,time
+
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+class Sender_Renamer_Add_Date(FlowCB):
+
+   def destfn(self,msg) -> str:
+
+       logger.info('before: m=%s' % msg )
+       relPath = msg["relPath"].split('/')
+       datestr = time.strftime('%Y%m%d%H%M%S',time.gmtime())
+       return relPath[-1] + '_' + datestr
+
+
+
+
Exemple de débogage des fonctions destfn sr3 ::

fractal% python3 +Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0] on linux +Type “help”, “copyright”, “credits” or “license” for more information. +>>> from sender_renamer_add_date import Sender_Renamer_Add_Date +>>> fb=Sender_Renamer_Add_Date(None) +>>> msg = { ‘relPath’ : ‘relative/path/to/file.txt’ } +>>> fb.destfn(msg) +‘file.txt_20220725130328’ +>>>

+
+
+
+
+
+

v3 seulement: post,gather

+

Le polling/posting est en fait effectuée dans des classes de rappel de flux (flowcb). +Le statut de sortie n’a pas d’importance, toutes ces routines seront appelées dans l’ordre.

+

Le retour d’un gather est une liste de messages de notification à ajouter à worklist.incoming

+

Le retour d’un post n’est pas défini. Le but est de créer un effet secondaire +qui affecte un autre processus ou serveur.

+
+
exemples:
    +
  • flowcb/gather/file.py - lire des fichiers à partir du disque (pour le post et watch)

  • +
  • flowcb/gather/message.py - comment les messages de notification sont reçus par tous les composants

  • +
  • flowcb/post/message.py - comment les messages de notification sont publiés par tous les composants.

  • +
  • flowcb/poll/nexrad.py - cela poll le serveur AWS de la NOAA pour les données. +installer une configuration pour l’utiliser avec sr3 add poll/aws-nexrad.conf

  • +
+
+
+
+
+

v3 Exemples complexes

+
+

flowcb/nodupe

+

suppression des doublons dans la v3, a:

+
    +
  • +
    un after_accept qui achemine les doublons à partir de worklist.incoming.

    ( ajout de non-dupes à la cache de réception.)

    +
    +
    +
  • +
+
+
+

flowcb/retry

+
+
    +
  • dispose d’une fonction after_accept pour ajouter des messages de notification à la +fil d’attente entrante, afin de déclencher une autre tentative de traitement.

  • +
  • a une routine after_work faisant quelque chose d’inconnu … FIXME.

  • +
  • a une fonction de publication pour prendre les téléchargements échoués et les mettre +sur la liste des nouvelles tentatives pour un examen ultérieur.

  • +
+
+
+
+
+

Table of v2 and sr3 Equivalents

+

Voici un aperçu des plugins inclus dans Sarracenia, +On peut parcourir les deux arbres, et à l’aide du tableau ci-dessous, +peut examiner, comparer et contraster les implémentations.

+ +

La dénomination donne également un exemple de mappage de convention de nom… par ex. plugins dont le nom v2 commence par :

+
    +
  • msg_… -> filter/… où accept/…

  • +
  • file_… -> work/…

  • +
  • poll_… -> poll/… où gather/…

  • +
  • hb_… -> housekeeping/…

  • +
+

sont mappés aux répertoires conventionnels sr3 à droite.

+

Les chemins relatifs des dossiers ci-dessus sont indiqués dans le tableau (les liens sont dans le code source, donc en anglais):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

V2 plugins (all in one directory…)

Sr3 flow callbacks (treeified)

data_…

+

modifier le fichier en vol.

+

subclass sarracenia.transfer

+

pas d´exemple disponible actuelement, veuillez consulter le code source.

+

destfn_sample.py

destfn/sample.py

file_age.py

work/age.py

file_delete.py

work/delete.py

file_email.py

send/email.py

file_rxpipe.py

work/rxpipe.py

hb_memory

housekeeping/resources.py

html_page.py

subclass sarracenia.transfer

+

pas d´exemple disponible actuelement, veuillez consulter le code source.

+

voir poll/nasa_mls_nrt.py comme exemple de tel cas.

+

msg_2http.py

accept/tohttp.py

msg_2localfile.py, msg_2local.py (not sure)

accept/tolocalfile.py

msg_delete.py

filter/deleteflowfiles.py

msg_fdelay.py

filter/fdelay.py

msg_filter_wmo2msc.py

filter/wmo2msc.py

msg_log.py,file_log.py, hb_log.py, post_log.py

log.py

msg_pclean.py, msg_pclean_f90.py

+

msg_pclean_f92.py

+

pclean.py +filter/pcleanf90.py

+

filter/pcleanf92.py <../../Reference/flowcb.html#module-sarracenia.flowcb.filter.pcleanf92>`_

+

post_rate_limit.py

incorporé dans l´application messageRateMax

msg_rename_dmf.py

accept/renamedmf.py

msg_rename_whatfn.py

accept/renamewhatfn.py

msg_rename4jicc.py

accept/rename4jicc.py

msg_stopper.py

incorporé dans l´application messageCountMax

msg_sundew_pxroute.py

accept/sundewpxroute.py

msg_speedo.py

accept/speedo.py

msg_to_clusters.py

accept/toclusters.py

msg_WMO_type_suffix.py

accept/wmotypesuffix.py

anciennement inclu dans l´application +suppresion de duplicata +hb_cache.py

nodupe/__init__.py

incoporé dan l´appli message subscriber

gather/message.py

incoporé dan l´appli message poster

post/message.py

incoporé dan l´appli file scan or noticing.

gather/file.py

incoporé dan l´appli retry logic

+

hb_retry.py

+

retry.py

poll_email.py

poll/mail.py

poll_nexrad.py

poll/nexrad.py

poll_noaa.py

poll/noaa_hydrometric.py

poll_usgs.py

poll/usgs.py

spare

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/AMQPprimer.html b/fr/Contribution/AMQPprimer.html new file mode 100644 index 000000000..3041b079c --- /dev/null +++ b/fr/Contribution/AMQPprimer.html @@ -0,0 +1,514 @@ + + + + + + + AMQP - Introduction à sarracénia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

AMQP - Introduction à sarracénia

+

Il s’agit d’un briefing court mais plutôt dense pour expliquer +la motivation de l’utilisation de l’AMQP par la pompe de données MetPX-Sarracenia. +Sarracenia est essentiellement une application AMQP, +il est donc très utile de comprendre l’AMQP. +L’AMQP est un sujet vaste et intéressant en soi. Aucune tentative n’est faite pour expliquer +tout cela ici. Ce mémoire fournit juste un peu de contexte et ne présente que des +concepts de base nécessaires pour comprendre et/ou utiliser Sarracenia. Pour plus d’informations +sur l’AMQP lui-même, un ensemble de liens est maintenu à +sur le site web Metpx mais un moteur de recherche +révélera également une richesse de matériel.

+ +
+

Sélection des fonctionnalités AMQP

+

AMQP est un protocole universel de transmission de messages avec de nombreuses +options pour prendre en charge de nombreux modèles de messagerie différents. MetPX-sarracenia spécifie et utilise un +petit sous-ensemble de modèles AMQP. Un élément important du développement de Sarracenia a été de +choisir parmi les nombreuses possibilités un petit sous-ensemble de méthodes qui sont générales et +facile à comprendre, afin de maximiser le potentiel d’interopérabilité.

+
+

Analogie FTP

+

Spécifier l’utilisation d’un protocole seul peut être insuffisant pour fournir suffisamment d’informations pour +l’échange de données et l’interopérabilité. Par exemple, lors de l’échange de données via FTP, un certain nombre de choix +doivent être fait au-delà du protocole.

+
    +
  • utilisation authentifiée ou anonyme ?

  • +
  • comment signaler qu’un transfert de fichier est terminé (bits d’autorisation? suffixe? préfixe?)

  • +
  • convention de nommage

  • +
  • transfert binaire ou texte

  • +
+

Des conventions convenues au-delà du simple FTP (IETF RFC 959) sont nécessaires. +Semblable à l’utilisation de FTP seul comme protocole de transfert est insuffisant +pour spécifier une procédure complète de transfert de données, l’utilisation d’AMQP, +sans plus d’informations, est incomplète. L’intention des conventions superposées à +l’AMQP est d’être un montant minimum pour obtenir un échange de données significatif.

+
+
+

AMQP: pas 1.0, mais 0.8 or 0.9

+

AMQP 1.0 standardise le protocole sur le fil, mais supprime toute normalisation des courtiers. +Étant donné que l’utilisation de courtiers est essentielle à l’utilisation de Sarracenia, +était un élément fondamental des normes antérieures, +et comme la norme 1.0 est relativement controversée, ce protocole suppose un courtier standard pré 1.0, +comme il est fourni par de nombreux courtiers gratuits, tels que rabbitmq et Apache QPid, souvent appelés 0.8, +mais les courtiers de 0,9 et de post-0,9 pourraient bien interagir.

+
+
+

Échanges et files d’attente nommés

+

Dans AMQP avant la version 1.0, de nombreux acteurs différents peuvent définir des paramètres de communication, tels que les échanges +auquels publier, les files d’attente où les messages de notification s’accumulent et les liaisons entre les deux. Les applications +et les utilisateurs déclarent et utilisent leurs échanges, files d’attente et liaisons. Tout cela a été abandonné +dans le passage à la version 1.0 faisant des échanges thématiques, un fondement important des modèles pub/sub +beaucoup plus difficile.

+

dans AMQP 0.9, un abonné peut déclarer une fil d’attente, puis plusieurs processus (avec les bonnes +autorisations et le nom de la fil d’attente) peuvent consommer à partir de la même fil d’attente. Cela nécessite d’être capable +de nommer la fil d’attente. Dans un autre protocole, tel que MQTT, on ne peut pas nommer la fil d’attente, et donc +ce modèle de traitement n’est pas pris en charge.

+

La convention de mappage décrite dans `Topic < ../../../Reference/sr_post.7.rst#topic>`_, permet à +MQTT d’établir des hiérarchies distinctes qui fournissent une distribution fixe entre +les travailleurs, mais pas exactement la fil d’attente partagée auto-équilibrée fournie par AMQP.

+
+

Note

+

Dans RabbitMQ (le broker initial utilisé), les autorisations sont attribuées à l’aide d’expressions régulières. Alors +un modèle d’autorisation où les utilisateurs AMQP peuvent définir et utiliser leurs échanges et files d’attente +est appliqué par une convention d’affectation de noms facilement mappée aux expressions régulières (toutes ces +ressources incluent le nom d’utilisateur vers le début). Les échanges commencent par : xs_<user>_. +Les noms des files d’attente commencent par : q_<user>_.

+
+
+
+

Échanges thématiques

+

Les échanges thématiques sont utilisés exclusivement. AMQP prend en charge de nombreux autres +types d’échanges, mais la rubrique de sr3_post est envoyée afin de prendre en charge le filtrage +côté serveur à l’aide du filtrage par rubrique. À l’AMQP 1.0, les échanges thématiques +(en fait, tous les échanges ne sont plus définis). Le filtrage côté serveur permet d’utiliser +beaucoup moins de hiérarchies de rubriques et d’utiliser des sous-divisions beaucoup plus efficaces.

+

Dans Sarracenia, les topics sont choisis pour refléter le chemin des fichiers annoncés, ce qui permet le +filtrage direct côté serveur, à compléter par un filtrage côté client sur la +réception des messages.

+

La racine de l’arborescence des rubriques est la version de la charge utile du message. Cela permet aux courtiers individuels +de facilement prendre en charge plusieurs versions du protocole en même temps pendant les transitions. v02, +créé en 2015, est la troisième itération du protocole et les serveurs existants prennent régulièrement en charge les +versions précédentes simultanément de cette manière. La deuxième sous-rubrique définit le type de message. +Au moment de la rédaction de cet article : v02.post est le préfixe de rubrique pour les messages de notification actuels.

+
+
+

Peu de données

+

Les messages AMQP contiennent des messages de notification, pas de données de fichier réelles. AMQP est optimisé pour et suppose +de petits messages. Garder les messages petits permet un maximum de transmission de messages et permet +aux clients d’utiliser des mécanismes de priorité basés sur le transfert de données, plutôt que sur les messages de notification. +Accommoder de grands messages créerait de nombreuses complications pratiques et nécessiterait inévitablement +la définition d’une taille de fichier maximale à inclure dans le message lui-même, ce qui entraîne une +complexité pour couvrir plusieurs cas.

+

Sr3_post est destiné à être utilisé avec des fichiers arbitrairement volumineux, via la segmentation et le multi-streaming. +Les blocs de fichiers volumineux sont annoncés indépendamment et les blocs peuvent suivre différents chemins +entre la pompe initiale et la livraison finale. Le protocole est unidirectionnel,parce qu’il +n’y a pas de dialogue entre l’éditeur et l’abonné. Chaque publication est un élément autonome qui +est un message dans un flux qui, à la réception, peut être réparti sur un certain nombre de nœuds.

+

Cependant, il est probable que, pour les petits fichiers sur des liens à latence élevée, il est +plus efficace d’inclure le corps des fichiers dans les messages de notification eux-mêmes, +plutôt que de forcer une phase de récupération séparée. L’avantage relatif dépend de :

+
    +
  • la grossièreté relative du filtrage côté serveur signifie qu’un certain filtrage est effectué sur +côté client. Toutes les données incorporées pour les messages de notification ignorées côté client +sont des déchets.

  • +
  • Sarracenia établit des connexions à longue durée de vie pour certains protocoles, tels que SFTP, +la surcharge relative pour une récupération peut donc ne pas être longue.

  • +
  • On atteindra un taux de messagerie plus élevé sans que les données soient intégrées, et si +les messages de notification sont distribués à un certain nombre de travailleurs, il est possible que le résultat +de taux de messages est plus élevé sans données intégrées (en raison d’une distribution plus rapide pour +téléchargement parallèle) que les économies réalisées grâce à l’intégration.

  • +
  • plus la latence de la connexion est faible, moins l’avantage de performance est faible +d’intégration, et plus cela devient un facteur limitant sur la haute performance de transferts.

  • +
+

D’autres travaux sont nécessaires pour mieux clarifier quand il est judicieux d’intégrer du contenu +dans les messages de notification. Pour l’instant, l’en-tête content est inclus pour permettre de telles expériences +à se produire.

+
+
+

Autres paramètres

+

AMQP a de nombreux autres paramètres et une fiabilité pour un cas d’utilisation particulier +est assuré en faisant les bons choix.

+
    +
  • persistance (les files d’attente survivent aux redémarrages du courtier, par défaut true),

  • +
  • expiration (combien de temps une fil d’attente doit exister lorsque personne n’en consomme. Valeur par défaut : quelques +minutes pour le développement, mais peut être réglé beaucoup plus longtemps pour la production)

  • +
  • message_ttl (durée de vie des messages de notification en fil d’attente. Les messages trop vieux ne le pourront pas +être livré : la valeur par défaut est éternelle.)

  • +
  • La pré-récupération est un AMQP réglable pour déterminer le nombre de messages de notification qu’un client va +récupérer à partir d’un courtier à la fois, en optimisant le streaming. (valeur par défaut : 25)

  • +
+

Ceux-ci sont utilisés dans les déclarations de files d’attente et d’échanges pour fournir des +traitement des messages. Cette liste n’est pas exhaustive.

+
+
+
+

Mappage des concepts AMQP à Sarracenia

+../../_images/AMQP4Sarra.svg +

Un serveur AMQP est appelé Broker. Broker est parfois utilisé pour désigner le logiciel, +d’autres fois le serveur exécutant le logiciel broker (même confusion que serveur web.) Dans le diagramme ci-dessus, +Le vocabulaire AMQP est en orange et les termes Sarracenia sont en bleu.

+

Il existe de nombreuses implémentations de logiciels de courtage différentes. Nous utilisons rabbitmq. +Ne pas essayer d’être spécifique à rabbitmq, mais les fonctions de gestion diffèrent d’une implémentation à l’autre. +Ainsi, les tâches d’administration nécessitent un ‘portage’ alors que les principaux éléments de l’application ne le font pas.

+
+
Queues sont généralement prises en charge de manière transparente, mais vous devez savoir
    +
  • Un consommateur/abonné crée une fil d’attente pour recevoir des messages de notification.

  • +
  • Les files d’attente des consommateurs sont liées aux échanges (AMQP-speak)

  • +
+
+
Un exchange est un entremetteur entre les files d’attente publisher et consumer.
    +
  • Un message arrive d’un éditeur.

  • +
  • le message va à l’échange, est-ce que quelqu’un s’intéresse à ce message?

  • +
  • dans un échange basé sur une rubrique, la rubrique du message fournit le exchange key.

  • +
  • intéressé : comparer la clé de message aux liaisons des files d’attente des consommateurs.

  • +
  • le message est acheminé vers les files d’attente des consommateurs intéressés, ou supprimé s’il n’y en a pas.

  • +
+
+
+
    +
  • +
    Plusieurs processus peuvent partager une queue, ils en suppriment simplement à tour de rôle les messages de notification.
      +
    • Ceci est fortement utilisé pour sr_sarra et sr_subcribe plusieurs instances.

    • +
    +
    +
    +
  • +
  • Les Queues peuvent être durables, donc même si votre processus d’abonnement meurt, +si vous revenez dans un délai raisonnable et que vous utilisez la même fil d’attente, +vous n’aurez manqué aucun message de notification.

  • +
  • +
    Comment décider si quelqu’un est intéressé.
      +
    • Pour Sarracenia, nous utilisons (norme AMQP) topic based exchangess.

    • +
    • Les abonnés indiquent les sujets qui les intéressent, et le filtrage se produit côté serveur/courtier.

    • +
    • Les sujets ne sont que des mots-clés séparés par un point. caractères génériques : # correspond à n’importe quoi,

      +
        +
      • correspond à un mot.

      • +
      +
    • +
    • Nous créons la hiérarchie des rubriques à partir du nom du chemin (mappage à la syntaxe AMQP)

    • +
    • La résolution et la syntaxe du filtrage des serveurs sont définies par AMQP. (. séparateur, caractères génériques # et *)

    • +
    • Le filtrage côté serveur est grossier, les messages de notification peuvent être filtrés +après le téléchargement à l’aide de regexp sur les chemins réels (les directives reject/accept).

    • +
    +
    +
    +
  • +
  • +
    préfixe de sujet ? Nous commençons l’arborescence des sujets avec des champs fixes
      +
    • v02 la version/format des messages de notification sarracenia.

    • +
    • Publier… le type de message, il s’agit d’un message de notification +d’un fichier (ou d’une partie d’un fichier) disponible.

    • +
    +
    +
    +
  • +
+
+
+

Sarracenia est une application MQP

+

Dans la version 2, MetPX-Sarracenia n’est qu’un léger wrapper/revêtement autour de l’AMQP. +Dans la version 3, cela a été retravaillé et un pilote MQTT a été ajouté pour le rendre +moins spécifique à l’AMQP.

+
    +
  • Une pompe MetPX-Sarracenia est une application PYTHON AMQP qui utilise un courtier (rabbitmq) +pour coordonner les transferts de données client SFTP et HTTP, et accompagne un +serveur Web (apache) et serveur sftp (openssh) sur la même adresse d’utilisateur.

  • +
  • Chaque fois que cela est raisonnable, nous utilisons leur terminologie et leur syntaxe. +Si quelqu’un connaît l’AMQP, il comprend. Sinon, ils peuvent faire des recherches.

  • +
  • Les utilisateurs configurent un broker, au lieu d’une pompe.

    +
      +
    • les utilisateurs peuvent explicitement choisir leurs noms de queue.

    • +
    • les utilisateurs définissent subtopic,

    • +
    • les sujets avec séparateur de points sont peu transformés, plutôt que codés.

    • +
    • fil d’attente durable.

    • +
    • nous utilisons des message headers (AMQP-speak pour les paires clé-valeur) plutôt que l’encodage en JSON ou dans un autre format de charge utile.

    • +
    +
  • +
  • réduire la complexité par le biais de conventions.

    +
    +
      +
    • n’utiliser qu’un seul type d’échanges (Topic), s’occuper des liaisons.

    • +
    • les conventions de nommage pour les échanges et les files d’attente.

      +
      +
        +
      • les échanges commencent par x.

        +
          +
        • xs_Weather - l’échange pour la source (utilisateur amqp) nommée Météo pour poster des messages de notification

        • +
        • xpublic – échange utilisé pour la plupart des abonnés.

        • +
        +
      • +
      • les files d’attente commencent par q

      • +
      +
      +
    • +
    +
    +
  • +
  • Les ressources Internet sont plus utiles et réduisent notre charge de documentation.

  • +
  • Nous écrivons moins de code (exposer l’AMQP brut signifie moins de colle.)

  • +
  • Moins de potentiel de bugs / plus de fiabilité.

  • +
  • nous faisons un nombre minimum de choix/restrictions

  • +
  • définir des valeurs par défaut raisonnables.

  • +
+
+
+

Révision

+

Si vous avez compris le reste du document, cela devrait avoir du sens pour vous :

+

Un courtier AMQP est un processus de serveur qui héberge les échanges et les files d’attente +utilisés pour acheminer les messages de notification +avec une latence très faible. Un éditeur envoie des messages de notification à un échange, tandis qu’un consommateur lit les +messages de notification de leur fil d’attente. Les files d’attente sont liées aux échanges. Sarracenia lie un courtier +à un serveur Web pour fournir des notifications rapides et utilise des échanges de sujets pour activer le +filtrage côté serveur des consommateurs. L’arborescence des rubriques est basée sur l’arborescence de fichiers que vous pouvez +naviguez si vous visitez le serveur Web correspondant.

+
+
+

Annexe A : Contexte

+
+

Pourquoi utiliser AMQP?

+
    +
  • standard ouvert, multiples implémentations libres.

  • +
  • transmission de messages à faible latence.

  • +
  • encourage les modèles/méthodes asynchrones.

  • +
  • langue, protocole et fournisseur neutres.

  • +
  • très fiable.

  • +
  • adoption robuste (deux sections suivantes à titre d’exemples)

  • +
+
+
+

D’où vient l’AMQP?

+
    +
  • Norme ouvertes international du monde financier.

  • +
  • De nombreux systèmes propriétaires similaires existent, AMQP construit pour échapper au verrouillage. +Le standard est construit avec une longue expérience des systèmes de messagerie des fournisseurs, et donc assez mature.

  • +
  • invariablement utilisé dans les coulisses en tant que composant dans le traitement côté serveur, et non visible par l’utilisateur.

  • +
  • de nombreuses entreprises web (soundcloud)

  • +
  • voir une bonne adoption dans la surveillance et l’intégration pour le HPC

  • +
+
+
+

Pile Intel/Cray HPC

+

Intel/Cray HPC stack

+../../_images/IntelHPCStack.png + +
+
+

OpenStack

+

AMQP is the messaging technology chosen by the OpenStack cloud.

+../../_images/OpenStackArch.png + +
+
+

Comment adopter l’AMQP

+

Adopter AMQP ressemble plus à l’adoption de XML qu’à l’adoption de FTP. l’Interopérabilité FTP +est facile car les choix sont limités. Avec XML, cependant, vous obtenez plus de palette que de peinture. Beaucoup +de dialectes différents, méthodes de schéma, etc… XML sera valide et analysé, mais sans +normalisation supplémentaire, l’échange de données reste incertain. Pour une réelle interopérabilité, +il faut normaliser des dialectes spécifiques. Exemples:

+
+
    +
  • RSS/Atom,

  • +
  • Common Alerting Protocol (CAP)

  • +
+
+

Les courtiers AMQP et le logiciel client peuvent se connecter et envoyer des messages de notification, mais sans +normalisation supplémentaire, les applications ne communiqueront pas. AMQP appellent +ces couches supplémentaires applications. AMQP permet tous les messages imaginables, +de sorte qu’une application bien formée est construite en éliminant les fonctionnalités de +considération, choix des couleurs à utiliser. +Sarracenia est une application de message AMQP passant au transfert de fichiers.

+

Au fur et à mesure que CAP réduit le XML, Sarracenia réduit la portée de l’AMQP. Ce rétrécissement est +nécessaire pour obtenir un résultat utile : +l’interopérabilité. Les conventions et formats de Sarracenia sont définis dans :

+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/Design.html b/fr/Contribution/Design.html new file mode 100644 index 000000000..02dbfe8eb --- /dev/null +++ b/fr/Contribution/Design.html @@ -0,0 +1,887 @@ + + + + + + + 1 Conception Strawman — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Draft

+
+

1 Conception Strawman

+

Ce document reflète la conception actuelle résultant de discussions et de réflexions +à un niveau plus détaillé que le document d’esquisse. Voir Aperçu +pour une vue d’ensemble des exigences de conception. Voir ‘cas d’utilisation use-cases.html.html pour +une exploration de la fonctionnalité du fonctionnement de cette conception dans différentes situations. +La manière de progresser vers une mise en œuvre fonctionnelle est décrite dans le plan.

+ +
+

1.1 Hypothèses/contraintes

+
+
    +
  • Existe-t-il des systèmes de fichiers en cluster disponibles partout ? Non.

  • +
  • Une équipe opérationnelle peut vouloir surveiller/alerter lorsque certains transferts rencontrent des difficultés.

  • +
  • La sécurité peut vouloir exécuter une analyse différente sur différents trafics (chaque bloc?) +La sécurité peut nous demander de refuser certains types de fichiers, afin qu’ils subissent des analyses plus lourdes. +ou effectuez une analyse plus lourde sur ces types de fichiers.

  • +
  • Les zones extranet ne peuvent pas initier de connexions aux zones internes. +Les zones extranet reçoivent des connexions entrantes de n’importe où.

  • +
  • Les zones d’opérations gouvernementales peuvent initier des connexions n’importe où. +cependant, la science est considérée comme une sorte d’extranet pour tous les partenaires.

  • +
  • Personne ne peut initier de connexions dans les réseaux partenaires, mais tous les départements partenaires +peuvent initier des connexions dans science.gc.ca zone. Dans les zones scientifiques, il y a le système de +fichiers partagés, où les serveurs accèdent à un système de fichiers commun orienté cluster, ainsi +qu’à quelques petites zones restreintes, où l’accès est très limité pour assurer la disponibilité.

  • +
  • Au sein du CNRC, il y a des laboratoires avec de l’équipement qui ne peut pas être entretenu, du point de vue des logiciels, +pour corriger les vulnérabilités divulguées en raison de dépendances excessives aux tests (c.-à-d. certifier +qu’un agitateur de train fonctionne toujours après l’application d’un patch.) Ces systèmes n’ont pas accès +à Internet, seulement à quelques autres systèmes sur le site.

  • +
  • Les collaborateurs sont des entités universitaires, d’autres gouvernements ou commerciaux dont les +scientifiques du gouvernement échangent des données.

  • +
  • Les collaborateurs se connectent aux ressources extranet à partir de leurs propres réseaux. +De la même façon que des partenaires, (sous réserve d’exceptions) aucune connexion ne peut être initiée +à un réseau de collaborateurs.

  • +
  • Il n’y a pas de proxys, aucun système dans l’extranet n’a reçu d’autorisations exceptionnelles pour +Initier des connexions entrantes. Protocoles de stockage de fichiers, etc. sont complètement isolés entre +eux. Aucun système de fichiers ne traverse les limites des zones réseau.

  • +
  • Une méthode pour améliorer la fiabilité du service consiste à utiliser les services internes à des fins internes +et réserver des services destinés au public aux utilisateurs externes. Des services isolés à l’intérieur +sont complètement imperméables aux ‘intempéries’ d’Internet (DDOS de diverses formes, charge, etc…) +Les charges internes et externes peuvent être mises à l’échelle indépendamment.

  • +
+
+
+
+

1.2 Nombre de commutateurs

+

L’application est censée prendre en charge n’importe quel nombre de topologies, c’est-à-dire +n’importe quel nombre de pompes S = 0,1,2,3 +peut exister entre l’origine et la livraison finale, et faire ce qu’il faut.

+

Pourquoi tout n’est-il pas point à point, ou quand insérez-vous une pompe?

+
+
    +
  • les règles de topologie/firewall du réseau nécessitent parfois d’être au repos dans une zone de transfert entre deux +organisations. Les exceptions à ces règles créent des vulnérabilités, alors il vaux mieux éviter. +Chaque fois que le trafic empêche d’initier une connexion, cela indique qu’une pompe Store & Forward +peut être nécessaire.

  • +
  • topologie physique. Bien que la connectivité puisse être présente, l’utilisation optimale de +la bande passante peut impliquer de ne pas prendre le chemin par défaut de A à B, mais peut-être passer par C.

  • +
  • quand le transfert n’est pas 1:1, mais 1:<source ne sait pas comment> beaucoup. Le pompage prend +soin de l’envoyer à plusieurs points.

  • +
  • lorsque les données sources doivent être disponibles de manière fiable. Cela se traduit par la +réalisation de nombreuses copies, +plutôt qu’une seul, il est donc plus facile pour la source de publier une fois et d’avoir le réseau +s’occuper de la réplication.

  • +
  • Pour des raisons de gestion, vous souhaitez observer de manière centralisée les transferts volumineux de données.

  • +
  • pour des raisons de gestion, d’acheminer les transferts d’une certaine manière.

  • +
  • pour des raisons de gestion, pour s’assurer que les échecs de transfert sont détectés et escaladés +quand importants. Ils peuvent être corrigés plutôt que d’attendre une surveillance ad hoc pour détecter +le problème.

  • +
  • Pour les transferts asynchrones. Si la source a beaucoup d’autres activités, elle peut vouloir +confier la responsabilité à un autre service pour effectuer des transferts de fichiers potentiellement longs. +La pompe est insérée très près de la source et est full store & forward. sr_post +se termine (presque instantanément), et à partir de ce moment-là, le réseau de pompage gère les transferts.

  • +
+
+
+
+

1.3 Sélection des fonctionnalités AMQP

+

AMQP est un protocole universel de transmission de messages avec de nombreuses options différentes pour prendre en charge de nombreuses +différents modèles de messagerie. MetPX-sarracenia spécifie et utilise un petit sous-ensemble de +modèles AMQP. En effet, un élément important du développement de la sarracenia a été de choisir parmi les +de nombreuses possibilités un petit sous-ensemble de méthodes qui sont générales et faciles à comprendre, +pour maximiser le potentiel d’interopérabilité.

+

Spécifier l’utilisation d’un protocole seul peut être insuffisant pour fournir suffisamment d’informations pour +l’échange de données et l’interopérabilité. Par exemple, lors de l’échange de données via FTP, un certain nombre de choix +doivent être faits au-delà du protocole de base.

+
+
    +
  • utilisation authentifiée ou anonyme ?

  • +
  • comment signaler qu’un transfert de fichier est terminé (bits d’autorisation? suffixe? préfixe?)

  • +
  • convention de nommage.

  • +
  • transfert texte ou binaire.

  • +
+
+

Des conventions convenues au-delà du simple FTP (IETF RFC 959) sont nécessaires.

+

À l’instar de l’utilisation du FTP seul comme protocole de transfert est insuffisant pour spécifier +une procédure complète de transfert de données, l’utilisation de l’AMQP, sans plus d’informations, est incomplète.

+

AMQP 1.0 standardise le protocole sur le câble, mais laisse de côté de nombreuses fonctionnalités +de l’interaction du courtier. Comme le recours à des courtiers est la clé de l’utilisation par sarracenia, +était un élément fondamental des normes antérieures, et comme la norme 1.0 est relativement controversée, +ce protocole suppose un courtier standard antérieur à 1.0, comme le fournissent de nombreux courtiers +gratuits, tels que RabbitMQ, souvent appelé 0,8, mais 0,9 et post 0,9. +Les courtiers sont également susceptibles de bien interopérer.

+

Dans AMQP, de nombreux acteurs différents peuvent définir des paramètres de communication. Pour créer un +modèle de sécurité, Sarracenia contraint ce modèle : les clients sr3_post ne sont pas censés déclarer +des échanges. Tous les clients sont censés utiliser les échanges existants qui ont été déclarés par +les administrateurs de courtiers. Les autorisations client sont limitées à la création de files +d’attente pour leur propre usage, +en utilisant des schémas de nommage convenus. File d’attente pour le client : qc_<user>.????

+

Les échanges topic-based sont utilisés exclusivement. AMQP prend en charge de nombreux autres types d’échanges, +mais sr3_post envoye la rubrique afin de prendre en charge le filtrage côté serveur à l’aide du topic +basé sur le filtrage. Les rubriques reflètent le chemin d’accès des fichiers annoncés, ce qui permet un +filtrage direct côté serveur, complété par un filtrage côté client à la réception des messages.

+

La racine de l’arborescence des topics est la version de la charge utile du message. Cela permet aux courtiers uniques +de prendre facilement en charge plusieurs versions du protocole en même temps pendant les transitions. v02 +est la troisième itération du protocole et les serveurs existants prennent régulièrement en charge les versions précédentes +simultanément de cette manière. Le deuxième topic de l’arborescence des topics définit le type de message. +Au moment de la rédaction de cet article : v02.post est le préfixe de topic pour les messages de notification actuels.

+

Les messages AMQP contiennent des messages de notification, pas de données de fichier réelles. AMQP est +optimisé pour et assume des petits messages. Garder les messages petits permet un débit maximal des messages +et permet aux clients qui d’utiliser des mécanismes de priorité basés sur le transfert de données, +plutôt que sur les messages de notification. Accommoder des messages volumineux créerait de nombreuses +complications pratiques et nécessiterait inévitablement la définition d’une taille de fichier maximale +à inclure dans le message lui-même, ce qui entraîne de la complexité pour couvrir plusieurs cas.

+

sr_post est destiné à être utilisé avec des fichiers arbitrairement volumineux, via la segmentation et le multi-streaming. +Les blocs de fichiers volumineux sont annoncés indépendamment, et les blocs peuvent suivre différents chemins +entre la pompe initiale et la livraison finale.

+

Les vhosts AMQP ne sont pas utilisés. Je n’en ai jamais vu le besoin. Les commandes prennent en charge leur option, +mais il n’y avait aucun but visible à les utiliser.

+

Les aspects de l’utilisation de l’AMQP peuvent être des contraintes ou des caractéristiques :

+
+
    +
  • Les interactions avec un courtier sont toujours authentifiées.

  • +
  • Nous définissons le anonymous pour une utilisation dans de nombreuses configurations.

  • +
  • Les utilisateurs s’authentifient auprès du cluster local uniquement. Nous n’imposons aucune +sorte d’informations d’identification ou de propagation d’identité ou de fédération, ou de confiance distribuée.

  • +
  • Les pompes représentent les utilisateurs en transférant des fichiers en leur nom, il n’est pas nécessaire +d’inclure des informations sur les utilisateurs des sources ultérieurement dans le réseau.

  • +
  • Cela signifie que si l’utilisateur A de S0 est défini et qu’un utilisateur reçoit le même nom sur S1, +ils peuvent entrer en collision. triste. Accepté comme limitation.

  • +
+
+
+
+

1.4 Application

+

Description de la logique d’application pertinente pour la discussion. Il existe un “plan de contrôle” +où les messages de notification concernant les nouvelles données disponibles sont mises en place et +les messages de journal signalant l’état des transferts des mêmes données sont acheminés entre +les utilisateurs du plan de contrôle et les pompes. Une pompe est un courtier AMQP et les utilisateurs +s’authentifient auprès du courtier. Les données peuvent (la plupart du temps) avoir une autre +méthode d’authentification différente.

+

Il existe des cas d’utilisation de sécurité très différents pour le transfert de fichiers :

+
+
    +
  1. Diffusion publique des données sont produites, dont la confidentialité n’est pas un problème, +le but est de diffuser à tous ceux qui sont intéressés aussi rapidement et de manière aussi fiable +que possible, impliquant potentiellement de nombreuses copies. L’authentification des données est +généralement null dans ce cas. Les utilisateurs émettent simplement des requêtes HTTP GET sans +authentification. Pour l’authentification AMQP, cela peut être fait de manière anonyme, sans que +les fournisseurs ne puissent surveiller. S’il doit y avoir un support de la source de données, +la source affectera un utilisateur non anonyme pour le trafic AMQP, et le client s’assurerait +que la journalisation fonctionne, ce qui permettrait au fournisseur de surveiller et +alerter lorsque des problèmes surviennent.

  2. +
  3. Transfert privé Les données exclusives sont générées et doivent être déplacées vers un endroit +où elles peuvent être archivé et/ou traité efficacement, ou partagé avec des collaborateurs spécifiques. +Le trafic AMQP et HTTP doit être chiffré avec SSL/TLS. L’authentification est généralement courante +entre AMQP et HTTPS. Pour Apache httpd, la méthode htpasswd/htaccess devra être configurée en permanence +par le système de livraison. Ces transferts peuvent avoir des exigences de haute disponibilité.

  4. +
  5. Transfert par des tiers Le plan de contrôle est explicitement utilisé uniquement pour contrôler le +transfert, l’authentification aux deux extrémités se fait séparément. Les utilisateurs s’authentifient +auprès de la pompe sans données ou une pompe SEP avec AMQP, mais l’authentification aux deux extrémités +est hors du contrôle de Sarracenia. Le transfert par des tiers est limité à S=0. Si les données ne +traversent pas la pompe, elles ne peuvent pas être transmises. Aucun routage n’est donc pertinent dans ce cas. +Cela dépend également de la disponibilité des deux points finaux, donc plus difficile à assurer dans la pratique.

  6. +
+
+

Les transferts publics et privés sont destinés à soutenir des chaînes arbitraires de pompes entre +source et consommateur. Les cas dépendent du routage des messages de notification et des messages de journal.

+
+

Note

+

Routage vers l’avant… Transferts privés et publics… Pas encore clair, toujours en considération. +Ce qui est écrit ici à ce sujet est provisoire. On se demande si on divise, et on fait +public d’abord, puis privé plus tard?

+
+

Pour simplifier les discussions, les noms seront sélectionnés avec un préfixe things selon le type +de l’entité:

+
+
    +
  • Les échanges commencent par x.

  • +
  • Les files d’attente commencent par q.

  • +
  • Les utilisateurs commencent par u. Les utilisateurs sont également appelés sources

  • +
  • Les serveurs commencent avec svr

  • +
  • Les clusters commencent par c

  • +
  • ‘pompes’ est utilisé comme synonyme de cluster, et ils commencent par S (S majuscule) : S0, S1, S2…

  • +
+
+
+
Sur les pompes:
    +
  • Les utilisateurs que les pompes utilisent pour s’authentifier les uns les autres sont des +interpump accounts. Un autre mot: feeder , concierge ?

  • +
  • Les utilisateurs qui injectent des données dans le réseau sont appelés sources.

  • +
  • Les utilisateurs qui s’abonnent aux données sont appelés consumers.

  • +
+
+
+
+
+

1.5 Routage

+

Il existe deux flux distincts à acheminer : les messages de notification et les journaux. +L’en-tête suivant dans les messages concerne le routage, qui est défini dans tous les messages.

+
+
    +
  • source - l’utilisateur qui a injecté les messages de notification d’origine.

  • +
  • source_cluster - le cluster où la source a injecté les messages de notification.

  • +
  • to_clust - la liste séparée par des virgules des groupes de destination.

  • +
  • private - le drapeau pour indiquer si les données sont privées ou publiques.

  • +
+
+

Un objectif important du routage des messages de notification est que la source décide où vont +les messages de notification, donc le pompage des produits individuels doit être effectué uniquement +sur le contenu des messages de notification, et non sur une configuration d’administrateur.

+

Les administrateurs configurent les connexions interpompes (via SARRA et d’autres composants) +pour s’aligner sur les topologies de réseau, mais une fois configurées, toutes les données +doivent circuler correctement avec seules les commandes de routage initiées par la source. +Une configuration peut être nécessaire sur toutes les pompes chaque fois qu’une nouvelle +pompe est ajoutée au réseau.

+
+

1.5.1 Routage des publications

+

Le routage des publications est le routage des messages de notification annoncés par les données sources. +Les données correspondant à la source qui suivent la même séquence de pompes que les messages de notification +elles-mêmes. Lorsqu’un message de notification est traité sur une pompe, il est téléchargé, puis le +message de notification est modifié pour refléter la disponibilité de la pompe du saut suivant.

+

Les messages de publication sont définis dans la page de manuel sr_post(7). Ils sont initialement émis par sources, +publié à xs_source. Après la pré-validation, ils passent (avec les modifications décrites dans Sécurité) à +xPrivate ou xPublic.

+
+

Note

+

FIXME: Provisoire!? +Si ce n’est pas un échange séparé, alors n’importe qui peut voir n’importe quel message de notification +(pas les données, mais oui le message de notification). +Je pense que ce n’est pas bon.

+
+

Pour les données publiques, les feeders des pompes en aval se connectent à xPublic. +Ils regardent l’en-tête to_clust dans chaque message et consultent un fichier post2cluster.conf. +post2cluster.conf est juste une liste de noms de cluster configurés par l’administrateur

+
ddi.cmc.ec.gc.ca
+dd.weather.gc.ca
+ddi.science.gc.ca
+
+
+

Cette liste de clusters est censée être les clusters qui sont accessibles en traversant +cette pompe. Si un cluster dans post2cluster.conf est répertorié dans la to_clust du +champ de message, puis les données doivent (?). +Des feeders distincts en aval se connectent à xPrivate pour les données privées. Seuls les feeders sont +autorisé à se connecter à xprivate.

+
+

Note

+

FIXME: peut-être alimenter des échanges privés spécifiques pour chaque feeder? x2ddiedm, x2ddidor, x2ddisci ? +L’utilisation d’un xPrivate signifie que les pompes peuvent voir les messages qu’elles ne sont peut-être +pas autorisées à télécharger (problème moindre qu’avec xPublic, mais dépend de la fiabilité de la pompe en aval.)

+
+
+
+

1.5.2 Journaux de routage

+

Les messages de journal sont définis dans la page de manuel sr_log(7). Ils sont émis par les consommateurs à la fin, +ainsi que des feeders lorsque les messages traversent les pompes. Les messages de journal sont publiés sur +l’échange xl_<user> et après validation du journal mis en fil d’attente pour l’échange XLOG.

+

Les messages en xlog destinés à d’autres clusters sont acheminés vers des destinations par +le composant Log2Cluster en utilisant le fichier de configuration log2cluster.conf. log2cluster.conf +utilise des champs séparés par des espaces : le premier champ est le nom du cluster (défini selon soclust dans +messages de notification, le second est la destination pour envoyer les messages de journal pour publication +provenant de ce cluster) Exemple, log2cluster.conf:

+
clustername amqp://user@broker/vhost exchange=xlog
+
+
+

Lorsque la destination du message est le cluster local, log2user (log2source ?) copie +les messages où source=<user> à sx_<user>.

+

Lorsqu’un utilisateur souhaite afficher ses messages, il se connecte à sx_<user> et s’abonne. +Cela peut être fait en utilisant sr_subscribe -n –topic_prefix=v02.log ou l’équivalent sr_log.

+
+
+
+

1.6 Modèle de sécurité

+
+

1.6.1 Utilisateurs, files d’attente et échanges

+
+
Chaque utilisateur Alice, sur un courtier auquel elle a accès :
    +
  • a un xs_Alice d’échange, où elle écrit ses messages de notification et lit ses journaux.

  • +
  • a un xl_Alice d’échange, où elle écrit ses messages de journal.

  • +
  • peut créer des files d’attente qs_Alice_.* pour se lier aux échanges.

  • +
+
+
Les commutateurs se connectent les uns aux autres à l’aide de comptes inter-échanges.
    +
  • Alice peut créer et détruire ses propres files d’attente, mais personne d’autre.

  • +
  • Alice ne peut écrire qu’à son xs_exchange,

  • +
  • Les échanges sont gérés par l’administrateur, et non par n’importe quel utilisateur.

  • +
  • Alice ne peut publier que les données qu’elle publie (il s’agira d’elle)

  • +
+
+
+
+

Note

+

Testeur ^q_tester.* ^q_tester.*|xs_tester ^q_tester.*|^xl_tester$ +Laissant toutes les autorisations pour les files d’attente pour les utilisateurs AMQP donne +également l’autorisation de créer/configurer/écrire des objets AMQP dont le nom commence par q_tester +dans cet exemple.

+
+
+
+

1.6.2 Pré-validation

+

La pré-validation fait référence aux contrôles de sécurité et d’exactitude effectués sur +les informations fournies par le message de notification avant le téléchargement des données elles-mêmes. +Certains outils peuvent appeler cela validation des messages

+
+
    +
  • nettoyage des entrées (recherche d’erreurs/entrées malveillantes.)

  • +
  • un nombre indéfini de contrôles qui doivent être configurables (script ?)

  • +
  • varient selon la configuration et l’installation (tailles)

  • +
+
+
+
Lors de la lecture d’une source :
    +
  • un message de notification arrive sur xs_Alice, d’un utilisateur connecté en tant qu’Alice.

  • +
  • écrase la source pour être Alice : source=Alice … ou rejeter?

  • +
  • définit certains en-têtes pour lesquels nous ne faisons pas confiance aux utilisateurs : cluster=

  • +
  • Définissez l’en-tête du cluster sur local.

  • +
+
+
Lecture à partir d’un chargeur :
    +
  • La source n’a pas d’importance. (les feeders peuvent représenter d’autres utilisateurs)

  • +
  • Ne pas écraser la source.

  • +
  • s’assurer que le cluster n’est pas un cluster local (car ce serait un mensonge.) ?

  • +
+
+
De toute façon:
    +
  • vérifier la taille de partitionnement, si elle dépasse le maximum de la pompe, rejeter.

  • +
  • Vérifiez les limitations de bande passante en place. En cas de dépassement, attendez.

  • +
  • Vérifiez la limite d’utilisation du disque en place. En cas de dépassement, attendez.

  • +
  • Si l’indicateur privé est défini, alors acceptez en copiant sur xPrivate

  • +
  • Si l’indicateur privé n’est pas défini, alors acceptez par copie à xPublic

  • +
+
+
Résultats:
    +
  • Accepter signifie : mettre en fil d’attente le message vers un autre échange (xinput) pour téléchargement.

  • +
  • Rejeter signifie : ne pas copier le message (toujours accepter & ack pour qu’il quitte la fil d’attente) +message du journal du produit.

  • +
  • Hold signifie : ne pas consommer… mais dormez un moment.

  • +
+
+
+

Le hold est pour des raisons de type défaillance temporaire, telles que la bande passante de l’espace disque. +Étant donné que ces raisons sont indépendantes du message particulier, le hold s’applique à +l’ensemble de la fil d’attente, pas seulement le message.

+

Après le prétraitement, un composant tel que sr_sarra suppose que le message de notification est bon, +et le traite simplement. Cela signifie qu’il récupérera les données de la source de publication. +Une fois les données téléchargées, elles passent par la post-validation.

+
+
+

1.6.3 Post-Validation

+

Lorsqu’un fichier est téléchargé, avant de l’annoncer à nouveau pour des sauts ultérieurs, +il passe par une analyse. Les outils peuvent appeler ceci validation de fichier:

+
+
    +
  • lorsqu’un fichier est téléchargé, il passe par post-validation,

  • +
  • Invoquer un ou plusieurs antivirus choisis par la sécurité

  • +
  • les scanners ne seront pas les mêmes partout, même à différents endroits à l’intérieur de +La même organisation peut avoir des normes d’analyse différentes (fonction sur la zone de sécurité).

  • +
  • Accepter signifie : il est acceptable d’envoyer ces données à d’autres sauts du réseau.

  • +
  • Rejeter signifie : ne pas transmettre ces données (éventuellement supprimer la copie locale.) +Essentiellement une quarantaine

  • +
+
+
+
+

1.6.4 Validation du journal

+

Lorsqu’un client comme sarra ou subscribe termine une opération, il crée un message de journal +correspondant au résultat de l’opération. (C’est une granularité beaucoup plus faible qu’un +fichier journal local.) Il est important qu’un client ne puisse pas usurper l’identité d’un autre +lors de la création de messages de journal.

+
+
    +
  • Les messages dans les échanges n’ont aucun moyen fiable de déterminer qui les a insérés.

  • +
  • Ainsi, les utilisateurs publient leurs messages de journal sur l’échange sl_<user>.

  • +
  • Pour chaque utilisateur, le lecteur de journal lit le message et écrase l’utilisateur consommateur +pour forcer la correspondance. (si vous lisez un message de sl_Alice, cela force le champ utilisateur consommateur +à être Alice) voir sr_log(7) pour le champ d’utilisateur

  • +
  • sl_* sont en écriture seule pour tous les utilisateurs, ils ne peuvent pas lire leurs propres messages +de notification pour cela.

  • +
  • Y a-t-il une vérification de la consommation d’host?

  • +
  • Accepter un message de journal signifie publier sur l’échange xlog.

  • +
  • Seules les fonctions d’administration peuvent lire à partir de xlog.

  • +
  • Le traitement en aval provient de l’échange XLOG qui est supposé être propre.

  • +
  • Rejeter un message de journal signifie de le copier nulle part.

  • +
  • Le contrôle des ressources n’a pas de sens lorsque des canaux sont utilisés pour le routage inter-pompes. +Essentiellement, tout ce que les pompes en aval peuvent faire est de transmettre au cluster source. +Les pompes recevant les messages de journal ne doivent pas convertir l’utilisateur consommateur sur ces liens. +Preuve de la nécessité d’une sorte de réglage : réglage entre l’utilisateur et entre les pompes.

  • +
+
+
+

Note

+

FIXME : si vous rejetez un message de journal, génère-t-il un message de journal ? +Potentiel de déni de service en générant simplement des messages de journal de tourbières infinis. +Il est triste que si une connexion est mal configurée en tant qu’utilisateur, lorsqu’elle est inter-pompe, +Cela entraînera l’abandon des messages. Comment détecter une erreur de configuration?

+
+
+
+

1.6.5 Transfert de données privé vs public

+

Dans le passé, les transferts ont été publics, il s’agissait simplement de partager des informations publiques. +Une exigence cruciale du paquet est de prendre en charge les copies de données privées, lorsque les +fins du transfert ne sont pas partagées avec d’autres personnes arbitraires.

+
+

Note

+

FIXME: Cette section est une idée à moitié cuite! Je ne sais pas comment les choses vont tourner. +problème de base: Alice se connectant à S1 veut partager avec Bob, qui a un +compte sur S3. Pour aller de S1 à S3, il faut traverser S2. La façon normale +de ce routage est effectué est via un abonnement sr_sarra à xpublic sur S1, et +S2. Ainsi, Eve, un utilisateur sur S1 ou S2, peut voir les données, et probablement les télécharger. +sauf si les autorisations http sont définies sur refuser sur S1 et S2. Eve n’aurait pas accès. +Implémenter via http/auth permettant des comptes inter-pompes sur S2 +pour accéder au compte S1/<private> et S3 à S2/<private>. puis autorisez Bob sur +S3.

+
+

Il existe deux modes d’envoi de produits via un réseau, privé et public. +Avec l’envoi public, l’information transmise est supposée être publique et disponible +à tous les arrivants. Si quelqu’un voit les données sur une pompe intermédiaire, alors ils sont susceptibles +de pouvoir les télécharger à volonté sans autres arrangements. Les données publiques sont publiées +pour les copies inter-pompes utilisant l’échange xPublic, auquel tous les utilisateurs peuvent également accéder.

+

Les données privées ne sont mises à la disposition que des personnes explicitement autorisées à y accéder. +Les données privées sont mises à disposition uniquement sur l’échange xPrivate. Seul les utilisateurs +de canal Interpump ont accès à ces messages.

+
+

Note

+
    +
  • Deux échanges sont-ils nécessaires ou la définition d’autorisations est-elle suffisante ?

  • +
  • si personne sur B n’est autorisé, alors seul C est capable de télécharger à partir de B, +ce qui fonctionne tout simplement.

  • +
  • Cela ne fonctionne qu’avec http car la définition des autorisations sftp va être un enfer.

  • +
  • Si vous utilisez seulement http, alors Even peut toujours voir toutes les publications, +mais pas obtenir de données, sauf si xprivate se produit.

  • +
+
+

Pour les topologies SEP (voir Topologies), les choses sont beaucoup plus simples car les utilisateurs +finaux peuvent simplement utiliser des bits de mode.

+
+
+

1.6.6 Accès privé HTTPS

+
+

Note

+

FIXME: Pas encore conçu. +Vraiment pas encore cuit. Pour https, besoin de créer/gérer .htaccess (prédéfini mais généré tous les jours) +et les fichiers .htpasswd (générés tous les jours).

+
+

Besoin d’une sorte de message adm que les sources peuvent envoyer N pompes plus tard pour modifier le contenu de .htpasswd +CRUD? Ou simplement écraser à chaque fois? requête?

+

Sarra a probablement besoin de regarder cela et d’ajouter les fichiers ht* tous les jours. +Besoin de parler avec les gars de l’équipe de messagerie Web.

+

Comment changer les mots de passe

+
+
+
+

1.7 Topologies

+

Questions… Il existe de nombreux choix pour la disposition des clusters. On peut faire H/A +simple sur une paire de nœuds, simple actif/passif ? On peut aller à des conceptions évolutives +sur un tableau de nœuds, ce qui nécessite une charge d’équilibreur avant les nœuds de traitement. +Les disques d’un cluster peuvent être partagés ou individuels sur les nœuds de traitement, tout +comme l’état du courtage. Explorer s’il faut prendre en charge toutes les configurations, +ou pour déterminer s’il existe un modèle de conception particulier qui peut être appliqué de +manière générale.

+

Pour prendre ces décisions, une exploration considérable est nécessaire.

+

Nous commençons par nommer les topologies afin qu’elles puissent être facilement référencées dans +les discussions ultérieures. Aucune des topologies ne suppose que les disques sont pompés entre +les serveurs dans le style HA traditionnel.

+

D’après l’expérience, le pompage de disque est considéré comme peu fiable dans la pratique, car il implique +des problèmes complexes. Interaction avec de nombreuses couches, y compris l’application. +Les disques sont soit dédiés aux nœuds, soit un système de fichiers en cluster doit être utilisé. +On s’attend à ce que la demande porte sur ces deux cas.

+

Quelques documents abrégés :

+
+
Bunny

Une instance de broker partagée/en cluster, où plusieurs nœuds utilisent un broker commun pour se coordonner.

+
+
Effet Capybara

capybara à travers un serpent où un gros rongeur déforme le corps d’un serpent +au fur et à mesure qu’il est en cours de digestion. Symbolique d’un mauvais équilibrage de charge, où un nœud +subit un pic de charge et ralentit excessivement.

+
+
Vannage (winnowing) d’empreintes digitales

Chaque produit a une somme de contrôle et une taille destinées à l’identifier de manière unique, appelées +comme empreinte digitale. Si deux produits ont la même empreinte digitale, ils sont considérés comme +équivalent, et un seul peut être transmis. Dans les cas où plusieurs sources de données équivalentes +sont disponibles, mais les consommateurs en aval préféreraient recevoir des messages de notification uniques +des produits, les processus peuvent choisir de publier des notifications du premier produit +avec une empreinte digitale donnée, et ignorer les suivantes.

+

C’est la base de la stratégie la plus robuste pour la haute disponibilité, mais la mise en place de +plusieurs sources pour les mêmes données, acceptant les messages de notification pour tous, mais uniquement +en acheminant un en aval. En fonctionnement normal, une source peut être plus rapide que les +autres, et donc les produits de la deuxième source sont généralement “winnowed”. Lorsqu’une source +disparaît, les données de l’autre source sont automatiquement sélectionnées, au fur et à mesure que +les empreintes digitales sont maintenant fraiches et utilisés, jusqu’à ce qu’une source plus rapide +devienne disponible.

+

L’avantage de cette méthode est que maintenant une décision A / B est nécessaire, de sorte que +le temps de pompage est nul. D’autres stratégies font l’objet de retards considérables +en prenant la décision de repomper, et les pathologies que l’on pourrait résumer comme battant, +et/ou des impasses.

+
+
+
+

1.7.1 Autonome

+

Dans une configuration autonome, il n’y a qu’un seul nœud dans la configuration. J’exécute tous les composants +et n’en partage aucun avec d’autres nœuds. Cela signifie que le courtier et les services de données tels que sftp et +Apache sont sur un nœud.

+

Une utilisation appropriée serait une petite installation d’acquisition de données non-24x7, +pour prendre la responsabilité de la fil d’attente des données et s’éloigner de l’instrument.

+
+
+

1.7.2 DDSR : Configuration de commutation/routage

+

Il s’agit d’une configuration plus évolutive impliquant plusieurs nœuds de Data Mover et +potentiellement plusieurs brokers. Ces clusters ne sont pas des destinations de transferts +de données, mais des intermédiaires. Les données circulent à travers eux, mais les interroger +est plus compliqué car aucun nœud ne dispose de toutes les données disponibles. Les clients en aval +des DDSR sont essentiellement d’autres cas de sarracenia.

+

Plusieurs options sont encore disponibles dans ce modèle de configuration. +DDSR Un courtier par nœud ? (ou juste un courtier (clusterisé, logique) courtier?)

+

Sur un pompage/routeur, une fois que la livraison a eu lieu dans tous les contextes, pouvez-vous supprimer le fichier ? +Il suffit de regarder les fichiers journaux et de cocher chaque étendue qui confirme la réception. +Lorsque le dernier confirmé, supprimez. (rend re-xmit difficile ;-)

+

Basé sur un seuil de taille de fichier ? Si le fichier est trop volumineux, ne le conservez pas ?

+

L’objectif visé comporte un certain nombre d’options de mise en œuvre, qui doivent être subdivisées aux fins d’analyse.

+
+
+

1.7.3 DDSR indépendant

+

Dans Independent DDSR, il existe un équilibreur de charge qui distribue chaque connexion entrante à +un courtier individuel exécuté sur un seul nœud.

+

DDSR - courtier

+

La validation préalable à la récupération se produirait sur le courtier. Puis re-post pour les Sarra sur les movers.

+
+
    +
  • Chaque courtier de nœud et moteur de transfert agit indépendamment. Robustesse maximale à l’échec.

  • +
  • L’équilibreur de charge supprime les nœuds de déplacement du fonctionnement lors de la détection d’une défaillance.

  • +
  • Les fichiers individuels atterrissent, la plupart du temps entièrement sur des nœuds uniques.

  • +
  • Aucun Data Mover ne voit tous les fichiers de tous les utilisateurs d’un cluster.

  • +
+
+

CONFIRMEZ : les processus exécutés sur les nœuds individuels sont abonnés au broker local. +Très sensible à l’effet Capybara où tous les blocs de +des fichiers volumineux sont canalisés via un seul nœud de traitement. Les transferts de +fichiers volumineux le déclencheront.

+

CONFIRMEZ : Les performances maximales pour un seul transfert sont limitées à un seul nœud.

+
+
+

1.7.4 Courtier partagé DDSR

+

Bien que l’espace disque des nœuds de données reste indépendant, les courtiers sont regroupés pour +former une entité logique unique.

+

Sur tous les nœuds, les processus de déplacement utilisent des échanges et des files d’attente communs.

+
+
    +
  • Chaque nœud est transféré indépendamment, mais dépend du cluster de courtiers.

  • +
  • L’équilibreur de charge supprime les nœuds (courtier ou déménageur) du fonctionnement.

  • +
  • Les utilisateurs externes se connectent à des files d’attente partagées, pas à des files +d’attente spécifiques à un nœud.

  • +
  • Les moteurs de transfert se connectent aux files d’attente de cluster, obtenant des blocs.

  • +
  • Aucun Data Mover ne voit tous les fichiers de tous les utilisateurs d’un cluster.

  • +
  • Nécessite que le courtier soit regroupé, ce qui ajoute de la complexité ici.

  • +
+
+

Dans courtier partagé DDSR, l’effet Capybara est minimisé en tant que blocs individuels d’un transfert et +sont répartis sur tous les nœuds de déménagement. Lorsqu’un fichier volumineux arrive, tous les déménageurs +sur tous les nœuds peuvent ramasser des blocs individuels, de sorte que le travail est automatiquement +répartis entre eux.

+

Cela suppose que les fichiers volumineux sont segmentés. Comme différents nœuds de transfert auront +différents blocs d’un fichier, et la vue de données n’est pas partagée, pas de réassemblage des fichiers +est fait.

+

Le regroupement de courtiers est considéré comme une technologie mature et donc relativement fiable.

+
+
+

1.7.5 DD : Configuration de la diffusion des données (AKA : Data Mart)

+

La configuration de déploiement sr3 est davantage une configuration de point de terminaison. Chaque nœud est censé : +Avoir une copie complète de toutes les données téléchargées par tous les nœuds. Donner une vue unifiée rend +ca beaucoup plus compatible avec une variété de méthodes d’accès, telles qu’un navigateur de fichiers (sur HTTP, +ou sftp) plutôt que de se limiter aux messages de notification AMQP. C’est le type de vue présenté par +dd.weather.gc.ca.

+

Avec ce point de vue, tous les fichiers doivent être entièrement réassemblés à la réception, +avant d’annoncer la disponibilité en aval. Les fichiers peuvent avoir été fragmentés pour +être transférés entre les pompes intermédiaires.

+

Il existe plusieurs options pour obtenir cet effet visible de l’utilisateur final, chacune avec des compromis. +Dans tous les cas, il y a un équilibreur de charge devant les nœuds qui distribue les demandes entrantes +de connexion à un nœud pour le traitement.

+
+
    +
  • plusieurs nœuds de serveur. Chacun autonome.

  • +
+
+
    +
  • +
    SR - Load Balancer, juste redirige vers un nœud SR?

    dd1,dd2,

    +
    +
    +
  • +
+

Le courtier sur le nœud SR a une connexion par la suite.

+
+
+

1.7.6 DD indépendant

+
+
    +
  • L’équilibreur de charge transmet les demandes entrantes à plusieurs configurations independantes.

  • +
  • Chaque nœud télécharge toutes les données. L’espace disque requis pour les nœuds dans cette configuration +sont beaucoup plus grands que pour les nœuds DDSR, où chaque nœud ne contient que 1/n des données.

  • +
  • Chaque nœud annonce chaque produit qu’il a téléchargé, en utilisant son propre nom de nœud, car +Il ne sait pas si d’autres nœuds ont ce produit.

  • +
  • Une fois la connexion établie, le client communiquera exclusivement avec ce nœud. +Les performances ultimes sont limitées par les performances du nœud individuel.

  • +
  • Les Data Movers peuvent (pour une fiabilité maximale) être configurés indépendamment, mais si les entrées +sont sur le WAN, on peut réduire l’utilisation de la bande passante N fois en ayant N nœuds +Partagez les files d’attente pour les sources distantes, puis effectuez des transferts locaux entre les nœuds.

  • +
+
+

CONFIRMER: Le vannage d’empreintes digitales est-il nécessaire pour les copies intra-cluster?

+
+

Lorsqu’un seul nœud échoue, il cesse de télécharger et les autres n-1 nœuds continuent le transfert.

+
+
+

Note

+

FIXME : courtier partagé et système de fichiers partagé… Hmm… Pourrait utiliser un deuxième courtier +Instance pour faire un téléchargement coopératif via le vannage Fingerprint.

+
+
+
+

1.7.7 Shared-Broker DD

+
+
    +
  • Un seul broker en cluster est partagé par tous les nœuds.

  • +
  • Chaque nœud télécharge toutes les données. Espace disque requis pour les nœuds dans cette configuration +sont beaucoup plus grands que pour les nœuds DDSR, où chaque nœud ne contient que 1/n des données.

  • +
  • Les clients se connectent à une instance d’agent à l’échelle du cluster, de sorte que les liens de +téléchargement peuvent provenir de n’importe quel dans le cluster.

  • +
  • Si le broker en cluster échoue, le service est en panne. (devrait être fiable)

  • +
  • Un nœud ne peut pas annoncer chaque produit qu’il a téléchargé, en utilisant son propre nom de nœud, car +Il ne sait pas si d’autres nœuds ont ce produit. (Annonce en tant que DD1 vs. DD)

  • +
+

-Ou:

+
+

– Ne peut annoncer un produit qu’une fois qu’il est clair que chaque nœud actif possède le produit. +– 1er arrivé, 1er servi : appliquer le vannage des empreintes digitales. Annoncez uniquement le nœud qui a obtenu les données en premier.

+
+
    +
  • Comme dans la configuration indépendante, les nœuds partagent des files d’attente et téléchargent +une fraction des données en amont. +Ils ont donc besoin d’échanger des données entre eux, mais cela signifie utiliser un courtier. Il +est donc probable qu’il y aura deux courtiers accessibles par les nœuds, un nœud local et un partagé.

  • +
  • C’est plus compliqué, mais cela évite d’avoir besoin d’un système de fichiers en cluster. Hmm… Choisissez votre poison. +démo des deux?

  • +
+
+
+
+

1.7.8 DD de données partagées

+
+
    +
  • L’équilibreur de charge transmet la requête entrante à plusieurs nœuds.

  • +
  • Chaque nœud dispose d’un accès en lecture/écriture à un système de fichiers partagé/cluster.

  • +
  • Configuration du broker en cluster, tous les nœuds voient le même broker.

  • +
  • téléchargé une fois signifie disponible partout (écrit sur un disque partagé)

  • +
  • peut donc faire de la publicité immédiatement avec la spécification d’hôte partagé (DD vs DD1)

  • +
  • Si le broker en cluster échoue, le service est en panne. (devrait être fiable)

  • +
  • Si le système de fichiers en cluster échoue, le service est en panne. (??)

  • +
+
+
+
+

1.7.9 SEP: Shared End-Point Configuration

+

La configuration SEP, tous les nœuds de déplacement sont directement accessibles aux utilisateurs. +Le courtier ne fournit pas de service de données, juste un pur courtier de messages. Peut être appelé +sans données, ou un bunny.

+

Le broker est exécuté en cluster et rien ne peut être dit sur les nœuds de déplacement. +Les consommateurs et les observateurs peuvent être démarrés par n’importe qui sur n’importe quelle collection de nœuds, +et toutes les données visibles à partir de n’importe quel nœud où les systèmes de fichiers de cluster offrent cet avantage.

+

L’administration de l’espace disque est entièrement un paramètre de configuration d’utilisateur, pas dans le +contrôle de l’application (les utilisateurs définissent directement des quotas ordinaires pour leurs systèmes de fichiers)

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/Documentation.html b/fr/Contribution/Documentation.html new file mode 100644 index 000000000..ae90bb967 --- /dev/null +++ b/fr/Contribution/Documentation.html @@ -0,0 +1,370 @@ + + + + + + + Normes de documentation — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Normes de documentation

+
+

Structure des dossiers

+

Avant de commencer à travailler avec la documentation, lire l’intégralité de l’article de documentation Divo (et les liens sur la barre latérale gauche). +Cela ne prendra pas plus de 30 minutes et vous obtiendrez une compréhension complète de +la structure, du style et du contenu attendus de la documentation ici.

+https://documentation.divio.com/_images/overview.png + +

Nous regardons la directive unix dans notre documentation, chaque fichier de documentation fait une chose +et le fait bien en ce qui concerne les 4 quadrants.

+
+

Note

+

TODO:Ajoutez des exemples de liens vers chacune des sections ci-dessous:

+
+
+

Traitement

+

Toute la documentation se trouve sous l’arborescence docs/source. Il est traité à l’aide de sphinx, +invoqué à l’aide du Makefil dans docs/. On peut installer sphinx localement, et exécuter make pour +construire localement et déboguer. Le résultat est produit dans docs/build/html:

+
pip install -f requirements-dev.txt
+cd docs
+make html
+
+
+

Pointez ensuite un navigateur sur docs/Build/HTML.

+

Il existe un travail Github Actions qui effectue cette opération à chaque push vers les branches +appropriées pour mettre à jour la documentation principale. L’URL principale du site Web résultant est:

+
+
+

Le processus de publication est automatisé par deux actions github CI/CD :

+
+
+
+

C´est une bonne pratique de jeter un coup d´œil aux messages d’erreur sphinx générés par le premier action. Il y a typiquement +des centaines de petits problèmes à corriger (liens qui ne sont pas tout à fait corrects, formatage de tableau cassé, etc…)

+
+
+

Tutoriels

+
    +
  • Orienté vers l’apprentissage, en particulier apprendre comment plutôt que d’apprendre cela.

  • +
  • Permettez à l’utilisateur d’apprendre en faisant pour le faire démarrer, assurez-vous que votre +tutoriel fonctionne et que les utilisateurs peuvent voir les résultats immédiatement.

  • +
  • Les tutoriels doivent être reproductibles de manière fiable et axés sur des étapes concrètes +(et non sur des concepts abstraits) avec le minimum d’explications nécessaires.

  • +
+

De nombreux didacticiels sont créés à l’aide de jupyter notebooks. Voir docs/source/fr/Tutoriels/README.md pour +apprendre comment travailler avec eux.

+
+
+

How2Guides (Comment Faire)

+
    +
  • Axé sur le problème et l’objectif : “Je veux… Comment puis-je…” Différent des tutoriels parce que +les tutoriels sont pour les débutants, et how2guides (comment faire) suppose une certaine connaissance +et compréhension avec une configuration et des outils de base.

  • +
  • Fournir une série d’étapes axées sur les résultats d’un problème particulier.

  • +
  • N’expliquez pas les concepts, s’ils sont importants, ils peuvent être liés à .. /Explication/

  • +
  • Il existe plusieurs façons d’écorcher un chat, restez flexible dans votre guide afin que les utilisateurs +puissent voir comment les choses sont faites.

  • +
  • NOMMEZ BIEN LES GUIDES assez pour dire à l’utilisateur exactement ce qu’il fait en un coup d’œil.

  • +
+
+
+

Explication

+
    +
  • Axé sur la compréhension: peut également être considéré comme des discussions. Version beaucoup +plus détendue de la documentation où les concepts sont explorés à partir d’un niveau supérieur +ou de perspectives différentes.

  • +
  • Fournir un contexte, discuter des alternatives et des opinions tout en fournissant une référence +technique (pour les autres sections).

  • +
+
+
+

Référence

+
    +
  • Style dictionnaire.

  • +
  • Orienté vers l’information: descriptions des fonctionnalités déterminées par le code.

  • +
  • Strictement pour les pages de manuel et la référence directe des différents programmes, protocoles et fonctions.

  • +
+
+
+

Contribution

+
    +
  • Informations essentielles à l’amélioration et à la progression du projet Sarracenia, c’est-à-dire: pour +ceux qui cherchent à développer Sarracenia.

  • +
  • Guide(s) de style

  • +
  • Modèle(s)

  • +
+
+
+
+

Processus

+

Le processus de développement consiste à écrire ce que l’on a l’intention de faire ou ce que l’on a fait dans +un fichier reStructuredText +dans /docs/Explanation/Design/. Idéalement, la discussion de l’information y agit +comme point de départ qui peut être édité dans la documentation pour la ou les fonctionnalités au fur et à +mesure qu’elles sont mises en œuvre. Chaque nouveau composant sr_<whatever>, doit avoir des pages de manuel +pertinentes implémentées. Les how2guides et tutoriels devraient également être révisés pour tenir compte des +ajouts ou des modifications apportés au nouveau composant.

+
+

Error

+

Besoin de Peter pour identifier les informations importantes dans doc/design pour tirer sur le +/docs/Explanation/Design/

+
+
+
+

Guide de Style

+

L’exécution sur la ligne de commande doit être écrite dans le style suivant. +Un commentaire initial décrivant les étapes ou processus suivants:

+
$ command 1
+  relevant output
+$command 2
+  .
+  .
+  relevant output
+  newline relevant output
+
+
+

Remarques importantes:

+
    +
  • Le commentaire initial se termine par :: suivi d’une nouvelle ligne vide

  • +
  • Ensuite, se trouve le bloc de code indenté (deux espaces)

  • +
  • Syntaxe des commandes: ‘$ <cmd>

  • +
  • Vous pouvez également indiquer les commandes de niveau root avec ‘# <cmd>

  • +
  • La sortie de la commande est (deux espaces) en retrait de la commande principale.

    +
      +
    • Les lignes de sortie non pertinentes peuvent être remplacées par des points ou carrément omises.

    • +
    +
  • +
+

Choisissez et respectez une hiérarchie d’en-tête par défaut (ie : = > ~ > - > … pour un titre > h1 > h2 > h3… etc)

+
+

Style de Code

+

Nous suivons généralement les standards PEP 8 pour la mise en forme du code, +et on utilise YAPF pour reformater automatiquement le code. +Une exception au PEP 8 est que nous utilisons une longueur de ligne de 119 caractères.

+

Pour les docstrings dans le code, nous suivons le Guide de style Google. +Ces docstrings seront analysés dans une documentation formatée par Sphinx.

+

Des exemples détaillés peuvent être trouvés dans le +Documents du plugin Napoleon Sphinx +et les Guide Google de Style Python.

+

Exemples choisis de credentials.py:

+
class Credential:
+    """Objet qui contient des informations sur les informations d’identification, lues à partir d’un fichier
+     credentials, qui a une information d’identification par ligne, en format::
+        url option1=value1, option2=value2
+
+    Exemples::
+        sftp://alice@herhost/ ssh_keyfile=/home/myself/mykeys/.ssh.id_dsa
+        ftp://georges:Gpass@hishost/  passive = True, binary = True
+
+    `Format de la Documentation. <https://metpx.github.io/sarracenia/Reference/sr3_credentials.7.html>`_
+
+    Attributs:
+        url (urllib.parse.ParseResult): object with URL, password, etc.
+        ssh_keyfile (str): path to SSH key file for SFTP
+        passive (bool): use passive FTP mode, defaults to ``True``
+        binary (bool): use binary FTP mode, defaults to ``True``
+        tls (bool): use FTPS with TLS, defaults to ``False``
+        prot_p (bool): use a secure data connection for TLS
+        bearer_token (str): bearer token for HTTP authentication
+        login_method (str): force a specific login method for AMQP (PLAIN,
+            AMQPLAIN, EXTERNAL or GSSAPI)
+    """
+
+    def __init__(self, urlstr=None):
+        """Créer un objet Credential.
+
+            Args:
+                urlstr (str): a URL in string form to be parsed.
+        """
+
+
+
def isValid(self, url, details=None):
+    """Valide un objet URL et Credential. Vérifie les mots de passe vides, les schémas, etc.
+
+    Args:
+        url (urllib.parse.ParseResult): ParseResult objet pour un URL.
+        details (sarracenia.credentials.Credential): objet Crednetial sarra qui contient des details additionels
+        à propos de l'URL.
+    Returns:
+        bool: ``True`` si un URL est valide, ``False`` sinon.
+    """
+
+
+
+
+

Why rST?

+

reStructuredText a été choisi principalement parce qu’il prend en charge la création automatique d’une +table des matières avec la directive ‘.. Table des matières::’. +Comme beaucoup d’autres langages de Markup, il prend également en charge le style en ligne, +les tableaux, les en-têtes et les blocs littéraux.

+

Dans Jupyter Notebooks, malheureusement, seul Markdown est pris en charge, sinon RST est génial.

+
+
+
+

Localisation

+

Ce projet est destiné à être traduit en Français et en anglais à un minimum tel qu’il est +utilisé dans l’ensemble du gouvernement du Canada, qui possède ces deux langues officielles.

+

La documentation Française a la même structure de fichiers et les mêmes noms que la documentation anglaise, mais +est placé dans le sous-répertoire fr/. C’est plus facile si la documentation est produite +dans les deux langues à la fois. Utilisez au moins un outil de traduction automatique (tel que +deepl) pour fournir un point de départ. Même procédure pour les francophones.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Contribution/D\303\251veloppement.html" "b/fr/Contribution/D\303\251veloppement.html" new file mode 100644 index 000000000..2de929b38 --- /dev/null +++ "b/fr/Contribution/D\303\251veloppement.html" @@ -0,0 +1,1482 @@ + + + + + + + Guide du développeur MetPX-Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Guide du développeur MetPX-Sarracenia

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+

Outillage

+

Pour contribuer à la source Sarracenia, vous avez besoin de:

+
    +
  • Un environnement de développement Linux, soit un poste de travail, soit une machine virtuelle quelconque. +La configuration à l’aide d’Ubuntu est automatisée, mais un ajustement pour d’autres distributions est possible. +Le confort en ligne de commande est un must.

  • +
  • python3. L’application est développée en Python et dépend des versions python >= 3.6.

  • +
  • style: PEP8 sauf que la longueur maximale de la ligne est de 119, appliquée via +pycodestyle +pour VSCode, yapf3 ou un autre outil similaire.

  • +
  • Un compte sur github.com aidera à soumettre des patches.

  • +
+

Les éléments qui seront installés par configuration automatisée :

+
    +
  • un tas d’autres modules python indiqués dans les dépendances (setup.py ou debian/control)

  • +
  • Module Pyftpdlib python3, utilisé pour exécuter un serveur ftp sur un port élevé pendant le test de flux.

  • +
  • git. afin de télécharger la source à partir du dépôt GitHub, et de préparer et soumettre +changements.

  • +
  • Un courtier RabbitMQ dédié, avec accès administratif, pour exécuter les tests sr_insects. +Ceci est installé par des outils automatisés pour configurer l’environnement Linux. +Le test de flux crée et détruit les échanges et perturbera tous les flux actifs sur le courtier.

  • +
+

après avoir cloné le code source:

+
git clone -b development https://github.com/MetPX/sarracenia sr3
+git clone -b development https://github.com/MetPX/sarrac metpx-sr3c
+git clone https://github.com/MetPX/sr_insects insects
+cd sr3
+
+
+

Le reste du guide suppose que vous y êtes.

+
+
+

Documentation

+

Normes de documentation existent dans /docs/Contribution/Documentation.rst. +Le processus pour la construction locale des documents sont là, ainsi que les méthodes +de maintenance du site Web en direct.

+
+

Où documenter les options

+

Les options sont documentées dans le style de dictionnaire sr3_options(7) par ordre alphabétique. +Si cela en valait la peine, des exemples d’utilisation pourraient être ajoutés à d’autres guides.

+
+
+
+

Développement

+

En général, le flux de travail de développement consiste à obtenir un ordinateur portable ou une machine +virtuelle où l’on peut exécuter les flow_tests (disponible auprès de http://github.com/MetPX/sr_insects ). +La première étape lors de la configuration d’un environnement de développement s’agit de s’assurer que les +tests de flux sr_insects fonctionnent, car ils fonctionnent comme une porte d’entrée pour les commits +dans les branches importantes.

+

Le développement est le plus souvent effectué sur la plate-forme Ubuntu > = 18.04.

+
+

Flux de travail v2

+

Le travail de développement terminé pour la version 2 est engagé sur la branche v2_dev, qui est utilisée +pour produire des instantanés quotidiens. On ne devrait normalement pas engager de changements dans la +direction v2_dev, mais plutôt les fusionner à partir d’une branche de travail.

+

Les directions générales du développement sont nommées d’après la question qu’elles sont censées aborder +“v2_issue365”, par exemple. S’il y a plusieurs tentatives pour résoudre un problème donné, utilisez le problème +comme préfixe de nom. Par exemple, il pourrait y avoir issue365, mais si nous décidons que ce n’est pas +une bonne façon d’aborder le problème, il pourrait y avoir une direction issue365_methodB.

+

Avant de soumettre une demande d’extraction (Pull Request, PR), veuillez vous assurer que les tests de flow de +sr_insects ont été exécutés avec succès : au moins static_flow, flakey_broker et dynamic_flow

+

Lorsqu’un PR est généré, le deuxième développeur peut l’examiner pour détecter des préoccupations. +Une fois satisfait de la nature du correctif, le deuxième développeur doit retirer la branche +et exécutez à nouveau les tests de flux (les trois mêmes) pour confirmer. Seulement après que les tests de flux +ont été exécutés sur plusieurs machines qu’on peut ensuite fusionner avec stable.

+

Les problèmes propres à la v2 doivent être étiquetés v2only. +sur Launchpad.net :

+
    +
  • Les paquets de référentiel quotidiens de v2 seront construits à partir de v2_dev

  • +
  • Les paquets de référentiel de pré-version de v2 seront construits à partir de v2_dev

  • +
  • Les packages de référentiel de versions sont générés à partir de v2_stable.

  • +
+
+
+

Flux de travail v3

+

La prochaine version de Sarracenia est développée dans la branche development (travail en cours). +Comme la refactorisation majeure est pratiquement terminée, le travail restant est maintenant entièrement constructif +et tout le développement est coordonné par des issues exactement comme l’est la V2. Les problèmes propres à la v3 +sont les régressions ou améliorations qui n’ont pas de sens à ajouter à v2, ont la balise v3only. +Les problèmes communs entre les versions sont marqués v3.

+

Le flux de travail avec v3 est similaire à v2 mais avec des branches différentes. +Avoir tous les tests de flux +complétés avec assez de succès est un des critères d’acceptation dans development.

+

Pour exécuter les tests sr_insects, le référentiel doit être cloné avec la branche development. +Une porte pour la fusion à development est pour un deuxième développeur d’exécuter le flow_tests. +Pour la v03, ces tests doivent exécuter : static_flow, flakey_broker, dynamic_flow, transform_flow

+

Prévu d’ici le 11/04/2022:

+
    +
  • Main sera fusionné à partir de development, donc la branche par défaut pour les nouveaux arrivants sera SR3.

  • +
  • Launchpad aura de nouvelles recettes pour produire des paquets SR3 à partir de la branche principale.

  • +
+
+
+

sr_insects

+

Le référentiel sr_insects a sa propre base de données de problèmes, et le travail sur sr_insects est encouragé. +Les versions 2 et 3 sont prises en charge sur la branche principale de sr_insects. Cette branche devrait être +utilisé pour prendre en charge tout le développement dans les deux versions…

+
+
+
+

Installation locale

+

Il existe de nombreuses façons d’installer des paquets python sur un ordinateur. Différents développeurs +préféreront différentes méthodes, et toutes les méthodes doivent être testées avant chaque version. +Avant d´installer le paquet il faut généralement une librarie pour communiquer avec le courtier +de messages (généralement rabbitmq/AMQP, mais ca peut être MQTT également)

+
    +
  • Wheel Lorsque les gens utilisent différents systèmes d’exploitation (non-Ubuntu, non-Debian), +les gens installent des wheel, généralement qui ont été téléchargées sur pypi.python.org. D’un +autre côté, c’est un peu pénible / bruyant de télécharger chaque version de développement, donc +nous ne téléchargeons que des versions, donc les tests de wheel se font en construisant des roues +locales. Besoin de construire une nouvelle wheel chaque fois qu’un changement est apporté. +pip install amqp sera également nécessaire pour le support rabbitmq.

  • +
  • pip install (pas -e) tirerait une wheel vers le bas de pypi.python.org. Généralement pas utilisé +pendant le développement de Sarracenia lui-même. +pip install amqp sera également nécessaire pour le support rabbitmq.

  • +
  • pip install -e … vous permet de modifier le code source du package installé, idéal pour les +problèmes de débogage, car il permet des modifications en direct de l’application sans avoir à passer +par la construction et l’installation d’un nouveau package. +pip install amqp sera également nécessaire pour le support rabbitmq.

  • +
  • apt install installer le paquet Debian à partir de dépôts, de la même manière que pip install (pas -e), +normalement les instantanés de développement ne sont pas téléchargés vers des dépôts, donc bien que ce soit +la manière normale pour les utilisateurs de serveurs Ubuntu, il n’est pas disponible pendant le développement +du paquet lui-même. +apt install python3-amqp sera également nécessaire pour le support rabbitmq.

  • +
  • dpkg -i construit un paquet Debian pour l’installation locale. C’est ainsi que les packages sont testés +avant d’être téléchargés vers des référentiels. Il peut également être utilisé pour soutenir le développement +(il faut exécuter dpkg -i pour chaque changement de paquet). +apt install python3-amqp sera également nécessaire pour le support rabbitmq.

  • +
+

Le test sr_insects appelle la version de metpx-sarracenia installée sur le système, +et non ce qui est dans l’arbre de développement. Il est nécessaire d’installer le paquet sur +le système afin qu’il exécute les tests sr_insects.

+
+

Préparer une machine virtuelle Vanilla

+

Cette section décrit la création d’un environnement de test à utiliser dans un ordinateur virtuel. Une façon de +construire une machine virtuelle est d’utiliser Multipass (https://multipass.run) En supposant que ce soit +installé, on peut crée une machine virtuelle avec

+
multipass launch -m 8G -d 30G --name flow
+
+
+

Vous devez faire fonctionner SSH LocalHost dans le conteneur MultiPass. Peut le faire en copiant la clé privée +multipasse dans le conteneur:

+
fractal% multipass list
+Name                    State             IPv4             Image
+primary                 Stopped           --               Ubuntu 20.04 LTS
+flow                    Running           10.23.119.56     Ubuntu 20.04 LTS
+keen-crow               Running           10.23.119.5      Ubuntu 20.04 LTS
+fractal%
+
+
+

Problèmes étranges avec les clés ssh qui ne sont pas interprétées correctement par paramiko, solution de contournement, +( https://stackoverflow.com/questions/54612609/paramiko-not-a-valid-rsa-private-key-file )

+
fractal% sudo cat /var/snap/multipass/common/data/multipassd/ssh-keys/id_rsa | sed 's/BEGIN .*PRIVATE/BEGIN RSA PRIVATE/;s/END .*PRIVATE/END RSA PRIVATE/' >id_rsa_container
+chmod 600 id_rsa_container
+scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.175:/home/ubuntu/.ssh/id_rsa
+                                                                  100% 1704     2.7MB/s   00:00
+
+fractal% scp -i id_rsa_container id_rsa_container ubuntu@10.23.119.106:/home/ubuntu/.ssh/id_rsa
+The authenticity of host '10.23.119.106 (10.23.119.106)' can't be established.
+ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8.
+Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
+Warning: Permanently added '10.23.119.106' (ECDSA) to the list of known hosts.
+id_rsa_container                                                                                                                         100% 1712     9.4MB/s   00:00
+fractal% multipass shell flow
+Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-81-generic x86_64)
+
+ * Documentation:  https://help.ubuntu.com
+ * Management:     https://landscape.canonical.com
+ * Support:        https://ubuntu.com/advantage
+
+  System information as of Fri Aug 27 21:12:16 EDT 2021
+
+  System load:  0.42              Processes:             112
+  Usage of /:   4.4% of 28.90GB   Users logged in:       0
+  Memory usage: 5%                IPv4 address for ens4: 10.23.119.106
+  Swap usage:   0%
+
+
+1 update can be applied immediately.
+To see these additional updates run: apt list --upgradable
+
+
+To run a command as administrator (user "root"), use "sudo <command>".
+See "man sudo_root" for details.
+
+ubuntu@flow:~$
+
+
+

Invitez ensuite SSH à accepter la clé localhost:

+
ubuntu@flow:~$ ssh localhost ls -a
+The authenticity of host 'localhost (127.0.0.1)' can't be established.
+ECDSA key fingerprint is SHA256:jlRnxV7udiCBdAzCvOVgTu0MYJR5+kYzNwy/DIhkeD8.
+Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
+Warning: Permanently added 'localhost' (ECDSA) to the list of known hosts.
+.
+..
+.bash_logout
+.bashrc
+.cache
+.profile
+.ssh
+ubuntu@flow:~$
+
+
+

Cela fournira un shell dans une machine virtuelle initialisée. Pour le configurer:

+
git clone -b development https://github.com/MetPX/sarracenia sr3
+cd sr3
+
+
+

Il existe des scripts qui automatisent l’installation de l’environnement nécessaire pour pouvoir exécuter des tests:

+
travis/flow_autoconfig.sh
+travis/add_sr3.sh
+
+
+

ous devriez pouvoir voir une configuration vide:

+
sr3 status
+
+
+

SR3C et SR3 sont maintenant installés et devraient être prêts à exécuter un test de débit à partir du module +sr_insects, qui a également été cloné

+
cd ../sr_insects
+
+
+

La branche v03 de sr_insects prend en charge les tests des versions 2 et 3, et les deux versions sont maintenant +installées. Les tests de flux sont destinés à être exécutés pour confirmer la compatibilité entre v2 et v3, +et donc doivent également pouvoir tester la V2

+
ubuntu@flow:~/sr_insects$ dpkg -l | grep metpx
+ii  metpx-libsr3c                    3.21.08a1-0~202108270410~ubuntu20.04.1 amd64        C-Implementation of a Sarracenia Client
+ii  metpx-sarracenia                 2.21.08-0~202108241854~ubuntu20.04.1   all          Directory mirroring in real-time for users, file servers and web sites.
+ii  metpx-sr3                        3.00.008exp                            all          v3 Directory mirroring in real-time for users, file servers and web sites.
+ii  metpx-sr3c                       3.21.08a1-0~202108270410~ubuntu20.04.1 amd64        C-Implementation of a Sarracenia Client
+ubuntu@flow:~/sr_insects$
+
+
+

Le paquet v2 est metpx-sarracenia, tandis que le paquet v3 est metpx-sr3. Les tests de débit détecteront +quelle version est installée et testera v3 si les deux sont présents. Pour remplacer cela

+
ubuntu@flow:~/sr_insects$ export sarra_py_version=2.21.08
+ubuntu@flow:~/sr_insects$
+
+
+

Ensuite, on peut exécuter flow_tests à partir de ce shell normalement.

+
+
+

Python Wheel

+

Si vous n’avez pas utilisé add_sr3.sh (qui construit un paquet debian), alors on peut utiliser cette procédure +Pour une installation locale sur un ordinateur avec un Python Wheel pour les tests et le développement:

+
python3 setup.py bdist_wheel
+
+
+

ou… Sur les systèmes plus récents, utilisez build à la place

+
python3 -m build --no-isolation
+
+
+

Doit construire un Wheel dans le sous-répertoire dist. +Ensuite, en tant que root, installez ce nouveau paquet:

+
pip3 install --upgrade ...<path>/dist/metpx*.whl
+
+
+
+
+

Pip install locale

+

Pour une installation locale sur un ordinateur, à l’aide d’un pip. +Pour les tests et le développement:

+
pip3 install -e .
+export PATH=${HOME}/.local/bin:${PATH}
+
+
+

Utilisation du programme d’installation du package python local (PIP) pour créer une version modifiable localement. +Le code en dessus installera le paquet dans ~/.local/bin… Il faut donc s’assurer que le chemin comprend +ce répertoire.

+
+
+

Debian/Ubuntu

+

Pour une installation locale sur un ordinateur, en utilisant un paquet debian. +Ce processus construit un .deb local dans le répertoire parent en utilisant les mécanismes Debian standard.

+
    +
  • Vérifiez la ligne build-depends dans debian/control pour les dépendances qui pourraient être nécessaires +pour construire à partir de source.

  • +
  • Les étapes suivantes feront un build de sarracenia mais ne signeront pas les modifications ou le paquet source

    +
    cd metpx/sarracenia
    +sudo apt-get install devscripts
    +debuild -uc -us
    +sudo dpkg -i ../<the package just built>
    +
    +
    +
  • +
+

qui accomplit la même chose en utilisant l’empaquetage Debian. +Les options sont détaillées ci-dessous :

+
+
+

Commit de Code

+

Que faut-il faire avant de s’engager dans la branche principale? +Liste de contrôle:

+
    +
  • faire du développement sur une autre branche. Habituellement, la branche sera nommée d’après le problème +adressée. Exemple : issue240. Si nous abandonnons une approche initiale et en commençons une autre, +il peut y avoir issue240_2 pour une deuxième tentative. Il peut également y avoir des branches de fonctionnalités, +telles que v03.

  • +
  • Les tests sr_insects fonctionnent (Voir Tests) La branche principale doit toujours être fonctionnelle, +ne validez pas de code si les tests sr_insects ne fonctionnent pas.

  • +
  • Conséquence naturelle : si les changements de code signifient que les tests doivent changer, incluez le +changement de test dans le commit.

  • +
  • Mettre à jour la doc/ Les pages de manuel devraient idéalement recevoir leurs mises à jour en même temps +que le code.

  • +
+

Habituellement, il y aura beaucoup de cycles de ce type sur une branche de développement avant que l’un d’eux +ne soit prêt à émettre une pull request. Finalement, nous arrivons à Commits vers la branche principale

+
+
+
+

Description des Tests sr_insects

+

Avant de valider du code dans la branche principale, en tant que mesure d’assurance qualité, il faut exécuter +tous les autotests disponibles. On suppose que les modifications spécifiques apportées au code ont déjà passé +les tests unitaires. Veuillez ajouter des autotests appropriés à ce processus pour refléter les nouveaux. +D’une manière générale, il faut résoudre les problèmes au premier test qui échoue parce que chaque test +est plus compliqué que le précédent.

+

Il existe un dépôt git séparé contenant les tests les plus complexes https://github.com/MetPX/sr_insects

+

Un flux de travail de développement typique sera (N’essayez pas ceci, ceci est juste un aperçu des étapes qui seront +expliqué en détail dans les sections suivantes)

+
git branch issueXXX
+git checkout issueXXX
+cd sarra ; *make coding changes*
+cd ..
+debuild -uc -us
+cd ../sarrac
+debuild -uc -us
+sudo dpkg -i ../*.deb
+cd ..
+
+git clone -b development https://github.com/MetPX/sr_insects
+cd sr_insects
+sr3 status  # make sure there are no components configured before you start.
+            # test results will likely be skewed otherwise.
+for test in unit static_flow flakey_browser transform_flow dynamic_flow; do
+   cd $test
+   ./flow_setup.sh  # *starts the flows*
+   ./flow_limit.sh  # *stops the flows after some period (default: 1000) *
+   ./flow_check.sh  # *checks the flows*
+   ./flow_cleanup.sh  # *cleans up the flows*
+   cd ..
+done
+
+#assuming all the tests pass.
+git commit -a  # on the branch...
+
+
+
+

Unit

+

Le test unitaire dans sr_insects est le plus court qui prend environ une minute et ne nécessite +pas beaucoup de configuration. Ce sont des tests de santé mentale du comportement du code. Prend +généralement une minute ou deux sur un ordinateur portable.

+
+
+

Static Flow

+

Les tests static_flow sont un peu plus compliqués, testant plus de composants, utilisant un seul +composants monothread de manière linéaire (toutes les données avancent uniformément.) Il devrait être +plus simple pour identifier les problèmes car il n’y a pas de suppression et donc cela se prête bien +à répéter des tests de sous-ensembles pour identifier les problèmes individuels. Cela prend environ +deux minutes sur un ordinateur portable.

+
+
+

Flakey Broker

+

Les tests flakey_broker sont les mêmes que les static_flow, mais ralentis de sorte qu’ils durent +quelques minutes de plus, et le courtier est arrêté et redémarré pendant que la publication se produit. +Notez que post_log imprime avant qu’un message de notification ne soit publié (parce que post_log est +un plugin on_post, et cette action, permet de modifier le message de notification, donc il doit être +avant que la publication ne se produise réellement.)

+
+
+

Dynamic Flow

+

Le test dynamic_flow ajoute des fonctionnalités avancées : multi-instances, le composant winnow, +les tests logiques de nouvelle tentative, et inclut également les suppressions de fichiers. La majeure +partie de la documentation ici fait référence à l’exécution du dynamic_flow test, car c’est le plus +compliqué, et l’ancêtre des autres. Le test unitaire a été séparé du début du test dynamic_flow, +et le static_flow est une version simplifiée du test de flux original.

+

D’une manière générale, il convient d’exécuter les tests en séquence et de s’assurer que les résultats des tests +antérieurs soient sont bons avant de passer aux tests suivant.

+

Notez que le système de développement doit être configuré pour que les tests sr_insects s’exécutent correctement. +Voir la suite pour les instructions de configuration. Pour le développement avec une nouvelle installation du +système d’exploitation, les étapes de configuration ont été automatisées et peuvent être appliquées avec le +flow_autoconfig.sh dans sr_insects (https://github.com/MetPX/sr_insects/blob/stable/flow_autoconfig.sh). +L’exécution à l’aveugle de ce script sur un système fonctionnel peut entraîner des effets secondaires +indésirables; vous êtes prévenus !

+

La configuration que l’on essaie de répliquer :

+../../_images/Flow_test.svg

Le tableau suivant décrit ce que fait chaque élément du test de flux dynamique, et la couverture +du test montre les fonctionnalités couvertes.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Configuration

Fait

Couverture du test

subscribe t_ddx

copy from data mart to local broker +posting notification messages to +local xwinno00 and xwinnow01 +exchanges.

lit le data mart public amqps (v02) +comme utilisateur ordinaire.

+

fil d’attente partagé et plusieurs +processus +3 instances téléchargent de chaque q +poster amqp à un échange local (v02) +en tant qu’utilisatuer feeder +(admin) +post_exchangeSplit à xwinnow0x

+

winnow t0x_f10

traitement winnow qui publie pour +l’échange xsarra pour le +téléchargement.

+

Comme deux sources identiques +seulement la moitié des messages de +notifications sont postés au suivant

+

Lire l’AMQP v02 local +en tant qu’utilisateur de feeder.

+

Fonction de mise en cache complète +(Winnow) +post amqp v02 à l’échange local.

+

sarra download +f20

Téléchargez les données Winnowed à +partir du répertoire local data mart +(TESTDOCROOT= ~/sarra_devdocroot)

+

Ajouter un en-tête au niveau de la +couche d’application plus de 255 +caractères.

+

Lire l’AMQP v02 local (xsarra)

+

Télécharger à l’aide de Python +intégré +fil d’attente partagé et plusieurs +processus +5 instances téléchargent de chaque q +télécharge avec accel_wget plugin

+

Troncature d’en-tête AMQP lors de +la publication. +post amqp v02 à xpublic +en tant qu’utilisateur feeder +téléchargements HTTP depuis localhst

+

subscribe t

Télécharge en tant que client à +partir de localhost au repertoire +downloaded_by_sub_t.

lire amqp du courtier local +en tant qu’utilisatuer/client +ordinaire. +fil d’attente partagé et plusieurs +processus +5 instances téléchargent de chaque q

watch f40

regarder downloaded_by_sub_t +Publier chaque fichier qui y apparaît

+

Plafond de mémoire réglé bas

+

client v03 post du fichier local. +(fichier: url)

+

Redémarrage automatique au plafond +de la mémoire.

+

sender +tsource2send

lire fichier local, envoyer via sftp +au répertoire sent_by_tsource2send

+

Publier sur xs_tsource_output

+

client consomme des messages de +notification v03 +Le consommateur lit le fichier local

+

envoyer via sftp.

+

plugin replace_dir

+

Affichage de l’URL sftp. +Post V02 (reconversion de V03.)

+

option test post_exchange_suffix.

+

subscribe +u_sftp_f60

Télécharger via sftp à partir de +localhost en plaçant les fichiers +dans le répertoire +downloaded_by_sub_u

Téléchargement SFTP client.

+

accel_sftp plugin.

+

post test2_f61

Publier des fichiers dans +sent_by_tsource2send avec des URL +FTP dans l’échange xs_tsource_poll

+

(Wrapper Script appelle post)

+

Affichage explicite de fichiers

+

Publication d’URL ftp.

+

option post_exchange_suffix

+

poll f62

poller le répertoire +sent_by_tsource2send +publication d’URL de téléchargement +sftp

+

polling

+
+

option post_exchange_suffix

+

subscribe ftp_f70

Abonnez-vous aux publications +test2_f61 FTP. +Télécharger des fichiers depuis +localhost au répertoire +downloaded_by_sub_u

Téléchargement d’URL FTP.

subscribe q_f71

s’abonner au sondage, téléchargement +sur recd_by_srpoll_test1

Confirmation de la qualité du poste +de poll

shovel pclean f90

Nettoyez les fichiers pour qu’ils ne +s’accumulent pas +Simule les échecs pour forcer des +nouvelles tentatives

fonction shovel.

+

Logique de nouvelle tentative.

+

shovel pclean f91

Nettoyez les fichiers pour qu’ils ne +s’accumulent pas

shovel avec posting v03

+

Logique de nouvelle tentative.

+

shovel pclean f92

Nettoyez les fichiers pour qu’ils ne +s’accumulent pas

shovel avec consommation v03

+

posting v02.

+

Logique de nouvelle tentative.

+
+

Hypothèse: l’environnement de test est un PC Linux, soit un ordinateur portable/de bureau, soit un serveur sur lequel on +peut démarrer un navigateur. Si vous travaillez également avec l’implémentation C, il existe également les éléments suivants +Flux définis :

+../../_images/cFlow_test.svg
+
+
+

Exécution de tests de Flux

+

Cette section documente ces étapes de manière beaucoup plus détaillée. +Avant de pouvoir effectuer les tests sr_insects, certaines conditions préalables doivent être prises en compte. +Notez qu’il existe une intégration Github Actions pour au moins la branche principale +pour vérifier la fonctionnalité sur une variété de versions de Python. Consulter:

+
https://github.com/MetPX/sarracenia/actions
+
+
+
+

Note

+

Pour les derniers résultats des tests. Notez que les résultats incluent des dizaines +de tests et sont peu fiables, il peut généralement falloir quelques tentatives pour +que cela fonctionne complètement (3 ou 4 échouent après la tentative initiale, puis +réexécutez les échecs, puis peut-être un ou deux resteront, et le troisième passe, et le dernier passe.)

+
+
+

Installer des serveurs sur un poste de travail

+

Pour préparer un ordinateur à exécuter le test de flux, il faut installer des logiciels et des +configurations serveur. Ce même travail est effectué par travis/flow_autoconfig.sh +qui est exécuté dans Préparer une machine virtuelle Vanilla mais si vous devez le configurer +manuellement, voici le processus.

+

Installez un courtier localhost minimal et configurez les utilisateurs de test rabbitmq

+
sudo apt-get install rabbitmq-server
+sudo rabbitmq-plugins enable rabbitmq_management
+
+mkdir ~/.config/sarra
+cat > ~/.config/sarra/default.conf << EOF
+declare env FLOWBROKER=localhost
+declare env MQP=amqp
+declare env SFTPUSER=${USER}
+declare env TESTDOCROOT=${HOME}/sarra_devdocroot
+declare env SR_CONFIG_EXAMPLES=${HOME}/git/sarracenia/sarra/examples
+EOF
+
+RABBITMQ_PASS=S0M3R4nD0MP4sS
+cat > ~/.config/sarra/credentials.conf << EOF
+amqp://bunnymaster:${RABBITMQ_PASS}@localhost/
+amqp://tsource:${RABBITMQ_PASS}@localhost/
+amqp://tsub:${RABBITMQ_PASS}@localhost/
+amqp://tfeed:${RABBITMQ_PASS}@localhost/
+amqp://anonymous:${RABBITMQ_PASS}@localhost/
+amqps://anonymous:anonymous@hpfx.collab.science.gc.ca
+amqps://anonymous:anonymous@hpfx1.collab.science.gc.ca
+amqps://anonymous:anonymous@hpfx2.collab.science.gc.ca
+amqps://anonymous:anonymous@dd.weather.gc.ca
+amqps://anonymous:anonymous@dd1.weather.gc.ca
+amqps://anonymous:anonymous@dd2.weather.gc.ca
+ftp://anonymous:anonymous@localhost:2121/
+EOF
+
+cat > ~/.config/sarra/admin.conf << EOF
+cluster localhost
+admin amqp://bunnymaster@localhost/
+feeder amqp://tfeed@localhost/
+declare source tsource
+declare subscriber tsub
+declare subscriber anonymous
+EOF
+
+sudo rabbitmqctl delete_user guest
+
+sudo rabbitmqctl add_user bunnymaster ${RABBITMQ_PASS}
+sudo rabbitmqctl set_permissions bunnymaster ".*" ".*" ".*"
+sudo rabbitmqctl set_user_tags bunnymaster administrator
+
+sudo systemctl restart rabbitmq-server
+cd /usr/local/bin
+sudo mv rabbitmqadmin rabbitmqadmin.1
+sudo wget http://localhost:15672/cli/rabbitmqadmin
+sudo chmod 755 rabbitmqadmin
+
+sr3 --users declare
+
+
+
+

Note

+

Veuillez utiliser d’autres mots de passe dans les informations d’identification pour votre configuration, juste au cas où. +Les mots de passe ne doivent pas être hard-codés dans la suite d’autotests. +Les utilisateurs bunnymaster, tsource, tsub et tfeed doivent être utilisés pour exécuter des tests.

+

L’idée ici est d’utiliser tsource, tsub et tfeed comme comptes de courtier pour tous les +auto-test des opérations et stocker les informations d’identification dans le fichier credentials.conf normal. +Aucun mot de passe ou fichier clé ne doit être stocké dans l’arborescence source, dans le cadre d’une suite auto-test.

+
+
+
+

Configurer l’environnement de test de flux

+

Une fois l’environnement serveur établi, les tests de flux utilisent des transferts sftp vers localhost.

+

Il est également nécessaire que l’accès ssh sans mot de passe soit configuré sur l’hôte de test +pour l’utilisateur de système qui exécutera le test de flux. Cela peut être fait en créant une +paire de clés ssh privée/publique pour l’utilisateur (s’il n’y en a pas déjà) et en copiant la +clé publique dans le fichier authorized_keys dans le même répertoire que les clés (~/.ssh). +Pour les commandes associées, reportez-vous à la section http://www.linuxproblem.org/art_9.html

+

Notez que sur les systèmes où les anciennes versions de Paramiko (< 2.7.2) sont installées, +et où la paire de clés ssh a été générée avec OpenSSH >= 6.5, tester manuellement la commande +ci-dessous fonctionnera, mais Paramiko ne pourra pas se connecter. C’est probablement le cas s +i le fichier ~/.ssh/id_rsa contient BEGIN OPENSSH PRIVATE KEY. Pour contourner ce problème, +convertissez le format de la clé privée en utilisant ssh-keygen -p -m PEM -f ~/.ssh/id_rsa.

+

Pour confirmer que ce ssh sans mot de passe vers localhost fonctionne

+
ssh localhost ls
+
+
+

Cela devrait s’exécuter et se terminer. S’il vous demande un mot de passe, les tests de flux ne fonctionneront pas.

+

Vérifiez que le courtier focntionne:

+
systemctl status rabbitmq-server
+
+
+

Une partie du test de flux exécute un serveur sftp et utilise des fonctions client sftp. +Besoin du package suivant pour cela:

+
sudo apt-get install python3-pyftpdlib python3-paramiko
+
+
+

Le script d’installation démarre un serveur Web trivial, un serveur ftp et un démon que sr3_post appelle. +Il teste également les composants C, qui doivent également avoir déjà été installés. +et définit certains clients de test fixes qui seront utilisés lors des auto-tests

+
cd
+git clone https://github.com/MetPX/sr_insects
+cd sr_insects
+cd static_flow
+. ./flow_setup.sh
+
+blacklab% ./flow_setup.sh
+cleaning logs, just in case
+rm: cannot remove '/home/peter/.cache/sarra/log/*': No such file or directory
+Adding flow test configurations...
+2018-02-10 14:22:58,944 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/cno_trouble_f00.inc to /home/peter/.config/sarra/cpump/cno_trouble_f00.inc.
+2018-02-10 09:22:59,204 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/no_trouble_f00.inc to /home/peter/.config/sarra/shovel/no_trouble_f00.inc
+2018-02-10 14:22:59,206 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpost/veille_f34.conf to /home/peter/.config/sarra/cpost/veille_f34.conf.
+2018-02-10 14:22:59,207 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd1_f04.conf to /home/peter/.config/sarra/cpump/pelle_dd1_f04.conf.
+2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/pelle_dd2_f05.conf to /home/peter/.config/sarra/cpump/pelle_dd2_f05.conf.
+2018-02-10 14:22:59,208 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f14.conf to /home/peter/.config/sarra/cpump/xvan_f14.conf.
+2018-02-10 14:22:59,209 [INFO] copying /usr/lib/python3/dist-packages/sarra/examples/cpump/xvan_f15.conf to /home/peter/.config/sarra/cpump/xvan_f15.conf.
+2018-02-10 09:22:59,483 [INFO] copying /home/peter/src/sarracenia/sarra/examples/poll/f62.conf to /home/peter/.config/sarra/poll/f62.conf
+2018-02-10 09:22:59,756 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/shim_f63.conf to /home/peter/.config/sarra/post/shim_f63.conf
+2018-02-10 09:23:00,030 [INFO] copying /home/peter/src/sarracenia/sarra/examples/post/test2_f61.conf to /home/peter/.config/sarra/post/test2_f61.conf
+2018-02-10 09:23:00,299 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/tsarra_f20.conf to /home/peter/.config/sarra/report/tsarra_f20.conf
+2018-02-10 09:23:00,561 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow00_f10.conf to /home/peter/.config/sarra/report/twinnow00_f10.conf
+2018-02-10 09:23:00,824 [INFO] copying /home/peter/src/sarracenia/sarra/examples/report/twinnow01_f10.conf to /home/peter/.config/sarra/report/twinnow01_f10.conf
+2018-02-10 09:23:01,086 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sarra/download_f20.conf to /home/peter/.config/sarra/sarra/download_f20.conf
+2018-02-10 09:23:01,350 [INFO] copying /home/peter/src/sarracenia/sarra/examples/sender/tsource2send_f50.conf to /home/peter/.config/sarra/sender/tsource2send_f50.conf
+2018-02-10 09:23:01,615 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd1_f00.conf to /home/peter/.config/sarra/shovel/t_dd1_f00.conf
+2018-02-10 09:23:01,877 [INFO] copying /home/peter/src/sarracenia/sarra/examples/shovel/t_dd2_f00.conf to /home/peter/.config/sarra/shovel/t_dd2_f00.conf
+2018-02-10 09:23:02,137 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cclean_f91.conf to /home/peter/.config/sarra/subscribe/cclean_f91.conf
+2018-02-10 09:23:02,400 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cdnld_f21.conf to /home/peter/.config/sarra/subscribe/cdnld_f21.conf
+2018-02-10 09:23:02,658 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cfile_f44.conf to /home/peter/.config/sarra/subscribe/cfile_f44.conf
+2018-02-10 09:23:02,921 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/clean_f90.conf to /home/peter/.config/sarra/subscribe/clean_f90.conf
+2018-02-10 09:23:03,185 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/cp_f61.conf to /home/peter/.config/sarra/subscribe/cp_f61.conf
+2018-02-10 09:23:03,455 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/ftp_f70.conf to /home/peter/.config/sarra/subscribe/ftp_f70.conf
+2018-02-10 09:23:03,715 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/q_f71.conf to /home/peter/.config/sarra/subscribe/q_f71.conf
+2018-02-10 09:23:03,978 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/t_f30.conf to /home/peter/.config/sarra/subscribe/t_f30.conf
+2018-02-10 09:23:04,237 [INFO] copying /home/peter/src/sarracenia/sarra/examples/subscribe/u_sftp_f60.conf to /home/peter/.config/sarra/subscribe/u_sftp_f60.conf
+2018-02-10 09:23:04,504 [INFO] copying /home/peter/src/sarracenia/sarra/examples/watch/f40.conf to /home/peter/.config/sarra/watch/f40.conf
+2018-02-10 09:23:04,764 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t00_f10.conf to /home/peter/.config/sarra/winnow/t00_f10.conf
+2018-02-10 09:23:05,027 [INFO] copying /home/peter/src/sarracenia/sarra/examples/winnow/t01_f10.conf to /home/peter/.config/sarra/winnow/t01_f10.conf
+Initializing with sr_audit... takes a minute or two
+OK, as expected 18 queues existing after 1st audit
+OK, as expected 31 exchanges for flow test created.
+Starting trivial http server on: /home/peter/sarra_devdocroot, saving pid in .httpserverpid
+Starting trivial ftp server on: /home/peter/sarra_devdocroot, saving pid in .ftpserverpid
+running self test ... takes a minute or two
+sr_util.py TEST PASSED
+sr_credentials.py TEST PASSED
+sr_config.py TEST PASSED
+sr_cache.py TEST PASSED
+sr_retry.py TEST PASSED
+sr_consumer.py TEST PASSED
+sr_http.py TEST PASSED
+sftp testing start...
+sftp testing config read...
+sftp testing fake message built ...
+sftp sr_ftp instantiated ...
+sftp sr_ftp connected ...
+sftp sr_ftp mkdir ...
+test 01: directory creation succeeded
+test 02: file upload succeeded
+test 03: file rename succeeded
+test 04: getting a part succeeded
+test 05: download succeeded
+test 06: onfly_checksum succeeded
+Sent: bbb  into tztz/ddd 0-5
+test 07: download succeeded
+test 08: delete succeeded
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+Sent: bbb  into tztz/ddd 0-5
+/home/peter
+/home/peter
+test 09: bad part succeeded
+sr_sftp.py TEST PASSED
+sr_instances.py TEST PASSED
+OK, as expected 9 tests passed
+Starting flow_post on: /home/peter/sarra_devdocroot, saving pid in .flowpostpid
+Starting up all components (sr start)...
+done.
+OK: sr3 start was successful
+Overall PASSED 4/4 checks passed!
+blacklab%
+
+
+

Lorsqu’il exécute le programme d’installation, il exécute également tous les unit_tests existants. +Ne passez aux tests flow_check que si tous les tests de flow_setup.sh réussissent.

+
+
+

Exécuter un test de Flux

+

Le script flow_check.sh lit les fichiers journaux de tous les composants démarrés et compare +le nombre de messages de notification, à la recherche d’une correspondance dans les +- 10%. +Il faut quelques minutes pour que la configuration s’exécute avant qu’il y ait suffisamment +de données pour effectuer les mesures correctes

+
./flow_limit.sh
+
+
+

sample output:

+
initial sample building sample size 8 need at least 1000
+sample now   1021
+Sufficient!
+stopping shovels and waiting...
+2017-10-28 00:37:02,422 [INFO] sr_shovel t_dd1_f00 0001 stopping
+2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: instances option not implemented, ignored.
+info: instances option not implemented, ignored.
+2017-10-28 04:37:02,435 [INFO] 2017-10-28 04:37:02,435 [INFO] info: report option not implemented, ignored.
+info: report option not implemented, ignored.
+2017-10-28 00:37:02,436 [INFO] sr_shovel t_dd2_f00 0001 stopping
+running instance for config pelle_dd1_f04 (pid 15872) stopped.
+running instance for config pelle_dd2_f05 (pid 15847) stopped.
+    maximum of the shovels is: 1022
+
+
+

Ensuite, vérifiez avec flow_check.sh:

+
TYPE OF ERRORS IN LOG :
+
+  1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/'
+  1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/'
+
+
+test  1 success: shovels t_dd1_f00 ( 1022 ) and t_dd2_f00 ( 1022 ) should have about the same number of items read
+test  2 success: sarra tsarra (1022) should be reading about half as many items as (both) winnows (2240)
+test  3 success: tsarra (1022) and sub t_f30 (1022) should have about the same number of items
+test  4 success: max shovel (1022) and subscriber t_f30 (1022) should have about the same number of items
+test  5 success: count of truncated headers (1022) and subscribed messages (1022) should have about the same number of items
+test  6 success: count of downloads by subscribe t_f30 (1022) and messages received (1022) should be about the same
+test  7 success: downloads by subscribe t_f30 (1022) and files posted by sr3_watch (1022) should be about the same
+test  8 success: posted by watch(1022) and sent by sr_sender (1022) should be about the same
+test  9 success: 1022 of 1022: files sent with identical content to those downloaded by subscribe
+test 10 success: 1022 of 1022: poll test1_f62 and subscribe q_f71 run together. Should have equal results.
+test 11 success: post test2_f61 1022 and subscribe r_ftp_f70 1021 run together. Should be about the same.
+test 12 success: cpump both pelles (c shovel) should receive about the same number of messages (3665) (3662)
+test 13 success: cdnld_f21 subscribe downloaded (1022) the same number of files that was published by both van_14 and van_15 (1022)
+test 14 success: veille_f34 should post the same number of files (1022) that subscribe cdnld_f21 downloaded (1022)
+test 15 success: veille_f34 should post the same number of files (1022) that subscribe cfile_f44 downloaded (1022)
+test 16 success: Overall 15 of 15 passed!
+
+blacklab%
+
+
+

Si le flow_check.sh est adopté, alors on a une confiance raisonnable dans la fonctionnalité globale de +Python, mais la couverture du test n’est pas exhaustive. C’est la porte la plus basse pour s’engager +à des modifications apportées à votre code Python dans la branche principale. Il s’agit d’un échantillonnage plus qualitatif +des cas d’utilisation courants plutôt qu’un examen approfondi de toutes les fonctionnalités. Bien que ce ne soit pas le cas +approfondie, il est bon de savoir que les flux fonctionnent.

+

Notez que l’abonné fclean examine les fichiers et les conserve suffisamment longtemps pour qu’ils puissent +parcourir tous les autres tests. Il le fait en attendant un délai raisonnable (45 secondes, la dernière fois +vérifiée), puis il compare le fichier qui a été posté par sr3_watch aux fichiers créés en téléchargeant à partir +de celui-ci. Au fur et à mesure que le dénombrement sample now progresse, il imprime “OK” si les fichiers +téléchargés sont identiques à ceux postés par sr_watch. L’ajout de fclean et cfclean correspondant pour les +cflow_test sont cassés. La configuration par défaut qui utilise fclean et cfclean garantit que seulement +quelques minutes d’espace disque sont utilisées à un moment donné et permettent des tests beaucoup plus longs.

+

Par défaut, le flow_test n’est que de 1000 fichiers, mais on peut lui demander de fonctionner plus longtemps, +comme ceci:

+
./flow_limit.sh 50000
+
+
+

Pour accumuler cinquante mille fichiers avant de terminer le test. Cela permet de tester les performances +à long terme, en particulier l’utilisation de la mémoire au fil du temps et des fonctions d’entretien +du traitement on_heartbeat.

+
+
+

Flow Cleanup

+

Une fois le test terminé, exécutez le script ./flow_cleanup.sh, qui tuera les serveurs et les démons en cours +d’exécution et supprimera tous les fichiers de configuration installés pour le test de flux, toutes les files +d’attente, les échanges et les journaux. Cela doit également être fait entre chaque exécution du test de flux:

+
blacklab% ./flow_cleanup.sh
+Stopping sr...
+Cleanup sr...
+Cleanup trivial http server...
+web server stopped.
+if other web servers with lost pid kill them
+Cleanup trivial ftp server...
+ftp server stopped.
+if other ftp servers with lost pid kill them
+Cleanup flow poster...
+flow poster stopped.
+if other flow_post.sh with lost pid kill them
+Deleting queues:
+Deleting exchanges...
+Removing flow configs...
+2018-02-10 14:17:34,150 [INFO] info: instances option not implemented, ignored.
+2018-02-10 14:17:34,150 [INFO] info: report option not implemented, ignored.
+2018-02-10 14:17:34,353 [INFO] info: instances option not implemented, ignored.
+2018-02-10 14:17:34,353 [INFO] info: report option not implemented, ignored.
+2018-02-10 09:17:34,837 [INFO] sr_poll f62 cleanup
+2018-02-10 09:17:34,845 [INFO] deleting exchange xs_tsource_poll (tsource@localhost)
+2018-02-10 09:17:35,115 [INFO] sr3_post shim_f63 cleanup
+2018-02-10 09:17:35,122 [INFO] deleting exchange xs_tsource_shim (tsource@localhost)
+2018-02-10 09:17:35,394 [INFO] sr3_post test2_f61 cleanup
+2018-02-10 09:17:35,402 [INFO] deleting exchange xs_tsource_post (tsource@localhost)
+2018-02-10 09:17:35,659 [INFO] sr_report tsarra_f20 cleanup
+2018-02-10 09:17:35,659 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:35,661 [INFO] deleting queue q_tfeed.sr_report.tsarra_f20.89336558.04455188 (tfeed@localhost)
+2018-02-10 09:17:35,920 [INFO] sr_report twinnow00_f10 cleanup
+2018-02-10 09:17:35,920 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:35,922 [INFO] deleting queue q_tfeed.sr_report.twinnow00_f10.35552245.50856337 (tfeed@localhost)
+2018-02-10 09:17:36,179 [INFO] sr_report twinnow01_f10 cleanup
+2018-02-10 09:17:36,180 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:36,182 [INFO] deleting queue q_tfeed.sr_report.twinnow01_f10.48262886.11567358 (tfeed@localhost)
+2018-02-10 09:17:36,445 [WARNING] option url deprecated please use post_base_url
+2018-02-10 09:17:36,446 [WARNING] use post_base_dir instead of document_root
+2018-02-10 09:17:36,446 [INFO] sr_sarra download_f20 cleanup
+2018-02-10 09:17:36,446 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:36,448 [INFO] deleting queue q_tfeed.sr_sarra.download_f20 (tfeed@localhost)
+2018-02-10 09:17:36,449 [INFO] exchange xpublic remains
+2018-02-10 09:17:36,703 [INFO] sr_sender tsource2send_f50 cleanup
+2018-02-10 09:17:36,703 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:36,705 [INFO] deleting queue q_tsource.sr_sender.tsource2send_f50 (tsource@localhost)
+2018-02-10 09:17:36,711 [INFO] deleting exchange xs_tsource_output (tsource@localhost)
+2018-02-10 09:17:36,969 [INFO] sr_shovel t_dd1_f00 cleanup
+2018-02-10 09:17:36,969 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2018-02-10 09:17:37,072 [INFO] deleting queue q_anonymous.sr_shovel.t_dd1_f00 (anonymous@dd.weather.gc.ca)
+2018-02-10 09:17:37,095 [INFO] exchange xwinnow00 remains
+2018-02-10 09:17:37,095 [INFO] exchange xwinnow01 remains
+2018-02-10 09:17:37,389 [INFO] sr_shovel t_dd2_f00 cleanup
+2018-02-10 09:17:37,389 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+2018-02-10 09:17:37,498 [INFO] deleting queue q_anonymous.sr_shovel.t_dd2_f00 (anonymous@dd.weather.gc.ca)
+2018-02-10 09:17:37,522 [INFO] exchange xwinnow00 remains
+2018-02-10 09:17:37,523 [INFO] exchange xwinnow01 remains
+2018-02-10 09:17:37,804 [INFO] sr_subscribe cclean_f91 cleanup
+2018-02-10 09:17:37,804 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:37,806 [INFO] deleting queue q_tsub.sr_subscribe.cclean_f91.39328538.44917465 (tsub@localhost)
+2018-02-10 09:17:38,062 [INFO] sr_subscribe cdnld_f21 cleanup
+2018-02-10 09:17:38,062 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:38,064 [INFO] deleting queue q_tfeed.sr_subscribe.cdnld_f21.11963392.61638098 (tfeed@localhost)
+2018-02-10 09:17:38,324 [WARNING] use post_base_dir instead of document_root
+2018-02-10 09:17:38,324 [INFO] sr_subscribe cfile_f44 cleanup
+2018-02-10 09:17:38,324 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:38,326 [INFO] deleting queue q_tfeed.sr_subscribe.cfile_f44.56469334.87337271 (tfeed@localhost)
+2018-02-10 09:17:38,583 [INFO] sr_subscribe clean_f90 cleanup
+2018-02-10 09:17:38,583 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:38,585 [INFO] deleting queue q_tsub.sr_subscribe.clean_f90.45979835.20516428 (tsub@localhost)
+2018-02-10 09:17:38,854 [WARNING] extended option download_cp_command = ['cp --preserve=timestamps'] (unknown or not declared)
+2018-02-10 09:17:38,855 [INFO] sr_subscribe cp_f61 cleanup
+2018-02-10 09:17:38,855 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:38,857 [INFO] deleting queue q_tsource.sr_subscribe.cp_f61.61218922.69758215 (tsource@localhost)
+2018-02-10 09:17:39,121 [INFO] sr_subscribe ftp_f70 cleanup
+2018-02-10 09:17:39,121 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,123 [INFO] deleting queue q_tsource.sr_subscribe.ftp_f70.47997098.27633529 (tsource@localhost)
+2018-02-10 09:17:39,386 [INFO] sr_subscribe q_f71 cleanup
+2018-02-10 09:17:39,386 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,389 [INFO] deleting queue q_tsource.sr_subscribe.q_f71.84316550.21567557 (tsource@localhost)
+2018-02-10 09:17:39,658 [INFO] sr_subscribe t_f30 cleanup
+2018-02-10 09:17:39,658 [INFO] AMQP  broker(localhost) user(tsub) vhost(/)
+2018-02-10 09:17:39,660 [INFO] deleting queue q_tsub.sr_subscribe.t_f30.26453890.50752396 (tsub@localhost)
+2018-02-10 09:17:39,924 [INFO] sr_subscribe u_sftp_f60 cleanup
+2018-02-10 09:17:39,924 [INFO] AMQP  broker(localhost) user(tsource) vhost(/)
+2018-02-10 09:17:39,927 [INFO] deleting queue q_tsource.sr_subscribe.u_sftp_f60.81353341.03950190 (tsource@localhost)
+2018-02-10 09:17:40,196 [WARNING] option url deprecated please use post_base_url
+2018-02-10 09:17:40,196 [WARNING] use post_broker to set broker
+2018-02-10 09:17:40,197 [INFO] sr3_watch f40 cleanup
+2018-02-10 09:17:40,207 [INFO] deleting exchange xs_tsource (tsource@localhost)
+2018-02-10 09:17:40,471 [INFO] sr_winnow t00_f10 cleanup
+2018-02-10 09:17:40,471 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:40,474 [INFO] deleting queue q_tfeed.sr_winnow.t00_f10 (tfeed@localhost)
+2018-02-10 09:17:40,480 [INFO] deleting exchange xsarra (tfeed@localhost)
+2018-02-10 09:17:40,741 [INFO] sr_winnow t01_f10 cleanup
+2018-02-10 09:17:40,741 [INFO] AMQP  broker(localhost) user(tfeed) vhost(/)
+2018-02-10 09:17:40,743 [INFO] deleting queue q_tfeed.sr_winnow.t01_f10 (tfeed@localhost)
+2018-02-10 09:17:40,750 [INFO] deleting exchange xsarra (tfeed@localhost)
+2018-02-10 14:17:40,753 [ERROR] config cno_trouble_f00 not found.
+Removing flow config logs...
+rm: cannot remove '/home/peter/.cache/sarra/log/sr_audit_f00.log': No such file or directory
+Removing document root ( /home/peter/sarra_devdocroot )...
+Done!
+
+
+

Après la flow_cleanup.sh, pour vérifier qu’un test est terminé, utiliser:

+
sr3 status
+
+
+

ce qui devrait montrer qu’il n’y a pas de configurations actives.

+

Si le test static_flow fonctionne, réexécutez les autres tests : flakey_broker, +transform_flow, et dynamic_flow.

+
+
+

Longueur du test Dynamic Flow

+

Alors que la plupart des tests ont une durée fixe, le test de flux dynamique interroge une serveur distant +et peut fonctionner pour n’importe quelle longueur souhaitée. La longueur du flow_test dynamique par défaut +est de 1000 fichiers circulant dans les cas de test. En cas de développement rapide, +on peut fournir un argument pour raccourcir cela:

+
./flow_limit.sh 200
+
+
+

Vers la fin d’un cycle de développement, des flow_tests plus longs sont conseillées:

+
./flow_limit.sh 20000
+
+
+

pour identifier plus de problèmes. Exemple d’exécution à 100 000 entrées

+
blacklab% ./flow_limit.sh 100000
+initial sample building sample size 155 need at least 100000
+sample now 100003 content_checks:GOOD missed_dispositions:0s:0
+Sufficient!
+stopping shovels and waiting...
+2018-02-10 13:15:08,964 [INFO] 2018-02-10 13:15:08,964 [INFO] info: instances option not implemented, ignored.
+info: instances option not implemented, ignored.
+2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored.
+2018-02-10 13:15:08,964 [INFO] info: report option not implemented, ignored.
+running instance for config pelle_dd2_f05 (pid 20031) stopped.
+running instance for config pelle_dd1_f04 (pid 20043) stopped.
+Traceback (most recent call last):ng...
+  File "/usr/bin/rabbitmqadmin", line 1012, in <module>
+    main()
+  File "/usr/bin/rabbitmqadmin", line 413, in main
+    method()
+  File "/usr/bin/rabbitmqadmin", line 593, in invoke_list
+    format_list(self.get(uri), cols, obj_info, self.options)
+  File "/usr/bin/rabbitmqadmin", line 710, in format_list
+    formatter_instance.display(json_list)
+  File "/usr/bin/rabbitmqadmin", line 721, in display
+    (columns, table) = self.list_to_table(json.loads(json_list), depth)
+  File "/usr/bin/rabbitmqadmin", line 775, in list_to_table
+    add('', 1, item, add_to_row)
+  File "/usr/bin/rabbitmqadmin", line 742, in add
+    add(column, depth + 1, subitem, fun)
+  File "/usr/bin/rabbitmqadmin", line 742, in add
+    add(column, depth + 1, subitem, fun)
+  File "/usr/bin/rabbitmqadmin", line 754, in add
+    fun(column, subitem)
+  File "/usr/bin/rabbitmqadmin", line 761, in add_to_row
+    row[column_ix[col]] = maybe_utf8(val)
+  File "/usr/bin/rabbitmqadmin", line 431, in maybe_utf8
+    return s.encode('utf-8')
+AttributeError: 'float' object has no attribute 'encode'
+maximum of the shovels is: 100008
+
+
+

While it is runnig one can run flow_check.sh at any time:

+
NB retries for sr_subscribe t_f30 0
+NB retries for sr_sender 18
+
+      1 /home/peter/.cache/sarra/log/sr_cpost_veille_f34_0001.log [ERROR] sr_cpost rename: /home/peter/sarra_devdocroot/cfr/observations/xml/AB/today/today_ab_20180210_e.xml cannot stat.
+      1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f14_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan00' in vhost '/'
+      1 /home/peter/.cache/sarra/log/sr_cpump_xvan_f15_0001.log [ERROR] binding failed: server channel error 404h, message: NOT_FOUND - no exchange 'xcvan01' in vhost '/'
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0002.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CA/CWAO/09/CACN00_CWAO_100857__WDK_10905. Type: <class 'urllib.error.URLError'>, Value: <urlopen error [Errno 110] Connection timed out>
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Failed to reach server. Reason: [Errno 110] Connection timed out
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/SA/CYMM/09/SACN61_CYMM_100900___53321. Type: <class 'urllib.error.URLError'>, Value: <urlopen error [Errno 110] Connection timed out>
+      1 /home/peter/.cache/sarra/log/sr_sarra_download_f20_0004.log [ERROR] Download failed http://dd2.weather.gc.ca//bulletins/alphanumeric/20180210/CS/CWEG/12/CSCN03_CWEG_101200___12074
+more than 10 TYPES OF ERRORS found... for the rest, have a look at /home/peter/src/sarracenia/test/flow_check_errors_logged.txt for details
+
+test  1 success: shovels t_dd1_f00 (100008) and t_dd2_f00 (100008) should have about the same number of items read
+test  2 success: sarra tsarra (100008) should be reading about half as many items as (both) winnows (200016)
+test  3 success: tsarra (100008) and sub t_f30 (99953) should have about the same number of items
+test  4 success: max shovel (100008) and subscriber t_f30 (99953) should have about the same number of items
+test  5 success: count of truncated headers (100008) and subscribed messages (100008) should have about the same number of items
+test  6 success: count of downloads by subscribe t_f30 (99953) and messages received (100008) should be about the same
+test  7 success: same downloads by subscribe t_f30 (199906) and files posted (add+remove) by sr3_watch (199620) should be about the same
+test  8 success: posted by watch(199620) and subscribed cp_f60 (99966) should be about half as many
+test  9 success: posted by watch(199620) and sent by sr_sender (199549) should be about the same
+test 10 success: 0 messages received that we don't know what happenned.
+test 11 success: sarra tsarra (100008) and good audit 99754 should be the same.
+test 12 success: poll test1_f62 94865 and subscribe q_f71 99935 run together. Should have equal results.
+test 13 success: post test2_f61 99731 and subscribe r_ftp_f70 99939 run together. Should be about the same.
+test 14 success: posts test2_f61 99731 and shim_f63 110795 Should be the same.
+test 15 success: cpump both pelles (c shovel) should receive about the same number of messages (160737) (160735)
+test 16 success: cdnld_f21 subscribe downloaded (50113) the same number of files that was published by both van_14 and van_15 (50221)
+test 17 success: veille_f34 should post twice as many files (100205) as subscribe cdnld_f21 downloaded (50113)
+test 18 success: veille_f34 should post twice as many files (100205) as subscribe cfile_f44 downloaded (49985)
+test 19 success: Overall 18 of 18 passed (sample size: 100008) !
+
+blacklab%
+
+
+

Ce test a été lancé en fin de journée, car il prend plusieurs heures, et les résultats examinés le lendemain matin.

+
+
+

Échantillon à volume élevé

+

Essayer le test de flux avec un volume plus élevé de messages de notification (c’est-à-dire 100 000) +est un pas de plus vers l’objectif d’avoir un test de flux exécuté en continu. Ceci est motivé par +nos objectifs de test.

+
+

Limitation

+

Ubuntu a une limitation qui surpasse les montres inotify et que nous avons rencontrée dans +#204 . Nous pouvons surmonter cela en définissant la variable sysctl associée. +Tout d’abord, vérifiez quelle est la limite de votre système:

+
$ sysctl fs.inotify.max_user_watches
+fs.inotify.max_user_watches = 8196
+
+
+

Si la limite est trop basse (c.-à-d. 8196), changez-la à un niveau plus approprié pour l’essai de débit:

+
$ sudo sysctl fs.inotify.max_user_watches=524288
+
+
+

Pour rendre cette modification permanente, ajoutez cette ligne à /etc/sysctl.conf:

+
fs.inotify.max_user_watches=524288
+
+
+

Puis excuter sysctl -p Et le système devrait maintenant prendre en charge un volume élevé d’événements iNotify.

+
+
+

Flow Test Coincé

+

Parfois, les tests de flux (en particulier pour les grands nombres) sont bloqués en raison de problèmes avec +le flux de données (où plusieurs fichiers obtiennent le même nom) et donc les versions antérieures suppriment +les versions ultérieures et les nouvelles tentatives échouent toujours. Finalement, nous réussirons à nettoyer +le flux dd.weather.gc.ca, mais pour l’instant, il arrive parfois qu’un flow_check reste bloqué à “Retrying”. +Le test a exécuté tous les messages de notification requis et est à une phase de vidange des tentatives, +mais continue de réessayer pour toujours avec un nombre variable d’éléments qui ne tombe jamais à zéro.

+

Pour récupérer de cet état sans rejeter les résultats d’un test long, procédez comme suit:

+
^C to interrupt the flow_check.sh 100000
+blacklab% sr3 stop
+blacklab% cd ~/.cache/sarra
+blacklab% ls */*/*retry*
+shovel/pclean_f90/sr_shovel_pclean_f90_0001.retry        shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry        subscribe/t_f30/sr_subscribe_t_f30_0002.retry.new
+shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry        shovel/pclean_f92/sr_shovel_pclean_f92_0001.retry.state
+shovel/pclean_f91/sr_shovel_pclean_f91_0001.retry.state  subscribe/q_f71/sr_subscribe_q_f71_0004.retry.new
+blacklab% rm */*/*retry*
+blacklab% sr3 start
+blacklab%
+blacklab%  ./flow_check.sh 100000
+Sufficient!
+stopping shovels and waiting...
+2018-04-07 10:50:16,167 [INFO] sr_shovel t_dd2_f00 0001 stopped
+2018-04-07 10:50:16,177 [INFO] sr_shovel t_dd1_f00 0001 stopped
+2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: report option not
+implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: instances option not implemented, ignored.
+2018-04-07 14:50:16,235 [INFO] info: report option not
+implemented, ignored.
+running instance for config pelle_dd1_f04 (pid 12435) stopped.
+running instance for config pelle_dd2_f05 (pid 12428) stopped.
+maximum of the shovels is: 100075
+
+
+blacklab% ./flow_check.sh
+
+                 | dd.weather routing |
+test  1 success: sr_shovel (100075) t_dd1 should have the same number
+of items as t_dd2 (100068)
+test  2 success: sr_winnow (200143) should have the sum of the number
+of items of shovels (200143)
+test  3 success: sr_sarra (98075) should have the same number of items
+as winnows'post (100077)
+test  4 success: sr_subscribe (98068) should have the same number of
+items as sarra (98075)
+                 | watch      routing |
+test  5 success: sr3_watch (397354) should be 4 times subscribe t_f30 (98068)
+test  6 success: sr_sender (392737) should have about the same number
+of items as sr3_watch (397354)
+test  7 success: sr_subscribe u_sftp_f60 (361172) should have the same
+number of items as sr_sender (392737)
+test  8 success: sr_subscribe cp_f61 (361172) should have the same
+number of items as sr_sender (392737)
+                 | poll       routing |
+test  9 success: sr_poll test1_f62 (195408) should have half the same
+number of items of sr_sender(196368)
+test 10 success: sr_subscribe q_f71 (195406) should have about the
+same number of items as sr_poll test1_f62(195408)
+                 | flow_post  routing |
+test 11 success: sr3_post test2_f61 (193541) should have half the same
+number of items of sr_sender(196368)
+test 12 success: sr_subscribe ftp_f70 (193541) should have about the
+same number of items as sr3_post test2_f61(193541)
+test 13 success: sr3_post test2_f61 (193541) should have about the same
+number of items as shim_f63 195055
+                 | py infos   routing |
+test 14 success: sr_shovel pclean_f90 (97019) should have the same
+number of watched items winnows'post (100077)
+test 15 success: sr_shovel pclean_f92 (94537}) should have the same
+number of removed items winnows'post (100077)
+test 16 success: 0 messages received that we don't know what happenned.
+test 17 success: count of truncated headers (98075) and subscribed
+messages (98075) should have about the same number of items
+                 | C          routing |
+test 18 success: cpump both pelles (c shovel) should receive about the
+same number of messages (161365) (161365)
+test 19 success: cdnld_f21 subscribe downloaded (47950) the same
+number of files that was published by both van_14 and van_15 (47950)
+test 20 success: veille_f34 should post twice as many files (95846) as
+subscribe cdnld_f21 downloaded (47950)
+test 21 success: veille_f34 should post twice as many files (95846) as
+subscribe cfile_f44 downloaded (47896)
+test 22 success: Overall 21 of 21 passed (sample size: 100077) !
+
+NB retries for sr_subscribe t_f30 0
+NB retries for sr_sender 36
+
+
+

Donc, dans ce cas, les résultats sont toujours bons en dépit de ne pas être tout à fait +capable de résilier. S’il y avait un problème important, le cumul +l’indiquerait.

+
+
+
+

Flow tests avec MQTT

+

Les tests de flux peuvent être exécutés lorsque certains composants utilisent le protocole MQTT, au lieu d’AMQP.

+

FIXME: étapes manquantes, plus de clarté requise.

+
    +
  • Le courtier MQTT est installé

  • +
  • Les utilisateurs de BunnyMaster Tsource, Tfeed, Tsub sont définis et ont reçu des mots de passe (dépendants du courtier.)

  • +
  • Pour chaque utilisateur : une ligne d’URL mqtt://user:pw@brokerhost est ajoutée à ~/.config/sr3/credentials.conf

  • +
  • modifier la variable MQP dans ~/.config/sr3/default.conf, MQP est utilisée par les tests de flux.

  • +
+

La plupart des composants utiliseront MQTT au lieu d’amqp et peuvent être exécutés normalement.

+
+
+
+

Commits vers la branche principale

+

A part les fautes de frappe, les corrections de langue dans la documentation et l’incrémentation +de la version, les développeurs ne sont pas censés s’engager sur Main (branche principale). Tous les travaux +se produisent sur les branches de développement, et tous les tests doivent réussir avant +de considerer d’affecter Main. Une fois le développement de la branche terminé, +ou qu’une unité de travaux en cours est jugée utile de fusionner avec la branche principale, on +doit résumer les modifications de la branche pour le journal des modifications de Debian, +demande sur GitHub.

+
git checkout issueXXX  # v02_issueXXX  pour du travaille sur l´ancienne version.
+dch # résumer les changement dans changelog.
+vi doc/UPGRADING.rst # rarement en cas de changement visible dont l´usager doit prendre connaissance.
+vi doc/fr/UPGRADING.rst # bon... ceci est visible aux usagers, donc...
+git commit -a
+git push
+# déclencher un *pull request* chez github.com
+
+
+

Un deuxième développeur examinera la demande d’extraction et le réviseur décidera si +la fusion est appropriée. On s’attend à ce que le développeur examine chaque validation, et +la comprenne dans une certaine mesure.

+

Le github Actions examine les pull requests et effectue des tests de flux sur celles-ci. +Si les tests réussissent, alors c’est un bon indicateur qualitatif, mais les tests sont un peu +fragile pour le moment, donc s’ils échouent, il serait idéal que le réviseur exécute +les tests dans leur propre environnement de développement. S’il passe dans l’environnement du développeur local +on peut approuver une fusion malgré les plaintes de Github Actions.

+
+
+

Branches Principales

+

Il y a une longue discussion sur Quelle version est stable +La configuration actuelle est qu’il y a quatre branches principales:

+
    +
  • La branche principale est la version finale de SR3, fusionnant à partir de development. Utilisé pour créer des +packages SR3 dans le référentiel MetPX.

  • +
  • development … La version 3 La branche de travail en cours (WIP) est une prochaine version de Sarracenia +en développement. La branche development est utilisée pour créer des packages sr3 pour le +Quotidien +et les référentiels Pre-Release +sur launchpad.net.

  • +
  • Les branches de issue à fusionner avec development, devraient s’appeler v3_issueXXX

  • +
  • v2_dev … La branche d’intégration pour la maintenance v2 utilisée avant la promotion à v2_stable.

  • +
  • v2_stable … Généralement, cette branche obtient du code via des fusions à partir de v2_dev, après +que la pré-version a été testée sur autant de systèmes que possible. Utilisé pour construire des +paquets sur la version stable: MetPX

  • +
  • les branches issues à fusionner avec stable doivent être appelées issueXXX.

  • +
+
+
+

Dépots de pacquets Ubuntu: Launchpad.net

+

Pour les systèmes d’exploitation Ubuntu, le site launchpad.net est le meilleur moyen de fournir des packages +entièrement intégrés (construit par rapport aux niveaux de correctifs actuels de toutes les dépendances +(composants logiciels sur lesquels Sarracenia s’appuie) pour fournir toutes les fonctionnalités.)) Idéalement, +lors de l’exécution d’un serveur, on devrait utiliser l’un des référentiels, et permettre des correctifs +automatisés pour les mettre à niveau selon les besoins.

+

Référentiels:

+ +

Pour plus de détails, voir Quelle version est stable

+
+

Local Python

+

Utilisation d’une version non packagée :

+
+

Note

+

python3 setup.py build +python3 setup.py install

+
+
+
+

Windows

+

Installez winpython à partir de github.io version 3.4 ou supérieure. +Utilisez ensuite pip pour installer à partir de PyPI.

+
+
+
+

Conventions

+

Vous trouverez ci-dessous quelques pratiques de codage destinées à guider les développeurs lorsqu’il +contribuent à Sarracenia. Ce ne sont pas des règles strictes, juste des conseils.

+
+

Quand signaler

+

sr_report(7) Les messages de notification doivent être émis pour indiquer la disposition finale des +données elles-mêmes, et non des notifications ou des messages de rapport (ne signalez pas les messages +de rapport, cela devient une boucle infinie!) Pour le débogage et d’autres informations, le fichier +journal local est utilisé. Par exemple, sr_shovel n’émet aucun message sr_report(7), car aucune +donnée n’est transférée, seulement des messages.

+
+
+
+

Àjout d´un Dépendence

+

La gestion des dépendances est un sujet compliqué, car python a de nombreuses méthodes d’installation +différentes dans des environnements disparates, et Sarracenia est multi-plateforme. La pratique standard +de python pour les dépendances consiste à faire les nécessaires en les listant dans requirements.txt +ou setup.py, et demandez à tous les utilisateurs de les installer. Dans la plupart des applications +python, si une dépendance est manquante, elle se bloque simplement avec un message d’échec d’importation +de quelques sortes.

+

Dans Sr3, nous avons constaté qu’il existe de nombreux environnements différents déployés dans lesquels +les dépendances peuvent être plus gênantes qu’elles n’en valent la peine, donc chacune des dépendances +dans setup.py est également traité dans sarracenia/featuredetection, et le code de détection de +fonctionnalité permet à l’application de continuer à travailler, juste sans la fonctionnalité fournie +par le module manquant. C’est ce qu’on appelle la dégradation ou mode dégradé. L’idée étant +d’aider l’utilisateur à faire le maximum, dans l’environnement dont il dispose, tout en leur disant +ce qui manque, et ce qu’il faudrait idéalement ajouter.

+

pleine discussion (en anglais seulement):

+

Managing Dependencies (Discussion)

+

version courte:

+

En plus de requirements.dev/setup.py, si vous devez ajouter une nouvelle bibliothèque qui ne fait pas partie de +piles incluses, généralement fournies par un package os ou pip séparé, alors vous voulez +prévoir que sr3 fonctionne toujours dans le cas où le package n’est pas disponible (bien que +sans la fonction que vous ajoutez) et pour ajouter un support pour expliquer ce qui manque en utilisant +le module sarracenia/featuredetection.py.

+

Dans ce module se trouve une structure de données features, où vous ajoutez une entrée expliquant +l’importation nécessaire et les fonctionnalités qu’il apporte à Sr3. Vous ajoutez également les +protections if feature[‘x’][‘present’] dans le code où vous utilisez la fonctionnalité, afin de +permettre au code de se dégrader avec élégance.

+

Si la dépendance est ajoutée dans un plugin, alors il y a aussi une méthode pour celle décrite ici :

+

`Guide de Programmeur<../Explication/SarraPluginDev.html#ajout-de-dépendance-python-dans-les-callbacks>`_

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Contribution/Id\303\251eDeBase.html" "b/fr/Contribution/Id\303\251eDeBase.html" new file mode 100644 index 000000000..3500463cf --- /dev/null +++ "b/fr/Contribution/Id\303\251eDeBase.html" @@ -0,0 +1,324 @@ + + + + + + + Idée de Base — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Approved-Draft1-20150608

+
+

Idée de Base

+

MetPX-Sarracenia est un moteur de duplication ou de distribution de données qui exploite des technologies +standard existantes (serveurs sftp et Web et courtiers AMQP) pour obtenir des livraison de messages en temps réel +et une transparence de bout en bout dans les transferts de fichiers. Alors qu’à Sundew, chaque +pump est une configuration autonome qui transforme les données de manière complexe, dans +sarracenia, les sources de données établissent une structure qui est réalisée à travers n’importe quel +nombre de pompes intermédiaires jusqu’à ce qu’elles arrivent chez un client. Le consommateur peut +fournir un accusé de réception explicite qui se propage à travers le réseau jusqu’a une +source.

+

Alors que le pompage traditionnel des fichiers est une affaire de point à point où la connaissance n’est que +entre chaque segment, dans Sarracenia, l’information circule de bout en bout dans les deux sens. +À la base, sarracenia expose un arbre de dossiers accessibles sur le Web (WAF), en utilisant +tout serveur HTTP standard (testé avec apache). Les applications météo sont douces en temps réel, +où les données doivent être livrées le plus rapidement possible au tronçon suivant, et +les minutes, peut-être les secondes, comptent. Les technologies standard de web push, ATOM, RSS, etc… +sont en fait des technologies d’interrogation qui, lorsqu’elles sont utilisées dans des +applications à faible latence, consomment beaucoup de bande passante et de surcharge. +Pour exactement ces raisons, ces normes stipulent un intervalle d’interrogation de minimum cinq minutes. +La mise en fil d’attente avancée des messages (AMQP) apporte de véritables notifications push +et rend l’envoi en temps réel beaucoup plus efficace.

+../../_images/e-ddsr-components.jpg +

Les sources de données annoncent leurs produits, les systèmes de pompage extraient les données sur leurs +arbres WAF, puis annoncent leurs arbres pour les clients en aval. Lorsque les clients +téléchargent des données, ils peuvent écrire un message de journal sur le serveur. Les serveurs sont configurés +pour transférer ces messages du journal du client via les serveurs intermédiaires vers +la source. La Source peut voir l’intégralité du chemin emprunté par les données pour accéder à chaque +client. Avec les applications de pompage traditionnelles, les sources ne voient que ce qu’elles ont livré +au premier saut d’une chaîne. Au-delà de ce premier saut, le routage est opaque et le traçage +du chemin des données nécessite l’assistance des administrateurs de chaque système intervenant. +Avec l’envoi de fichiers journaux de Sarracenia, le réseau de pompage est totalement transparent +aux sources. Avec des journaux de bout en bout, des diagnostics sont grandement simplifiés pour tout le monde.

+

Pour les fichiers volumineux / hautes performances, les fichiers sont segmentés lors de l’ingestion +s’ils sont suffisamment grand pour que cela en vaille la peine. Chaque fichier peut traverser le +réseau de pompes indépendamment, et le remontage n’est nécessaire qu’aux points d’extrémité. +Un fichier de taille suffisante annoncera sa disponibilité de plusieurs segments pour le transfert, +et plusieurs threads ou de nœuds de transfert ramassera des segments et les transférera. +Plus il y a de segments disponibles, plus le parallélisme du transfert. Sarracenia gère le +parallélisme et l’utilisation du réseau sans intervention explicite de l’utilisateur. Comme les +pompes intermédiaires ne stockent pas et ne transferent pas de fichiers entiers, la taille maximale +du fichier pouvant traverser le réseau est maximisée.

+

Ces concepts ci-dessous ne sont pas en ordre (encore?) peut-être que nous le ferons plus tard. +Pas sûr des priorités, juste le nombre pour pouvoir s’y référer. +Ils sont destinés à guider (réfléchir?) des décisions de conception / mise en œuvre:

+

Pour chaque objectif/considération/conseil ci-dessous, voyez s’ils ont du sens, +et semblent utiles. Nous devrions nous débarrasser de tout ce qui n’est pas utile.

+
    +
  1. La pompe est, ou n’importe quel nombre de pompes sont, transparentes. +En d’autres termes: +La source est en charge des données qu’elles fournissent.

    +

    La source détermine la distribution (étendue et autorisations) +La source peut obtenir n’importe quelle information sur elle-même:

    +
    - quand le statut change:  start,stop,drop.
    +- quand les messages de notifications sont acceptés.
    +- lorsque les données sont extraites par un consommateur (scope layer ou end point.)
    +
    +
    +
  2. +
  3. Les courtiers AMQP ne transfèrent aucune donnée d’utilisateur, mais uniquement des métadonnées.

    +

    raisonnement: +doivent garder les messages de notification petits afin que le taux de transfert soit élevé. +De grands messages de notification gommageront les travaux. Les autorisations deviennent également intéressantes. +se retrouvent avec un seuil de “taille maximale” et mettent en œuvre deux méthodes pour tout.

    +
  4. +
  5. Les modifications de configuration doivent se propager et ne peuvent pas être uniques à un hôte +vous ne devriez pas avoir à faire dsh, ou px-push. +Ce genre de gestion est intégré. le bus de messages est là pour cela. +peut utiliser ‘scope’ pour que les commandes se propagent à travers plusieurs clusters.

  6. +
  7. Le journal est une donnée.

    +

    Il ne suffit pas que justice soit faite. Justice doit être perçue comme étant faite.

    +

    Il ne suffit pas que les données soient fournies. Cette livraison doit être enregistrée, +et ce journal doit être renvoyé à la source. Alors que nous voulons fournir +suffisamment d’informations pour les sources de données, nous ne voulons pas noyer le réseau +dans les métadonnées. Les journaux des composants locaux auront beaucoup plus d’informations, +les messages de journal traversant le réseau jusqu’à la source sont des “dispositions finales” +chaque fois qu’une opération est terminée ou finalement abandonnée.

    +
  8. +
  9. Il s’agit d’un outil de distribution de données, pas d’un réplicateur d’arborescence de fichiers.

    +
      +
    • nous n’avons pas besoin de savoir ce que linux uid/gid possédait à l’origine.

    • +
    • nous ne nous soucions pas de savoir quand il a été modifié.

    • +
    • nous ne nous soucions pas de ses bits d’autorisation d’origine.

    • +
    • nous ne nous soucions pas du ACL qu’il a (ils ne sont pas pertinents sur la destination.)

    • +
    • nous ne nous soucions pas des attributs étendus. (portabilité, win,mac,lin,netapp?)

    • +
    +

    encore douteux à propos de celui-ci. Est-ce que cela aide?

    +
  10. +
  11. Ne pas s’inquiéter de la performance dans la phase 1 +- les performances sont rendues possibles par l’évolutivité de la conception

    +
    -- la segmentation/réassemblage fournit le multi-threading.
    +- la segmentation signifie un transfert de fichiers plus volumineux avec un plus grand parallélisme.
    +   ajoute plusieurs flux lorsque cela en vaut la peine, utilise un seul flux
    +   quand cela a du sens.
    +-- la validation permet de limiter la bande passante de la source.
    +
    +
    +
      +
    • il faut d’abord prouver que toutes les pièces mobiles fonctionnent ensemble.

    • +
    • beaucoup plus tard, on peut revenir pour voir comment faire en sorte que chaque moteur de transfert aille plus vite.

    • +
    +
  12. +
  13. Ce n’est pas une application web, ce n’est pas un serveur FTP.

    +

    Cette application utilise HTTP comme l’un des protocoles de transport, c’est tout. +Il n’essaie pas d’être un site Web, pas plus qu’il n’essaie d’être un serveur sftp.

    +
  14. +
  15. Une gestion commune n’est pas nécessaire, il suffit de passer des journaux.

    +

    Différents groupes peuvent gérer différentes pompes. +lorsque nous interconnectons des pompes, elles deviennent une source pour nous. +les messages de journal sont routés vers les sources de données, de sorte qu’elles obtiennent nos journaux sur leur +données. (la sécurité peut avoir quelque chose à dire à ce sujet.)

    +
  16. +
  17. Il doit fonctionner n’importe où. +ubuntu,centos – primaire. +mais windows aussi.

    +

    Nous essayons de faire une pompe que d’autres peuvent facilement adopter. +Cela signifie qu’ils peuvent installer et démarrer.

    +
    +
    Il doit être facile à configurer, à la fois pour le client et le serveur.

    (cet aspect est traité dans l’emballage)

    +
    +
    +
  18. +
  19. l’application n’a pas besoin de rechercher une fiabilité absolue.

  20. +
+
+

La défaillance de nœud est rare dans un environnement de centre de données. +Bien travailler dans le cas normal est la priorité. +s’il se brise, l’information n’est jamais perdue. +Dans le pire des cas, il suffit de re-poster, et le système renverra les pièces manquantes +à travers les nœuds qui restent.

+

Il peut y avoir des diagnostics pour déterminer quels fichiers sont “en vol” +lorsqu’un nœud donné tombe en panne (minuteries deadman). Mais pas sûr que des acks multiples +avec des garanties en cas de défaillance du nœud sont nécessaires. +aller plus vite et être plus simple est probablement plus fiable dans la pratique.

+

il ne s’agit pas d’une base de données, mais d’un moteur de transfert.

+
+
    +
  1. Les bulletins deviennent moins courants, les fichiers sont plus volumineux… Pas de fichier trop volumineux.

  2. +
+
+

les anciennes applications sont utilisées pour de minuscules fichiers (des millions d’entre eux) dans EC / MSC. +mais même dans EC, les fichiers deviennent de plus en plus volumineux et vont probablement beaucoup croître. +Les données des capteurs satellitaires sont maintenant très critiques, et elles sont beaucoup plus volumineuses. +Un avertissement météorologique traditionnel au format WMO était limité à 15 Koctets (limité par des données internes). +à 32 Ko maintenant) et ces tailles étaient rarement atteintes. C’était plus comme 7-12K. +un avertissement météorologique XML moderne moyen (CAP) est de 60K donc, une augmentation de cinq à huit fois. +WMO a depuis relevé la limite à 500 000 octets pour les messages WMO-GTS. et d’autres mécanismes, +tels que FTP, n’ont pas de limite fixe.

+

D’autres domaines scientifiques utilisent des fichiers très volumineux (mesurés en téraoctets). +pour les faire circuler à travers les pompes. Cela vaut la peine de penser à transporter d’énormes fichiers.

+
+
    +
  1. Le fonctionnement normal ne devrait pas nécessiter de connaissances en programmation.

  2. +
+
+

La configuration et le codage sont des activités distinctes. Il ne faut pas avoir à modifier les scripts +pour configurer les éléments standard de l’application. Le logiciel peut être beaucoup plus simple s’il +laisse simplement toutes les fonctionnalités implémentées sous forme de scripts de plug-in. +En laissant les détails locaux pour les scripts. Mais la plupart des gens ne pourront pas l’utiliser.

+

Au moins besoin de fournir toutes les fonctionnalités de base via CLI. +Les fichiers de configuration font une partie intégrante de l’interface de ligne de commande, +c’est pourquoi nous essayons de choisir avec soin là aussi. +Pour les programmeurs, la différence entre script et la config est subtile, +ce n’est pas le cas pour la plupart des autres.

+

L’écriture de scripts est seulement requise pour étendre les fonctionnalités au-delà de ce qui est standard, +pour offrir plus de souplesse. Si la flexibilité s’avère généralement utile au fil du temps, +alors, cela devrait être sorti des scripts et dans le domaine de la configuration.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/Publication.html b/fr/Contribution/Publication.html new file mode 100644 index 000000000..cd8bac0c3 --- /dev/null +++ b/fr/Contribution/Publication.html @@ -0,0 +1,691 @@ + + + + + + + Publier une Version de MetPX-Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Publier une Version de MetPX-Sarracenia

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+

Processus de Pre-Publication

+

Pour publier une nouvelle version, on commence avec une pre-release, et, après une période +de vérification, la publication d´une version stable. MetPX-Sarracenia est distribué de +différentes manières, et chacune a son propre processus de construction. Les versions +packagées sont toujours préférables aux versions uniques, car elles sont reproductibles.

+

Pour publier une pré-version, il faut :

+
    +
  • en commençant par la branche développement (pour sr3) ou v2_dev (pour v2.)

  • +
  • exécuter le processus d’assurance qualité sur tous les systèmes d’exploitation à la recherche de régressions sur les anciens systèmes basés sur 3.6.

    +
    +
      +
    • github exécute des tests de flux pour Ubuntu 20.04 et 22.04, examinez ces résultats.

    • +
    • github exécute des tests unitaires (ne fonctionne que sur les versions les plus récentes de Python.), examinez ces résultats.

    • +
    • trouver le serveur Ubuntu 18.04. créez un package local, exécutez des tests de flux.

    • +
    • trouver le serveur Redhat 8. package de construction : python3 setup.py bdist_rpm, exécutez des tests de flux

    • +
    • recherchez le serveur Redhat 9, construisez le package : python3 -m build –no-isolation. exécuter des tests de flux

    • +
    +
    +
  • +
  • examinez Debian/changelog et mettez-le à jour en fonction de toutes les fusions (merge) vers la branche depuis la version précédente.

  • +
  • Définissez la balise (tag) de pré-version.

  • +
  • commettre ce qui précède.

  • +
  • pypi.org

    +
    +
      +
    • pour assurer la compatibilité avec python3.6, mettre à jour une branche python3.6 (pour redhat 8 et/ou ubuntu 18.)

    • +
    • utilisez la branche python3.6 pour publier sur pypi (car la compatibilité ascendante fonctionne, mais pas vers le bas.)

    • +
    • téléchargez la pré-version pour que l’installation avec pip réussisse.

    • +
    +
    +
  • +
  • launchpad.org :

    +
    +
    +
    +
  • +
  • créer des packages RedHat.

    +
    +
      +
    • trouver le serveur Redhat 8. package de construction : python3 setup.py bdist_rpm

    • +
    • trouver le serveur Redhat 9, package de construction : python3 -m build –no-isolation

    • +
    +
    +
  • +
  • sur github : rédiger une version.

    +
    +
      +
    • créez des notes de version lorsque vous y êtes invité.

    • +
    • copiez les instructions d’installation d’une version précédente (principalement pour Ubuntu.)

    • +
    • attacher: +- roue construite sur python3.6 sur Ubuntu 18 (téléchargée sur pypi.org) +- Windows binaire. +- des redhat 8 et 9 tours étiquetés comme tels.

    • +
    +
    +
  • +
  • encourager les tests de pré-version, attendre un certain temps pour les bloqueurs, le cas échéant.

  • +
+
+
+

Processus de Publication de Version Stable

+

Une version stable est simplement une version préliminaire qui a été +re-étiqueté comme stable après une certaine période d’attente pour les problèmes +se lever. Puisque tous les tests ont été effectués pour la pré-version, +la version stable ne nécessite aucun test explicite.

+
    +
  • fusionner de la pre-release à la version stable : :

    +

    git checkout stable +git merge pre-release +# il y aura des conlits à résoudre pour les fichier: debian/changelog and sarracenia/_version.py +# Pour le changelog: +# - joindre les changements pour tous les rcX dans une seule block pour la version stable. +# - assurer que la version en haut du block est juste et étiqueté: unstable +# - editer la signature pour avoir le bon responsable et date pour le relache. +# pour sarracenia/_version.py +# - ajuster pour qu´il contienne la bonne version. +git tag -a v3.xx.yy -m “v3.xx.yy” +git push origin v3.xx.yy

    +
  • +
  • fusionner de la pre-release_py36 à stable_36:

    +

    git checkout stable_py36 +git merge pre-release_py36 +# les mêmes ajustements que pour la version stable. +git tag -a o3.xx.yy -m “o3.xx.yy” +git push origin v3.xx.yy

    +
  • +
  • sur Launchpad.net

    + +
  • +
  • sur re +* go on redhat 8, build rpm:

    +

    git checkout stable_py36 +python3 setup.py bdist_rpm

    +
  • +
  • go on redhat 9, build rpm:

    +

    git checkout stable_py36 +rpmbuild –build-in-place -bb metpx-sr3.spec

    +
  • +
  • Sur github.com, Create a Release from a tag

    +
      +
    • copie/coller procédure d´installation d´un vieux relâche.

    • +
    • télécharger en pièce-jointe: un .whl

    • +
    • télécharger en pièce-jointe: redhat 8 rpm

    • +
    • télécharger en pièce-jointe: redhat 9 rpm

    • +
    • télécharger en pièce-jointe: windows.exe

    • +
    +
  • +
+
+
+

Detailles

+
+

Assurance de Qualité

+

Le processus d’assurance qualité (AQ) se déroule principalement dans la branche développement. +avant d’accepter une contribution, et sauf exceptions connues,

+
    +
  • Les tests d’assurance qualité déclenchés automatiquement par les demandes de pull vers +la branche de développement devraient tous réussir.

    +
    +

    (Toutes les actions github associées.) +tests : static, no_mirror, flakey_broker, restart_server,dynamic_flow sont inclus dans “flow.yml”

    +
    +
  • +
  • +
    Créez une machine virtuelle Ubuntu 18.04 et exécutez les tests de flux pour vous assurer qu’elle fonctionne.

    (méthode d’installation : clonage depuis le développement sur github.) +tests : statique, no_mirror, flakey_broker, restart_server, Dynamic_flow

    +
    +
    +
  • +
  • +
    Créez une machine virtuelle Redhat 8 et exécutez le test de flux pour vous assurer qu’il fonctionne.

    (méthode d’installation : clonage depuis le développement sur github.) +tests : statique, no_mirror, flakey_broker, restart_server, Dynamic_flow

    +
    +
    +
  • +
  • Créez une machine virtuelle Redhat 9 et exécutez le test de flux pour vous assurer qu’il fonctionne.

  • +
  • construire un exécutable Windows… tester ?

  • +
+

Pour une discussion approfondie, voir : https://github.com/MetPX/sarracenia/issues/139

+

Une fois les étapes ci-dessus terminées, le processus de pré-version peut continuer.

+
+
+

Schéma de contrôle de version

+

Chaque version sera versionnée en tant que <protocol version>.<YY>.<MM> <segment>

+

Où:

+
    +
  • version du protocole est la version du message. Dans les messages de notification Sarra, +ils sont tous préfixés par v02 (pour le moment).

  • +
  • YY est les deux derniers chiffres de l’année de la sortie initiale de la série.

  • +
  • MM est un numéro de mois à DEUX chiffres, c’est-à-dire pour avril: 04.

  • +
  • segment est ce qui serait utilisé dans une série. +De pep0440: +X.YaN # Version Alpha +X.YbN # Version Beta +X.YrcN # Version Candidate +X.Y # Version Final +X.ypN #ack! Version corrigé.

  • +
+

Actuellement, 3.00 est toujours stabilisé, de sorte que la convention année/mois n’est pas appliquée. +Les versions sont actuellement 3.00.iibj où:

+
+
    +
  • ii – nombre incrémentiel de versions préliminaires de 3.00

  • +
  • j – incrément bêta.

  • +
+
+

À un moment donné, 3.00 sera complet et suffisamment solide pour que nous +reprenions la convention année/mois, espérons-le 3.24.

+

Les versions finales n’ont pas de suffixe et sont considérées comme stables. +Stable devrait recevoir des corrections de bugs si nécessaire de temps en temps.

+
+
+

Définir la version

+

Ceci est fait pour démarrer le développement d’une version. D´habitude, on fais cela immédiatement +après que la version précedente a été relachée.

+
    +
  • git checkout development

  • +
  • Modifier sarracenia/_version.py manuellement et définissez le numéro de version.

  • +
  • Modifier CHANGES.rst pour ajouter une section pour la version.

  • +
  • Exécuter dch pour démarrer le journal des modifications de la version actuelle.

    +
      +
    • assurer que UNRELEASED soit l’étiquette de status au lieu de unstable (peut-être automatiquement faite par dch)

    • +
    +
  • +
  • git commit -a

  • +
  • git push

  • +
+
+
+

Branches Git pour la pré-publication

+

Avant la publication, assurez-vous que tous les tests d’assurance qualité de la section ci-dessus ont été réussis. +Lorsque le développement d’une version est terminé. Les événements suivants devraient se produire :

+

Un tag doit être créé pour identifier la fin du cycle :

+
git checkout development
+git tag -a v3.16.01rc1 -m "release 3.16.01rc1"
+git push
+git push origin v3.16.01rc1
+
+
+

Une fois la balise (tag) dans la branche de développement, promouvez-la en stable :

+
git checkout pre-release
+git merge development
+git push
+
+
+

Une fois stable est mis à jour sur github, les images du docker seront automatiquement mises à jour, mais +il faut ensuite mettre à jour les différentes méthodes de distribution : PyPI, et Launchpad

+

Une fois la génération du package terminée, il faut « Définir la version »_ +en développement jusqu’au prochain incrément logique pour garantir qu’il n’y ait aucun développement ultérieur +se produit et est identifié comme la version publiée.

+
+
+

Configurer une branche compatible Python3.6

+

Canonical, la société derrière Ubuntu, fournit Launchpad pour permettre à des tiers de créer des applications. +packages pour leurs versions de système d’exploitation. Il s’avère que les versions les plus récentes du système d’exploitation ont des dépendances +qui ne sont pas disponibles sur les anciens. La branche de développement est donc configurée pour s’appuyer sur des versions plus récentes. +versions, mais une branche distincte doit être créée lors de la création de versions pour Ubuntu Bionic (18.04) et +focal (20.04.) La même branche peut être utilisée pour construire sur Redhat 8 (une autre distribution qui utilise Python 3.6)

+

Après Python 3.7.?, la méthode d’installation passe du setup.py obsolète à l’utilisation de pyproject.toml, +et les outils python hatch. Avant cette version, Hatchling n’est pas pris en charge, donc setup.py doit être utilisé. +Cependant, la présence de pyproject.toml trompe setup.py en lui faisant croire qu’il peut l’installer. À +pour obtenir une installation correcte il faut :

+
    +
  • supprimez pyproject.toml (car setup.py est confus.)

  • +
  • supprimer le dépôt “pybuild-plugin-prproject” de debuan

  • +
+

en détail:

+
# on ubuntu 18.04 or redhat 8 (or some other release with python 3.6 )
+
+git checkout pre-release
+git branch -D pre-release_py36
+git branch stable_py36
+git checkout stable_py36
+vi debian/control
+# remove pybuild-plugin-pyproject from the "Build-Depends"
+git rm pyproject.toml
+# remove the new-style installer to force use of setup.py
+git commit -a -m "adjust for older os"
+
+
+

Ca se peut qu´une –force soit requis à un moment donné. e.g.:

+
git push origin stable_py36 --force
+
+
+

Une fois la branche mise à jour, procedez aux instructions de Launchpad.

+
+
+

PyPi

+

Parce ce que les pacquets Python sont compatibles vers le haut, mais pas vers le bas, il faut +les créer sur Ubuntu 18.04 (la plus ancienne version de Python et du système d’exploitation +en utilisation.) afin que les installations pip fonctionnent sur la plus grande varieté +de systèmes.

+

En supposant que les informations d’identification de téléchargement pypi sont en place, le téléchargement +d’une nouvelle version était auparavant une ligne unique:

+
python3 setup.py bdist_wheel upload
+
+
+

sur des systèmes plus anciens ou plus récents:

+
python3 -m build --no-isolation
+twine upload dist/metpx_sarracenia-2.22.6-py3-none-any.whl
+
+
+

Notez que le fichier CHANGES.rst est en texte restructuré et est analysé par pypi.python.org lors du téléchargement.

+
+

Note

+

Lors du téléchargement de packages en version préliminaire (alpha, bêta ou RC), PYpi ne les sert pas aux utilisateurs par défaut. +Pour une mise à niveau transparente, les premiers testeurs doivent fournir le --pre switch à pip:

+
pip3 install --upgrade --pre metpx-sarracenia
+
+
+

À l’occasion, vous souhaiterez peut-être installer une version spécifique:

+
pip3 install --upgrade metpx-sarracenia==2.16.03a9
+
+
+

L’utilisation de setup.py par ligne de commande est déconseillée. Remplacé par build and twine.

+
+
+
+
+

Launchpad.net

+

Généralités sur l’utilisation de Launchpad.net (site pour génerer les paquets pour Ubuntu) pour MetPX-Sarracenia.

+
+

Dépôts et recettes

+

Pour les systèmes d’exploitation Ubuntu, le site launchpad.net est le meilleur moyen de fournir +des pacquets entièrement intégrés (construit avec les niveaux de correctifs actuels de toutes +les dépendances (composants logiciels sur lesquels Sarracenia s’appuie) pour fournir toutes les +fonctionnalités.)) Idéalement, lors de utilisation d’un serveur, celui-ci devrait utiliser +l’un des Dépôts, et installer les correctifs automatisés pour les mettre à niveau si nécessaire.

+

Avant chaque build d’un package, il est important de mettre à jour le miroir du dépôt git sur le tableau de bord.

+ +

Attendez que ca soit fini (quelques minutes.)

+

Dépôts :

+
    +
  • +
    Quotidien https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-daily (vivant sur dev… )

    devrait, en principe, toujours être correct, mais des régressions se produisent et tous les +tests ne sont pas effectués avant chaque contribution dans les branches de développement. +Recettes:

    +
      +
    • metpx-sr3-daily – la construction quotidienne automatisée des packages sr3 s’effectue à partir de la branche development.

    • +
    • sarracenia-daily – la construction quotidienne automatisée des packages v2 s’effectue à partir de la branche v2_dev

    • +
    +
    +
    +
  • +
  • +
    Pré-Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx-pre-release (pour les fonctionnalités les plus récentes.)

    de la branche développement. Les développeurs déclenchent manuellement les builds ici lorsque cela semble approprié (tests +code prêt à être publié.)

    +
      +
    • metpx-sr3-pre-release – créez à la demande des packages sr3 à partir de la branche de pre_release.

    • +
    • metpx-sr3-pre-release-old – build à la demande des packages sr3 à partir de la branche pre-release_py36.

    • +
    • metpx-sarracenia-pre-release – build à la demande des packages sr3 à partir de la branche v2_dev.

    • +
    +
    +
    +
  • +
  • +
    Release https://launchpad.net/~ssc-hpc-chp-spc/+archive/ubuntu/metpx (pour une stabilité maximale)

    de la branche v2_stable. Après avoir testé sur des systèmes abonnés aux pré-versions, les développeurs +fusionner de la branche v2_dev dans celle de v2_stable et déclencher manuellement une construction.

    +
      +
    • metpx-sr3 – à la demande, créez des packages sr3 à partir de la branche stable.

    • +
    • metpx-sr3-old – créez à la demande des packages sr3 à partir de la branche stable_py36.

    • +
    • sarracenia-release – sur deman, construisez les packages v2 à partir de la branche v2_stable.

    • +
    +
    +
    +
  • +
+
+
+

Launchpad

+
+

Build Automatisée

+
    +
  • Assurez-vous que le miroir de code est mis à jour en vérifiant les Détails de l’importation en vérifiant +Cette page pour Sarracenia

  • +
  • Si le code n’est pas à jour, faites Import Now , et attendez quelques minutes pendant qu’il est mis à jour.

  • +
  • Une fois le référentiel à jour, procédez à la demande de build.

  • +
  • Accédez à la recette sarracenia release

  • +
  • Accédez à la recette sr3 release

  • +
  • Cliquez sur le bouton Request Build(s) pour créer une nouvelle version.

  • +
  • pour Sarrac, suivez la procédure here

  • +
  • Les packages construits seront disponibles dans le +metpx ppa

  • +
+
+
+

Builds quotidiennes

+

Les builds quotidiennes sont configurées à l’aide de +cette recette Python +et cette recette pour C et +sont exécutés une fois par jour lorsque des modifications sont apportées au référentiel.These packages are stored in the +Ces packages sont stockés dans le metpx-daily ppa. +On peut également Request Build(s) à la demande si vous le souhaitez.

+
+
+

Processus manuel

+

Le processus de publication manuelle des packages sur Launchpad ( https://launchpad.net/~ssc-hpc-chp-spc ) +implique un ensemble d’étapes plus complexes, et donc le script pratique publish-to-launchpad.sh sera +le moyen le plus simple de le faire. Actuellement, les seules versions prises en charge sont trusty et xenial. +La commande utilisée est donc la suivante

+
publish-to-launchpad.sh sarra-v2.15.12a1 trusty xenial
+
+
+

Toutefois, les étapes ci-dessous sont un résumé de ce que fait le script :

+
    +
  • pour chaque distribution (precise, trusty, etc) mettez à jour debian/changelog pour refléter la distribution

  • +
  • Générez le package source en utilisant

    +
    debuild -S -uc -us
    +
    +
    +
  • +
  • signez les fichiers .changes et .dsc:

    +
    debsign -k<key id> <.changes file>
    +
    +
    +
  • +
  • Télécharger vers Launchpad:

    +
    dput ppa:ssc-hpc-chp-spc/metpx-<dist> <.changes file>
    +
    +
    +
  • +
+

Remarque : Les clés GPG associées au compte du tableau de bord doivent être configurées +afin de faire les deux dernières étapes.

+
+
+

Rétroportage d’une dépendance

+

Exemple:

+
backportpackage -k<key id> -s bionic -d xenial -u ppa:ssc-hpc-chp-spc/ubuntu/metpx-daily librabbitmq
+
+
+
+
+

Ubuntu 18.04

+

Pour ubuntu 18.04 (bionique), il y a quelques problèmes. La recette s’appelle: metpx-sr3-daily-bionic, et il +prend la source à partir d’une branche différente : v03_launchpad. Pour chaque version, cette branche +doit être rebasée à partir de development

+
    +
  • git checkout v03_launchpad

  • +
  • git rebase -i development

  • +
  • git push

  • +
  • import source

  • +
  • Request build from metpx-sr3-daily-bionic Recipe.

  • +
+

En quoi cette branche v03_launchpad est-elle différente ? Elle:

+
    +
  • Supprime la dépendance sur python3-paho-mqtt car la version dans le repository d´ubuntu est trop ancienne.

  • +
  • Suppression de la dépendance sur python3-dateparser, car ce paquet n’est pas disponible dans le repository d´ubuntu.

  • +
  • remplacer la cible de test dans debian/rules, parce que tester sans les dépendances échoue

    +
    override_dh_auto_test:
    +   echo "disable on 18.04... some deps must come from pip"
    +
    +
    +
  • +
+

The missing dependencies should be installed with pip3.

+
+
+

Création d’un programme d’installation Windows

+

On peut également construire un programme d’installation Windows avec cela +script. +Il doit être exécuté à partir d’un système d’exploitation Linux (de préférence Ubuntu 18) +dans le répertoire racine de git de Sarracenia.

+

déterminer la version de python:

+
fractal% python -V
+Python 3.10.12
+fractal%
+
+
+

C’est donc python 3.10. Une seule version mineure aura le package intégré nécessaire +par pynsist pour construire l’exécutable. On valide chez:

+
https://www.python.org/downloads/windows/
+
+
+

afin to confirmer que la version avec un binaire embedded pour 3.10 et le 3.10.11 +Ensuite, à partir du shell, exécutez

+
sudo apt install nsis
+pip3 install pynsist wheel
+./generate-win-installer.sh 3.10.11 2>&1 > log.txt
+
+
+

Le paquet final doit être placé dans le répertoire build/nsis.

+
+
+
+
+

github

+

Cliquez sur Releases, modifiez la release :

+
    +
  • Devrions-nous avoir des noms de sortie?

  • +
  • copier/coller des modifications de CHANGES.rst

  • +
  • copier/coller le bit d’installation à la fin d’une version précédente.

  • +
  • Construire des paquets localement ou télécharger à partir d’autres sources. +Glissez-déposez dans la version.

  • +
+

Cela nous donnera la possibilité d’avoir d’anciennes versions disponibles. +launchpad.net ne semble pas garder les anciennes versions.

+
+
+

ubuntu 18

+

Problème rencontre lors de géneration de pacque pour pypi.org sur ubuntu 18:

+
buntu@canny-tick:~/sr3$ twine upload dist/metpx_sr3-3.0.53rc2-py3-none-any.whl
+/usr/lib/python3/dist-packages/requests/__init__.py:80: RequestsDependencyWarning: urllib3 (1.26.18) or chardet (3.0.4) doesn't match a supported version!
+  RequestsDependencyWarning)
+Uploading distributions to https://upload.pypi.org/legacy/
+Uploading metpx_sr3-3.0.53rc2-py3-none-any.whl
+100%|██████████████████████████████████████████████████████████████████████████████████████████████████| 408k/408k [00:00<00:00, 120kB/s]
+HTTPError: 400 Client Error: '2.0' is not a valid metadata version. See https://packaging.python.org/specifications/core-metadata for more information. for url: https://upload.pypi.org/legacy/
+ubuntu@canny-tick:~/sr3$
+
+
+

On a générer via redhat8 à la place. Il semble que la version de twine de ubuntu 18 ne soit plus +en mésure de communiquer avec pypi.org. installation avec pip3 aura peut-être aussi regler le bobo.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/assemblage_on_part.html b/fr/Contribution/assemblage_on_part.html new file mode 100644 index 000000000..68bd4a24c --- /dev/null +++ b/fr/Contribution/assemblage_on_part.html @@ -0,0 +1,235 @@ + + + + + + + Réassemblage de fichiers — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Réassemblage de fichiers

+
+

Composants

+

sr_watch: Vous pouvez utiliser sr3_watch pour surveiller un répertoire pour les fichiers +de partition entrants (.Part) de sr_subscribe ou sr_sender, les deux ont la possibilité d’envoyer +un fichier dans des partitions. Dans le fichier de configuration pour sr3_watch les paramètres +importants à inclure sont les suivants :

+
    +
  • +
    chemin <chemin du répertoire à surveiller>
      +
    • on_part /usr/lib/python3/dist-packages/sarra/plugins/part_file_assemble.py

    • +
    • accept *.Part

    • +
    • accept_unmatch False # Fait qu’il ne fait qu’acccepter le modèle ci-dessus

    • +
    +
    +
    +
  • +
+

Part_File_Assemble (plugin): Ce plugin est un plugin on_part qui déclenche le code assembleur dans sr_file

+

sr_file: Contient le code de réassemblage… L’algorithme est décrit ci-dessous

+
+
+

Algorithme

+

Après avoir été déclenché par un fichier de pièce téléchargé :

+
+
    +
  • si le target_file n’existe pas :

    +
    +
      +
    • si le fichier de pièce téléchargé était la première partition (partie 0) :

      +
      +
        +
      • créer un nouveau target_file vide

      • +
      +
      +
    • +
    +
    +
  • +
+
+
    +
  • trouver quel numéro de partition doit être inséré ensuite (i)

  • +
+
+
    +
  • while i < Nombre total de blocs:

    +
    +
      +
    • file_insert_part()

      +
      +
        +
      • insère la part du fichier dans le fichier cible et calcule la somme de contrôle de la partie insérée

      • +
      +
      +
    • +
    • vérifier l’insertion en comparant les sommes de contrôle du fichier de partition et du bloc inséré dans le fichier

    • +
    • supprimer le fichier si vous êtes d’accord, sinon réessayer

    • +
    • déclencher on_file

    • +
    +
    +
  • +
+
+
+
+

Test

+

Créer un fichier de configuration sr3_watch selon le modèle ci-dessus. +Démarrez le processus en tapant la commande suivante : `sr_watch foreground path/to/config_file.cfg`

+

Ensuite, créez un fichier de configuration d’abonné et incluez `inplace off` afin que le fichier +soit téléchargé en plusieurs parties. +Démarrez l’abonné en tapant `sr_subscribe foreground path/to/config_file.cfg`

+

Maintenant, vous devez envoyer des messages de publication du fichier pour l’abonné, +par exemple: `./sr_post.py -pb amqp://tsource@localhost/ -pbu sftp://<user>@localhost/ -p /home/<user>/test_file -px xs_tsource  --blocksize 12M`

+
+
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/deltas.html b/fr/Contribution/deltas.html new file mode 100644 index 000000000..7d967a579 --- /dev/null +++ b/fr/Contribution/deltas.html @@ -0,0 +1,359 @@ + + + + + + + Discussion sur la propagation de la modification de fichiers — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Pre-Draft

+
+

Discussion sur la propagation de la modification de fichiers

+

C’était une réflexion précoce sur la façon de gérer les mises à jour de fichiers. +Les premières versions du protocole ne concernaient que des fichiers entiers. +Lorsque les ensembles de fichiers sont suffisamment volumineux, des mises +à jour partielles deviennent très souhaitables. +En outre, lorsque la taille des fichiers individuels est suffisamment grande et lors de la traversée +des liaisons WAN, on peut obtenir un avantage pratique substantiel en envoyant des +données utilisant plusieurs flux, plutôt qu’un seul.

+

Donc, dans v02, il y a un en-tête ‘parts’ qui indique la méthode de partitionnement +utilisé pour un transfert donné, et la conclusion sur le format/protocole +est maintenant documenté dans la page de manuel sr_post(7). +Ce dossier contient les premières discussions et notes.

+
+
Algorithme utilisé (quel que soit l’outil) :
    +
  • Pour chaque ‘bloc’ (taille de bloc intéressante) il faut générer une signature.

  • +
  • Lorsqu’un abonné lit un message de notification, il inclut la signature.

  • +
  • Il compare les signatures sur le fichier qu’il a déjà, et le met à jour pour qu’il corresponde.

  • +
+
+
+

L’algorithme zsync est la bonne idée, peut peut-être l’utiliser directement.

+
+

Que se passe-t-il si chaque notification concerne un bloc, pas un fichier ?

+

Expérience de Gedanken… par messages de blocage, plutôt que par fichiers entiers ? +Que se passe-t-il si les messages que nous envoyons sont tous par bloc?

+

Pourquoi est-ce vraiment cool?

+
+
    +
  • Il fait le truc gridftp, en divisant les transferts de fichiers uniques +en flux parallèles.

  • +
  • Pour les fichiers volumineux, les ddsr peuvent avoir tout un tas de fichiers partiels, +au lieu des complets, car le transfert est divisé sur +plusieurs nœuds, pas de problème, tant que les étapes ultérieures sont souscrites +à tous les DDSR.

  • +
  • les commutateurs intermédiaires n’ont pas besoin de stocker le fichier le plus volumineux +qui peut être transférés, seulement un certain nombre des plus gros morceaux. +Élimine le problème de taille maximale du fichier.

  • +
  • Cela concerne également les fichiers qui sont écrits au fil du temps, sans attendre +jusqu’à ce qu’ils soient terminés avant d’appuyer sur envoyer.

  • +
  • pour que le client fasse un envoi multithread, il suffit de démarrer +un nombre quelconque de sr_senders écoutant leur propre échange d’entrées. +Partager l’abonnement, tout comme sr_subscribe (dd_subscribe) le fait.

  • +
  • Pour les fichiers volumineux, vous pouvez voir les rapports d’avancement reçus par les sources +confirmation de chaque couche de commutation recevant chaque morceau.

  • +
+
+

disons que nous définissons une taille de bloc de 10 Mo, et que nous vérifions ce bloc, en notant le décalage, puis +on continue?

+

v01.post donc : +blocksz sz-inblocks rest blocknum flags chksum base-url relative-path flow ….

+

Voir Journalisation.txt pour une description de ‘Flow’, User Settable, avec une valeur par défaut.

+

flags indique si le chksum est pour le nom ou le corps. (si la somme de contrôle est pour le nom, +alors impossible d’utiliser des chksums bloquants.) … indique que les sommes de contrôle +de nom ne doivent être utilisées que pour les petits fichiers.

+

Le blocksz établit le multiplicateur pour le sz et le blocknum. le reste +est le dernier bit du fichier après la dernière limite de bloc.

+

Donc, vous calculez la somme de contrôle pour chaque bloc que vous envoyez un message avec le bloc,

+

De cette façon, pour les fichiers volumineux, le transfert peut être divisé sur un grand nombre de nœuds. +Mais alors le remontage est un peu un casse-tête. Chaque nœud de DDSR aura-t-il seulement +Fichiers volumineux fractionnaires (c’est-à-dire clairsemés) ? Tant que le sr_sub est aux deux DDSR, il devrait +tout obtenir. Que se passe-t-il avec les fichiers fragmentés ?

+

https://administratosphere.wordpress.com/2008/05/23/sparse-files-what-why-and-how/

+

c’est bon… +Cela fonctionnerait, sous Linux, mais c’est un peu étrange, et causerait de la confusion dans la +pratique. D’ailleurs, comment savons-nous quand nous avons terminé?

+

— — de remontage +Que diriez-vous de ce qui suit. Lorsque sr_subscribe(dd_subscribe) écrit des fichiers, il écrit dans un fichier +suffixé .sr§1 lorsqu’il reçoit un fichier, et qu’il y a un .sr§ il vérifie la taille +du fichier, si la partie courante est contiguë, il suffit d’ajouter (via lseek & write) +les données du fichier .sr§. Sinon, il crée un .sr§ <blocknum>séparé

+
+
+

suffixe .sr§

+

Mais cela signifie qu’ils annoncent les pièces… Hmm… Les noms signifient maintenant quelque chose, +Nous utilisons le caractère de section au lieu de partie. Pour éviter cela, choisissez un nom qui +est plus inhabituel que .part quelque chose comme .sr§partnum (en utilisant utf-8, intéressant +pour voir ce que l’encodage d’URL fera à cela.) Il est bon d’utiliser des +caractères spéciaux UTF, parce que personne d’autre ne les utilise, donc peu susceptible de s’affronter.

+

Et si quelqu’un annonce un fichier .sr§? Qu’est-ce que cela signifie? Avons-nous besoin de +le détecter?

+

Ensuite, il regarde dans le répertoire pour voir si un .sr§<blocknum +1> existe, et l’ajoute +si c’est le cas, et boucle jusqu’à ce que toutes les parties contiguës soient ajoutées (les +fichiers correspondant supprimés.)

+

Remarque : N’utilisez pas l’écriture d’ajout .sr§, mais toujours lseek et write. Cela empêche +race condition de causer des ravages. S’il y a plusieurs sr_subscribe (dd_subscribes) S’il y a plusieurs sr_subscribe +(dd_subscribes) écrivant le fichier, ils écriront simplement les mêmes données plusieurs fois (dans le pire des cas).

+

Quoi qu’il en soit, lorsque vous manquez de pièces contiguës, vous vous arrêtez.

+

Si le dernier bloc contigu reçu inclut la fin du fichier, effectuez la logique d’achèvement du fichier.

+
+
+

Comment sélectionner Chunksize

+
+
    +
  • Choix de la source?

  • +
  • Nous imposons les nôtres à DDSR ?

  • +
+
+

Une valeur par défaut 10*1024*1024=10485760 octets, avec remplacement possible.

+

peut vouloir que la partie validation impose une taille de bloc minimale +sur les fichiers publiés, en plus du chemin d’accès au fichier.

+

nous fixons un minimum, disons 10 Mo, puis si c’est moins de 5% du fichier, +utilisez 5%, jusqu’à ce que nous arrivions à un morceau maximum… disons 500 Mo. +Remplacer par la taille 0 (pas de morcellement d’un long envoi.)

+
+
Quelle est la surcharge pour envoyer un bloc?
    +
  • il n’y a pas de champs de largeur fixe dans les messages, ils sont tous de longueur variable ASCII.

  • +
  • estimer qu’une notification d’âge est de 100 octets,

  • +
  • Le message de journal est peut-être de 120 octets.

  • +
+
+
pour chaque bloc transféré.
+

la notification va dans un sens, +Au moins un message de journal par étendue passe dans l’autre sens.

+
+

Si nous disons deux sauts + livraison client (un troisième saut)

+

Donc un bloc aura de l’ordre de 100+4*120 = 580 octets.

+
+
+

Il a reconnu qu’il y avait une surcharge de protocole importante sur les petits fichiers.

+

Cependant, on pourrait espérer que les frais généraux deviendront plus raisonnables pour les fichiers plus volumineux, +Mais cela est limité par la taille du bloc. +Esthétiquement, il faut choisir une taille qui éclipse la surcharge.

+
+
+

Si nous faisons des cksums par blocs, chemin à partir de v00

+

compatibilité… amélioration… +Les alertes v00.notify se résument à :

+

v01.post pourrait être:

+
+
filesz 1 0 0 …
    +
  • la taille du bloc est la longueur du fichier entier, 1 bloc est la taille

  • +
  • pas de reste.

  • +
  • 0ème bloc (le premier, zéro origine comptée)

  • +
+
+
+

Ou nous prenons la convention selon laquelle une taille de bloc de zéro signifie pas de blocage… +dans lequel la poursuite serait:

+
+
+
0 1 fichiersz 0 …
    +
  • Stocker le SZ comme le reste.

  • +
  • désactiver le blocage pour ce fichier.

  • +
+
+
+
+

S’il y a une validation sur la taille de blocage, il doit il y avoir un moyen de le gérer.

+
+
+

Digression sur ZSync

+

zsync est disponible dans les référentiels et zsync(1) est le client de téléchargement existant. +zsyncmake(1) construit les signatures, avec une taille de bloc programmable.

+

Il semble que zsync est utilisable tel quel?

+
+
Inconvénientla portabilité.
+
besoin de zsync sur Windows et Mac pour les téléchargements, la dépendance est pénible.

il existe un binaire Windows, créé une fois en 2011… Hmm… +Je ne l’ai pas vu sur Mac OS non plus… soupir…

+
+
+
+
+

Nous envoyons les signatures dans les messages de notification, plutôt que de les publier sur le site. +Si nous définissons la taille du bloc à une valeur élevé, alors pour les fichiers < 1 bloc, il n’y a pas de signature.

+

Est ce que sr_sarra doit publier la signature sur le site, pour la compatibilité avec ZSYNC ?

+

Je ne veux pas forker zsyncmake pour chaque produit… +Même si nous n’utilisons pas zsync lui-même, nous pourrions vouloir être compatibles… Alors utilisez +un format tiers et avoir un comparable. 1ère mise en œuvre ferait l’affaire +Avec un forking, la 2e version peut répliquer l’algorithme en interne.

+

Peut-être avons-nous un seuil, si le fichier est inférieur à un mégaoctet, nous envoyons simplement +le nouveau. L’intention n’est pas de répliquer des arbres sources, mais de grands ensembles de données.

+
+
    +
  • Pour la plupart des cas (lors de l’écriture d’un nouveau fichier), nous ne voulons pas de frais généraux supplémentaires.

  • +
  • La cible est les gros fichiers qui changent, pour les petits, transférer à nouveau, n’est pas un gros problème.

  • +
  • veulent minimiser la taille de la signature (tout comme les voyages avec des notifications.)

  • +
  • Définissez donc une taille de bloc à une valeur vraiment grande.

  • +
+
+

Peut-être construire le client zsync dans sr_subscribe (dd_subscribe), mais utiliser zsync make côté serveur ? +Ou lorsque le fichier est assez volumineux, le fork d’un zsync n’est pas un gros problème? Mais Mac & Win…

+
+
+

Considérations relatives au serveur/protocole

+
+
HTTP :

– utilise la fonction de plage d’octets de HTTP. +– FIXME: trouver des échantillons à partir d’autres e-mails.

+
+
en SFTP/python/paramiko…

– il y a readv( … ) qui permet de lire des sous-ensembles d’un fichier. +– la commande read dans la spécification SFTP PROTOCOL a offset comme argument standard de read

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/index.html b/fr/Contribution/index.html new file mode 100644 index 000000000..7fdba0e60 --- /dev/null +++ b/fr/Contribution/index.html @@ -0,0 +1,255 @@ + + + + + + + Contribuer à Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Contribuer à Sarracenia

+
+

Contenus:

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Contribution/mod\303\250le_de_page_man.html" "b/fr/Contribution/mod\303\250le_de_page_man.html" new file mode 100644 index 000000000..d6899d00e --- /dev/null +++ "b/fr/Contribution/mod\303\250le_de_page_man.html" @@ -0,0 +1,256 @@ + + + + + + + SR3_TITRE — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+ +
+
+ + + +
+

SR3_TITRE

+
+

sr_titre

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr_titre foreground|start|stop|restart|reload|status configfile +sr_titre cleanup|declare|setup configfile

+
+
+

DESCRIPTION

+

sr_title Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do +eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim +veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea +commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit +esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat +cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id +est laborum. sr_subscribe(7) +avec des limitations.

+
+
    +
  • ne télécharge pas de données, ne fait circuler que des messages de notification.

  • +
  • s’exécute en tant qu’instance unique (pas plusieurs instances).

  • +
  • ne supporte aucun plugin.

  • +
  • Ne prend pas en charge de VIP pour la haute disponibilité.

  • +
  • bibliothèque d’expressions régulières différente: POSIX vs python.

  • +
  • Ne prend pas en charge regex pour la commande strip (pas de regex non-greedy).

  • +
+
+

Il peut donc servir de remplaçant pour :

+
+

sr_report(1) - Traiter les messages de notification de rapport.

+

sr_shovel(8) - Traiter les messages de notification de shovel.

+

sr_winnow(8) - Traiter les messages de notification de winnow.

+
+

La commande sr_cpump prend deux arguments : une action start|stop|restart|reload|status… (auto-décrit) +suivi d’un fichier de configuration.

+

L’action foreground est utilisée lors du débogage d’une configuration, lorsque l’utilisateur souhaite +exécutez le programme et son fichier de configuration de manière interactive… L’instance foreground +n’est pas concerné par d’autres actions. L’utilisateur cesserait d’utiliser l’instance foreground +en utilisant simplement <ctrl-c> sur Linux ou en utilisant d’autres moyens pour envoyer SIGINT ou SIGTERM au processus.

+

Les actions cleanup, declare, setup peuvent être utilisé pour gérer les ressources sur +le serveur RabbitMQ. Les resources sont des files d’attente ou des échanges. declare Crée +les ressources. setup crée et effectue en en plus les liaisons des files d’attente.

+

Les actions add, remove, edit, list, enable, disable agissent +sur des configurations.

+
+
+

CONFIGURATION

+

Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium +doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore +veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim +ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia +consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque +porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, +adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore +et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis +nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid +ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea +voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem +eum fugiat quo voluptas nulla pariatur?”

+
+
+

VARIABLES D’ENVIRONMENT

+

Si la variable SR_CONFIG_EXAMPLES est définie, la directive add peut être utilisée +pour copier des exemples dans le répertoire de l’utilisateur à des fins d’utilisation et/ou de personnalisation.

+

Une entrée dans le fichier ~/.config/sarra/default.conf (créée via sr_subscribe edit default.conf ) +pourrait être utilisé pour définir la variable

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

La valeur doit être disponible à partir de la sortie d’une commande list de l’implémentation python.

+
+
+

VOIR AUSSI

+

sr3_report(7) - Format des messages de rapport.

+

sr3_report(1) - Traiter les messages de rapport.

+

sr3_post(1) - Publier des messages de notification de fichiers spécifiques.

+

sr3_post(7) - Format des messages de notification.

+

sr3_subscribe(1) - le client de téléchargement.

+

sr3_watch(1) - Le démon de surveillance du répertoire.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Contribution/probl\303\250mes_mqtt.html" "b/fr/Contribution/probl\303\250mes_mqtt.html" new file mode 100644 index 000000000..445a273b2 --- /dev/null +++ "b/fr/Contribution/probl\303\250mes_mqtt.html" @@ -0,0 +1,197 @@ + + + + + + + Notes de mise en œuvre MQTT — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Notes de mise en œuvre MQTT

+
+

v3 vs. v5

+
    +
  • La version 3 a des renvois envoyés sur une base chronométrée, toutes les quelques secondes +(peut-être jusqu’à 20 secondes.) +Si jamais vous avez un arriéré, ces retransmissions seront une tempête de trafic toujours croissant.

  • +
  • La version 3 n’a pas d’abonnements partagés, ne peut utiliser qu’un seul processus par abonnement. +L’équilibrage de charge est plus difficile.

  • +
+
+
+

Abonnements partagés

+
    +
  • une fois que vous avez rejoint un groupe, vous y êtes jusqu’à ce que la session soit morte, même si vous vous déconnectez, +Il empilera 1/n messages dans votre fil d’attente.

  • +
+
+
+

Contre-pression

+
    +
  1. Le client paho est asynchrone

  2. +
  3. La meilleure pratique consiste à avoir des handlers de on_message très légers.

  4. +
  5. Le client paho accuse réception dans la bibliothèque à peu près au moment où on_message est appelé.

  6. +
+

Si vous avez une application qui prend du retard… par exemple, c’est lent à traiter, +mais comme la réception est asynchrone, tout ce que cela signifie, c’est que vous obtiendrez une +fil d’attente de messages sur l’hôte local. Idéalement, cela permettrait au courtier de savoir que +les choses vont mal et le courtier y enverra moins de données.

+

Méthode:

+
    +
  1. v5: Recevoir le maximum … Limitez le nombre de messages en transit entre le courtier et le client. +implémenté. Fonctionne.

  2. +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Contribution/v03.html b/fr/Contribution/v03.html new file mode 100644 index 000000000..14d4a16ca --- /dev/null +++ b/fr/Contribution/v03.html @@ -0,0 +1,952 @@ + + + + + + + Refactorisation de la version 3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Refactorisation de la version 3

+
+

Résumé

+

Ce document s’adresse aux développeurs qui ont besoin de travailler à la fois avec du code v2 et +le refactor qui s’appelait à l’origine v3, mais qui s’appelait finalement sr3. +Pour les développeurs familiers avec la v2, le document peut servir un peu de carte +au code source de sr3, qui est maintenant suffisamment stabilisé pour qu’il +passe les flow_tests. Sr3 n’est peut-être pas encore vraiment utilisable, mais la direction +est bien établi et le développement ultérieur est maintenant décrit en utilisant le problème +tracker (regardez l’étiquette v3only.) Donc, ce document est maintenant essentiellement +historique. Si quelqu’un ne connaît pas la v2, ce document n’aidera pas parce que c’est +exclusivement le mappage de v2 à sr3. La lecture du reste de la documentation v03 +devrait être plus utile.

+

Objectifs abstraits de sr3:

+
    +
  • compatibilité de configuration (compatible vers le haut à partir de v2.) y compris tous les plugins.

  • +
  • prise en charge de protocoles multiples. +possibilité de mettre des urls pour mqtt, ou différentes bibliothèques amqp, peut-être d’autres.

  • +
  • représenter en interne les choses dans les messages de notification v03, avoir quelque chose qui construit +v02 pour la compatibilité, mais fonctionnent dans v03.

  • +
  • moins de code, code plus simple. +code pythonique plus lisible, élégant. +faciliter l’entretien.

  • +
+
+

objectifs d’opportunité

+
+
    +
  • ajouter des choses pour le faire fonctionner comme une API?

  • +
  • potentiellement nouvelle api de plugin pour autoriser des groupes (de messages de notification et / ou de fichiers.)

  • +
  • Terminez la rotation des journaux.

  • +
  • Supposons que python > = 3.4 supprimer l’ancienne croûte.

  • +
  • Supposons que ubuntu > = 18.04 supprimer l’ancienne croûte.

  • +
  • Supposons systèmed, supprimez l’intégration sysv.

  • +
  • avoir des options qui adoptent camelCase dans la mesure du possible.

  • +
  • entièrement asynchrone, multi-sources et récepteurs.

  • +
+
+
+
+

État du Code

+

À partir du 24/08/2021, le code sr3 réussit tous les mêmes tests de flux que la v2 +sur un ordinateur portable (sauf dynamique dans sr3 #407). Il exécute ces mêmes tests en utilisant les mêmes configurations, donc la compatibilité +objectif est atteint. Sr3 accepte les URL du courtier mqtt et un problème est créé #389 pour amqp v1. +Sr3 est utilisé pour alimenter un aliment expérimental de WMO, bien qu’avec le besoin +pour le redémarrer régulièrement ( numéro #388 )

+

Le nouveau code sr3 a 4000 lignes de moins que la v2 et inclut mqtt.py (protocole broker supplémentaire) +ainsi qu’un module implémentant une couche de compatibilité avec les plugins v2. Par exemple, +la nouvelle routine de configuration est 30 % plus courte et plus cohérente en sr3 qu’en v2. +Le code est également beaucoup plus pythonique, car l’API est beaucoup plus +naturel avec plusieurs niveaux d’API qui peuvent être appris en consultant des jupyter notebooks.

+

code v2:

+
fractal% find -name '*.py' | grep -v .pybuild | grep -v debian | grep -v plugins | xargs wc -l
+ 133 ./sr_winnow.py
+ 544 ./sr_sftp.py
+  47 ./sr_tailf.py
+ 365 ./sr_cache.py
+ 164 ./sr_xattr.py
+1136 ./sr_message.py
+  51 ./sr_checksum.py
+ 129 ./pyads.py
+ 306 ./sr_http.py
+2204 ./sr_subscribe.py
+ 403 ./sr_consumer.py
+1636 ./sr_post.py
+ 265 ./sr1.py
+  54 ./sr_log2save.py
+ 206 ./sr_sarra.py
+ 286 ./sr_rabbit.py
+ 567 ./sr_file.py
+  28 ./__init__.py
+ 107 ./sr_report.py
+  74 ./sr_watch.py
+ 126 ./sr_shovel.py
+ 505 ./sr_retry.py
+ 956 ./sr_util.py
+ 355 ./sr_sender.py
+ 368 ./sr_cfg2.py
+1119 ./sr.py
+ 753 ./sr_poll.py
+ 729 ./sr_audit.py
+ 308 ./sr_credentials.py
+ 988 ./sr_instances.py
+ 608 ./sr_amqp.py
+ 455 ./sr_ftp.py
+3062 ./sr_config.py
+  33 ./sum/checksum_s.py
+  34 ./sum/checksum_d.py
+  34 ./sum/__init__.py
+  26 ./sum/checksum_0.py
+  30 ./sum/checksum_n.py
+  29 ./sum/checksum_a.py
+19223 total
+fractal%
+
+
+

code sr3:

+
2157 ./config.py
+ 342 ./credentials.py
+ 384 ./diskqueue.py
+ 183 ./filemetadata.py
+ 768 ./flowcb/gather/file.py
+  53 ./flowcb/gather/message.py
+   7 ./flowcb/housekeeping/__init__.py
+ 130 ./flowcb/housekeeping/resources.py
+ 250 ./flowcb/__init__.py
+ 145 ./flowcb/log.py
+  24 ./flowcb/nodupe/data.py
+ 345 ./flowcb/nodupe/__init__.py
+  24 ./flowcb/nodupe/name.py
+ 454 ./flowcb/poll/__init__.py
+  14 ./flowcb/post/__init__.py
+  55 ./flowcb/post/message.py
+ 117 ./flowcb/retry.py
+ 461 ./flowcb/v2wrapper.py
+1617 ./flow/__init__.py
+  80 ./flow/poll.py
+  34 ./flow/post.py
+  18 ./flow/report.py
+  29 ./flow/sarra.py
+  27 ./flow/sender.py
+  16 ./flow/shovel.py
+  29 ./flow/subscribe.py
+  35 ./flow/watch.py
+  16 ./flow/winnow.py
+ 793 ./__init__.py
+ 226 ./instance.py
+  36 ./identity/arbitrary.py
+  93 ./identity/__init__.py
+  24 ./identity/md5.py
+  17 ./identity/random.py
+  24 ./identity/sha512.py
+  17 ./moth/amq1.py
+ 585 ./moth/amqp.py
+ 313 ./moth/__init__.py
+ 548 ./moth/mqtt.py
+  16 ./moth/pika.py
+ 135 ./pyads.py
+ 349 ./rabbitmq_admin.py
+  26 ./sr_flow.py
+  52 ./sr_post.py
+2066 ./sr.py
+  50 ./sr_tailf.py
+ 383 ./transfer/file.py
+ 514 ./transfer/ftp.py
+ 361 ./transfer/https.py
+ 437 ./transfer/__init__.py
+ 607 ./transfer/sftp.py
+15519 total
+
+
+
+
+
+

V02 Plugin Points douloureux

+

L’écriture de plugins devrait être une activité simple pour les personnes ayant une connaissance rudimentaire +de Python et compréhension de la tâche à accomplir. Dans la version 2, +écrire des plugins est beaucoup plus difficile qu’il ne devrait l’être.

+
    +
  • erreur de syntaxe, v2 donne essentiellement une réponse binaire, soit la lecture dans le plugin a fonctionné +ou il ne l’a pas fait… il est très hostile par rapport au python normal.

  • +
  • lorsqu’un paramètre est placé dans un fichier de configuration, sa valeur est [ valeur ], et non valeur (il est dans une liste.)

  • +
  • problème de portée étrange de l’importation (l’importation dans l’ensemble ne se reporte pas sur on_message, besoin d’importer +dans le corps principal de la routine ainsi que dans le fichier python.)

  • +
  • Qu’est-ce qu’est self, qu’est-ce qu’est parent? Ces arguments pour les plugins ne sont pas évidents. +self se réfère généralement à l’appelant, pas au self dans une classe normale, et le parent est le flux, +donc aucun état ne peut être stocké dans self, et tout doit être stocké dans parent. Parent est une sorte de +fourre-tout pour les paramètres et les valeurs dynamiques dans une seule pile.

  • +
  • utilisation bizarre de l’API python logger … self.logger?

  • +
  • impossibilité d’appeler à partir de code python (pas d’API.)

  • +
  • impossibilité d’ajouter des messages de notification dans un plugin (ne peut traiter que le message que vous avez.)

  • +
  • incapacité de traiter des groupes de messages de notification à la fois (par exemple pour les envois simultanés ou +téléchargements, plutôt qu’un seul à la fois.

  • +
  • mauvaise gestion des accusés de réception des messages. v02 ne fait qu’accepter le message précédent +lorsqu’un nouveau est reçu.

  • +
  • manque de clarté sur les options, par rapport aux variables de travail, car elles se trouvent dans le même espace de noms +dans un plugin, si vous trouvez self.setting==True … est-ce parce que l’application l’a défini quelque part, +ou parce qu’une option a été définie par un client… s’agit-il d’un paramètre ou d’une variable ?

  • +
  • apporter des modifications aux messages de notification est un peu compliqué, car ils ont évolué sur différents formats de message.

  • +
+
+

Modifications apportées pour résoudre les problèmes

+
    +
  • utilisez importlib à partir de python, moyen beaucoup plus standard d’enregistrer des plugins. +maintenant les erreurs de syntaxe seront détectées comme n’importe quel autre module python importé, +avec un message d’erreur raisonnable.

  • +
  • pas de décoration étrange à la fin des plugins (self.plugin = , etc… juste du python ordinaire.)

  • +
  • Le choix étrange de parent comme lieu de stockage des paramètres est déroutant pour les gens. +La variable d’instance parent devient option, self.parent devient self.o

  • +
  • les rappels d’événements pluriels remplacent les rappels singuliers :

    +
    +
      +
    • after_accept (self, worklist) remplace on_message (self, parent)

    • +
    • after_work (self, worklist) remplace on_part / on_file (self, parent)

    • +
    +
    +
  • +
  • les messages de notification ne sont que des dictionnaires python. champs définis par json.loads( format de charge utile v03 ) +les messages de notification ne contiennent que les champs réels, pas de paramètres ou d’autres choses… +données simples.

  • +
  • les rappels déplacent les messages de notification entre les listes de travail. Une liste de travail n’est qu’une liste +de messages de notification. Il y en a quatre :

    +
      +
    • worklist.incoming - messages de notification à traiter.

    • +
    • worklist.rejected - message de notification qui ne doit pas être traité ultérieurement.

    • +
    • worklist.ok - messages de notification qui ont été traités avec succès.

    • +
    • worklist.retry : messages de notification pour lesquels le traitement a été tenté, mais qui a échoué.

    • +
    +
  • +
+

pourrait en ajouter d’autres… nombre important de demandes pour quelque chose comme différé

+
    +
  • acks effectués de manière plus proactive, dès qu’un message est traité +(pour les messages de notification rejetés ou ayant échoué, c’est beaucoup plus tôt que dans la version 2.)

  • +
  • ajouter un mécanisme de cadrage pour définir les propriétés du plugin.

  • +
  • propriétés alimentées à __init__ du plugin, le parent a disparu des plugins, ils devraient +juste se référer à self.o pour les options / paramètres dont ils ont besoin. (self.o sépare clairement les options +à partir de données de travail.)

  • +
  • analyse en ligne de commande à l’aide de la bibliothèque argParse standard python. Signifie que les mots-clés ne fonctionnent plus +avec un seul -. Choix de l’utilisation standard de – pour les options basées sur des mots, et - pour les abréviations. +exemples : Bon : –config, et -c, BAD : -config –c .

  • +
+
+
+ +
+

Problèmes connus (résolus dans sr3)

+
    +
  • le passage des messages de journal est vraiment étrange. Nous n’avons pas compris ce que +les objets de journalisation python étaient. Besoin de les utiliser de la manière normale. +de nouveaux modules sont construits de cette façon…

    +

    Dans les nouveaux modules, utilisez la convention logging.getLogger( __name__ ), mais +souvent, le nom ne correspond pas au fichier source réel… pourquoi? +Par exemple, un message de journal d’analyse de config.py s’affiche comme:

    +
    2020-08-13 ...  [INFO] sarra.sr_credentials parse_file ... msg text...
    +
    +
    +

    pourquoi est-il étiqueté sr_credentials? aucune idée.

    +
  • +
  • cette chose bizarre de try/except pour l’importation de modules … essayé de supprimer +mais il a cassé l’analyse des sommes de contrôle… doit passer du temps +sur ce problème en particulier. Sur les nouveaux modules ( sarra.config, +sarra.tmpc.*, sr.py ) en utilisant des importations normales. besoin probablement de +refactorisez le fonctionnement du mécanisme du plug-in de somme de contrôle, puis réessayez.

  • +
+

totalement remanié maintenant. La classe d’intégrité est normale et distincte de flowcb.

+
+
+

Plan concret (Fait)

+

Remplacez sarra/sr_config par sarra/sr_cfg2. La nouvelle sr_cfg2 utilise argparse +et un modèle plus simple pour l’analyse des fichiers de configuration. C’est devenu config.py

+

faire sr.py accepter des opérations sur des sous-ensembles, de sorte qu’il deviennent le point d’entrée unique. +internaliser la mise en œuvre de tous les éléments de gestion, déclarer etc…

+

HMPC - Topic Message Protocol Client… une généralisation de la bibliothèque du passage de message avec +une API simplifiée. résume les différences de protocole +(Ce dernier est devenu le module Moth.)

+

La méthode d’essai consiste à apporter des modifications et à les vérifier par rapport à la branche +sr_insects development. En général, un sr_insects non modifié devrait +fonctionner, mais comme les journaux changent, il y a une logique ajoutée sur cette branche +pour analyser les versions v2 et sr3 de la même manière. Ainsi, les tests de branche development +sont compatibles avec les versions stables et en cours de développement.

+

Pour que chaque composant fonctionne, entraînez-vous avec des tests unitaires individuels, puis +accédez à des tests de flux statique. Peut également faire flakey_broker. Le travail ne fait que se poursuivre +dans la mesure où tous les composants sont convertis. Une fois la conversion complète réalisée, +il faudra examiner dynamic_flow.

+

Le but n’est pas un produit fini, mais un produit avec une structure suffisammen te et appropriée +afin que les tâches puissent être déléguées avec un espoir raisonnable de succès.

+
+
+

Fait

+

La fonctionnalité de sr_amqp.py est entièrement reproduite dans moth/amqp.py +Toute la logique importante est préservée, mais elle est transcrite dans de nouvelles classes. +Devrait avoir un comportement de récupération en cas d’échec identique. Mais ce n’est pas le cas. Nous avons le +test de flux statique qui réussi, mais le courtier flakey, qui teste une telle récupération, +est actuellement cassé. (2022/03 tout va bien maintenant!)

+

sr_cfg2.py était encore un talon, il a beaucoup de fonctionnalités et d’options, mais +ca n’est pas clair comment l’étendre à tous. la chose à propos des instances qui +héritent de configure… c’est étrange, mais difficile de voir comment changer cela +ne cassera pas tout, en termes de plugins… penser à avoir des valeurs par défaut +distribué aux classes qui utilisent les paramètres, et ayant quelque chose +qui les rassemble, au lieu d’une seule chose de configuration massive. +renommé à config.py (aka: sarra.config) et l’exerçant avec +sr.py. C’est maintenant un remplacement complet.

+

Penser à simplement supprimer sr_ le préfixe des classes pour les remplacements, +puisqu’ils sont dans le répertoire sarra de toute façon. donc avoir une classe interne +sarra/instances, sarra/sarra <- remplacer le consommateur… C’est ce qui s’est passé +et est devenu un espace réservé pour la progression, ce qui signifie que les fichiers avec le préfixe sr_ +dans le nom, qui ne sont pas des points d’entrée, indiquent le code v2 qui +n’a pas encore été retiré/remplacé.

+

Ajout d’une sélection de configuration à sr.py (par exemple, subscribe/*) et +options setup et cleanup.

+

add/remove/enable/disable/edit (dans sr.py) terminé.

+

‘log’ abandonné pour l’instant… (quel journal ?)

+

ajout de list, show, et d’un prototype construit de shovel… Obligatoire +une instance (définit les fichiers d’état et les journaux) puis appelle le flux… +flow/run() est visiblement l’algorithme général, +le shovel est une sous-classe de flux.

+

On a un squelette pour les plugins v2 qui fonctionnent (v2wrapper.py) +implémentation basé sur l’importation et orienté groupe sur le sr3 plugin framework. ( #213 )

+

cache (maintenant appelé noDupe) fonctionnant.

+

réécrit le fonctionnement des rappels sr3 pour utiliser des listes de travail, puis refondre la +cache et le réessayez des plug-ins v2 en tant que rappels sr3 eux-mêmes.

+

classe abstraite de fil d’attente de messages renommée de tmpc à moth +(que mange une Sarracenia?)

+

Avec le shovel et le winnow remplacés par de nouvelles implémentations, il passe +le test de flux dynamique, y compris le module Retry porté sur sr3, et +un certain nombre de modules v2 utilisés tels quels.

+

Terminé une version initiale du composant sr3_post maintenant (dans sr3 : flowcb.gather.file.File) +Maintenant, on travaille sur sr_poll, ce qui prendra un certain temps car il implique un refactoring: sr_file, sr_http, +sr_ftp, sr_sftp dans le module de transfert.

+

Principalement effectué sr_subscribe, qui, dans l’ancienne version, est une classe de base pour tous les autres composants, +mais dans sr3 n’est que le premier composant qui télécharge réellement les données. Donc rencontrer tout les +problèmes avec le téléchargement de données et flowcb qui font des choses intéressantes. La plupart de fait, mais +flowcb ne fonctionne pas tout à fait.

+

sr_sarra était simple une fois sr_subscribe fait.

+

Transfert réimplémenté pour obtenir une valeur de retour conventionnelle comme le nombre d’octets +transférés, et s’ils diffèrent, cela signale un problème.

+

sr_sender d’envoi maintenant terminé, impliquait beaucoup plus de réflexion sur la façon de définir de nouveaux_ champs +dans les messages de notification. Mais une fois cela fait, on a pu supprimer à la fois l’expéditeur et sr_subscribe +(la classe parente de la plupart des composants) et a permis la suppression des sr_cache, sr_consumer, sr_file, +sr_ftp, sr_http, sr_message, sr_retry et sr_sftp, sum/*, sr_util.

+

C’est la fin de la partie la plus difficile.

+

Il y avait un engagement à reformater l’ensemble de la base du code en style PEP à l’aide de yapf3. +Maintenant, on a le hook de pré-commit yapf3 qui reformate les modifications afin que toute la base du code +reste au format yapf3.

+

Ont également une limitation du débit de messages écrits dans le noyau, donc maintenant on a message_rate_min, et message_rate_max +comme paramètres qui remplacent/déprécient le plugin v2 post_rate_limit.

+
+

Inquiétudes abordées

+

Cette section contient des problèmes qui ont été résolus. Ils ont été gênants pendant un certain temps, +donc en notant quelle était la solution.

+
    +
  • la journalisation à l’aide de __name__ finit parfois par prétendre provenir du mauvais fichier. +exemple:

    +
    2020-08-16 01:31:52,628 [INFO] sarra.sr_credentials set_newMessageFields FIXME new_dir=/home/peter/sarra_devdocroot/download
    +
    +
    +

    set_newMessageFields est dans config.py pas sr_credentials… pourquoi fait-il cela? +Attends probablement que tout le code hérité soit remplacé avant d’attaquer à ce problème. +si cela n’est pas corrigé, faites-en un rapport de bogue.

    +

    corrigé : note… le problème était que la déclaration de l’enregistreur qui devait être APRÈS toutes +Importations. Concrètement:

    +
    logger = logging.getLogger( __name__ )
    +
    +
    +

    doit être placé après toutes les importations.

    +
  • +
  • sr_audit ? que faire. Supprimé.

  • +
  • tous les fichiers non entry_point sr_*.py peuvent être supprimés. +supprimer le sous-répertoire sum. sr_util.py

  • +
+
+
+

Révision de l’Accel

+

la compatibilité des plugins est à l’étude… on a décidé de réécrire les plugins accel_* pour sr3, et +changer l’API car celle de la v2 présente des lacunes fondamentales :

+
    +
  • l’api do_get traite l’échec en soulevant une exception… il n’y a pas de vérification +des codes de retour sur les routines intégrées… Il est possible de s’en occuper par try/except, +mais on préférerait qu’un flux de programme normal puisse tracer et +signaler lorsqu’une défaillance d’i/o se produit (gardez try/except à une échelle aussi petite que possible.)

  • +
  • il y a une nature très idiosyncratique du do_get, par exemple dans la accel_scp v2, +où il appelle do_get, puis décide de ne pas s’éxecuter et tombe à celui qui est intégré. +Cette logique est rarement utile, difficile à expliquer et déroutante à diagnostiquer +pratiquement.

  • +
+

Avoir réécrit accel_wget, et accel_scp à la nouvelle api… on travaille via static-flow +pour les tester. Il est également logique de repérer les invocations v2 d’entre eux, et de les remplacer par sr3 +dans la configuration. Et la première tentative a été assez alambiquée… on n’était pas content. +pareil pour la 2ème tentative… on travaille sur une troisième.

+

Réécrit à nouveau, il suffit d’ajouter getAccelerated() à l’API de transfert, afin qu’il soit intégré +au lieu d’être un plugin. N’importe quelle classe de Transfer peut spécifier un accélérateur et il +sera déclenché par accel_threshold. Les accélérateurs https et sftp/scp sont implémentés.

+
+
+
+

ToDo

+

Éléments de la liste todo qui ont été traités.

+
    +
  • migrer sr_xattr.py vers sarra/xattr.py (maintenant appelé sarracenia/filemetadata.py)

  • +
  • corriger flakey_broker test poir qu’il réussise. (terminé!)

  • +
  • mise à jour de la documentation… tout changer pour utiliser le point d’entrée sr3, oui c’est fait. +(Voir le point de transition ci-dessous.)

  • +
  • considérer la transition, la vie avec les deux versions… faut-il sr.py –> sr3.py ? Oui. Fait. +devrions-nous avoir un paquet Debian séparé avec des points d’entrée de transition +(sr_subscribe et amis uniquement inclus dans le forfait compact, et tout) +l’interactivité native ne se produit que via sr3 ? +maintenant appelé metpx-sr3

  • +
  • peut-être déplacer tout le plugin d’un niveau (se débarrasser du répertoire) +donc Plugin devient une classe instanciée en sarra/__init__.py… Ca mets les +plugins et le code intégré à un niveau plus égal… par exemple comment +les protocoles de transfert de plugins fonctionnent-ils ? idée… C’est en quelque sorte fait +maintenant: plugin est devenu flowcb. L’intégrité est supprimée de la hiérarchie. +L’extension de classe est maintenant un type de plugin séparé (via l’importation)

  • +
  • changer le topic_prefix par défaut en v03.post. effectué 2021/02

  • +
  • changer le topic_prefix par défaut en v03. effectué 2021/03

  • +
  • changer topic_prefix à topicPrefix. effectué 2021/03

  • +
  • Ajustez le Guide du programmeur pour refléter le nouvel API. effectué 2021/02

  • +
  • l’incohérence des journaux entre ‘info’ et logging.INFO empêche un contrôle correct des journaux. +CORRIGÉ 2021/02.

  • +
  • accélérateurs manquants: sftp.putAcc, ftp.putAc, ftp.getAc, file.getAc,

  • +
  • migrer sr_credentials.py vers sarracenia/credentials.py.

  • +
  • supprimer post des arborescences de rubriques v03. Fait!

  • +
  • points d’entrée de nettoyage: sr_audit, sr_tailf, sr_log2save,

  • +
  • test avec dynamic-flow.

  • +
  • Support MQTT (Terminé!)

  • +
+
+
+

BUGS/Préoccupations/Problèmes

+

migré vers github issues avec la balise de v3only.

+
+
+

Après la parité : de vraies améliorations

+
+
+

TODO

+

À ce stade, je suis en mesure de signaler les problèmes existants en tant que problèmes avec la balise v03only. +voici donc les choses restantes après la refactorisation:

+
    +
  • ajouté le message “missing defaults”, examiner la liste et voir si nous devons tous les définir. +check_undeclared_options valeurs par défaut manquantes : {‘discard’, ‘exchangeSplit’, +‘pipe’, ‘post_total_maxlag’, ‘exchangeSuffix’, ‘destination’, ‘inplace’, +‘report_exchange’, ‘post_exchangeSplit’, ‘set_passwords’, ‘declare_exchange’, +‘sanity_log_dead’, ‘report_daemons’, ‘realpathFilter’, ‘reconnect’, +‘post_exchangeSuffix’, ‘save’, ‘pump_flag’, ‘cache_stat’, ‘declare_queue’, ‘restore’, +‘bind_queue’, ‘dry_run’, ‘sourceFromExchange’, ‘retry_mode’, ‘poll_without_vip’, ‘header’} +#405

  • +
  • #369 … clean shutdown

  • +
  • déterminer une implémentation AsyncAPI pour l’abonnement au moins. #401

  • +
  • faire en sorte que les transferts de fichiers partitionnés fonctionnent à nouveau. #396 +on_part_assembly.rst

  • +
  • convertir un poll existant en poll0 ? ancien poll. #394

  • +
  • alarm_set tronque en entiers… Hmm.. utiliser setitimer à la place? #397

  • +
  • l’option outlet est manquante. #398

  • +
  • Support vhost nécessaire. #384

  • +
  • sr_poll bug actif/passif #29

  • +
  • realpathFilter est utilisé par CMOI. Semble avoir disparu dans sr3. C’est là dans la version C. #399

  • +
  • porter le reste des plugins v02 vers des équivalents v03 et ajoutez des mappages dans config.py, #400 +de sorte qu’il ne nous reste presque plus de v2.

  • +
  • transfert / sftp.py supprimer file_index de l’implémentation ( # 367 ) dépendent de NoDupe.py

  • +
  • mode asynchrone complet pour les MQP. nécessite des fonctionnalités publish_retry. +(encore une fois dans les plans futurs ci-dessus.) #392

  • +
  • une fois le mode asynchrone complet disponible, autorisez plusieurs collectes(gathers) et publications. +(encore une fois dans les plans futurs ci-dessus.) #392

  • +
  • #33 ajouter le nom d’hôte à la fil d’attente par défaut.

  • +
  • #348 ajouter statehost à l’arborescence de répertoires .cache.

  • +
+
+
+

Pas cuit / À penser

+

Les choses du code structurel qui ne sont pas réglées peuvent changer. +Probablement besoin d’être réglé avant que quelqu’un d’autre ne plonge.

+
    +
  • propriétés scopables pour les classes internes, comme elles existent pour les plugins. #402 +Je pense que c’est fait. Il faudrait documenter quelque part, +tester et faire des démonstrations en même temps.

  • +
  • on a pris le code requis pour implémenter set_newMessageFields (maintenant appelé +Sarracenia.Message.updateFieldsAccept) textuellement à partir de la v2. +C’est assez poilu… peut-être se transformer en plugin, pour le sortir du +code principal? Ne pas qu’il disparaîtra un jour. C’est assez laid, mais +très utile et très utilisé dans les configurations existantes. probablement OK.

  • +
  • modification du modèle de récupération, de sorte que toute la nouvelle logique/tentative soit dans la +boucle principale, #392 +et moth revient immédiatement. Le but est qu’on pourrait avoir plusieurs +gathers pour plusieurs flux en amont et reçevoir des messages de notification de la part de celui qui est +vivant… on se retrouve également avec une seule boucle de cette façon… plus propre. +probablement équivalent au mode asynchrone mentionné ci-dessus.

  • +
  • gather comme un moyen de séparer le fait d’avoir plusieurs courtiers d’entrées. #392 +donc on pourrait éviter d’avoir besoin d’un winnow, mais juste d’avoir un abonné qui se connecte à +plusieurs en amont directement. +probablement équivalent à async et multi-gather.

  • +
  • pensez à l’API en sous-classant le flux… et l’intégration automatique +avec le point d’entrée sr… Hmm… probablement regarder cela lors de la mise à jour +Guide du programmeur.

  • +
  • plus de worklists? échec de renommage -> nouvelle tentative ou différé. Ajouter un nouvel échec +où l’échec représente un échec permanent. et l’autre représente +à réessayer plus tard.

  • +
  • MQTT issues

  • +
+
+
+

FIXME/Différé

+

Le but du travail principal de sr3 est d’obtenir un refactorisation au point où +le code est compréhensible pour les nouveaux codeurs, de sorte que les tâches peuvent être attribuées. +Cette section comprend un mélange de tâches qui, espérons-le, peuvent être assignées,

+

FIXME sont des choses laissées de côté qui doivent être vues.

+
    +
  • RELEASE BLOCKER poilu. #403 +watch ne fait pas de lot par lots. Il jette juste un arbre entier. +Cela devra être re-écrit avec une approche de style itérateur. +Donc si vous commencez dans une arborescence avec un million de fichiers, il analysera le million entier +et les présentera comme une liste de travail unique en mémoire. Cela aura des problèmes performances. +Vous souhaitez procéder de manière incrémentielle à l’aide de listes d’un lot ‘prefetch’ +à la fois.

    +

    Il existe une correction provisoire pour prétendre qu’il fait le traitement par lots correctement, mais +l’impacte de la mémoire et le retard de production du premier fichier sont toujours là, mais au moins +renvoie un lot à la fois.

    +
  • +
  • RELEASE BLOCKER journaux de poll et watch ont tendance à devenir énormes beaucoup trop rapidement. #389

  • +
  • essayez jsonfile pour créer des messages de notification à publier. peut construire json de manière incrémentielle, # 402 +vous n’avez donc pas besoin de supprimer les éléments _deleteOnPost (vous pouvez simplement les ignorer)

  • +
  • euh… ajouter les protocoles. mqtt et qpid-proton (amq1) #389

  • +
  • assurez-vous que l’arrêt fonctionne réellement… voir des égarés après les tests… mais trop de changement +pour vraiment savoir. besoin de vérifier… C’est le cas!

  • +
  • Nous avons renoncé à l’envoi partitionné comme un retranchement pour le refactor. Il viendra dans un +version ultérieure.

  • +
  • la plupart des fonctionnalités de reporting sont supprimées.

  • +
+
+
+

Transition

+

Je ne sais pas si une mise à niveau simple (de remplacement) est une bonne approche. Sera-t-il possible de tester sarra +suffisamment pour que des mises à niveau de pompes entières soient possibles? ou des mises à niveau incrémentielles +(parallèles) soit requis?

+

Cela dépend si sr3 fonctionnera comme un remplacement drop-in ou non. Il y a une certaine incompatibilité +que nous savons va se produira avec les plugins do_*. Si cela est suffisamment bien documenté et facilement +traité, alors ce n’est peut-être pas un problème. D’autre part, s’il y a des subtilités, +alors une approche parallèle pourrait être nécessaire.

+
+

Remplacement

+

Le paquet a le même nom que ceux de la v2 (metpx-sarracenia) qui différent que par le numéro de version. +L’installation du nouveau remplace complètement l’ancien. Cela nécessite que la nouvelle version soit égale +ou mieux que l’ancien dans tous les aspects, ou que l’installation soit limitée aux machines d’essai +jusqu’à ce que ce point soit atteint.

+

Cela prend plus de temps pour obtenir l’installation initiale, mais a une démarcation beaucoup plus claire (vous savez +lorsque vous avez terminé.)

+
+
+

Parallel

+

Nommez le paquet metpx-sarra3 et demandez au répertoire de classe python d’être sarra3 (au lieu de sarra.) +(aussi ~/.config/sr3 et ~/.cache/sr3. Les fichiers .cache doivent probablement être différents car +les fichiers de nouvelle tentative ont des formats différents? valider. ) On peut donc copier des configurations de l’ancien vers +le nouveau et exécuter les deux versions en parallèle. Le point d’entrée central serait sr3 (plutôt que +sr), et pour éviter toute confusion, les autres points d’entrée (sr_subscribe etc…) seraient omis +de sorte que le code v2 fonctionnerait inchangé. Peut nécessiter quelques ajustements pour que les classes sr +ignorent les instances des autres versions.

+

Ceci est similaire à la transition de python2 vers python3. Cela permet le déploiement de sr3 sans avoir +à s’y convertir entièrement. Cela permet d’exécuter certains composants et de gagner lentement en maturité +tandis que d’autres ne sont pas prêts. Cela facilite les tests A:B, en exécutant la même configuration +avec une version ou l’autre sans avoir l’installation ou l’utilisation d’une machine différente, +ce qui facilite la vérification de la compatibilité.

+
+
+

Conclusion

+

Avoir implémenté le modèle parallèle, avec APPNAME=sr3 ( ~/.config/sr3, ~/.cache/sr3 ) +le préfixe sr3_ remplace sr_ pour toutes les commandes et change la classe Python sarra au +nom complet de sarracenia pour éviter les conflits entre les classes de python.

+
+
+
+

Incompatibilités

+

Il n’est pas censé y en avoir. Il s’agit d’une liste en cours d’exécution de choses à corriger ou à documenter. +gros changements:

+
    +
  • dans sr3, utilisez – pour les options de mots complets, comme –config ou –broker. Dans la v2, vous pouvez utiliser -config et -broker, +mais cela finira mal dans sr3. Dans l’ancien analyseur de ligne de commande, -config et –config étaient identiques, ce qui +était idiosyncratique. Le nouvel analyseur d’options de ligne de commande est construit sur +ArgParse et interprète un seul - comme préfixe une seule option où le +les lettres suivantes sont et argumentent. Exemple:

    +

    -config hoho.conf -> dans la v2 fait référence au chargement du fichier hoho.conf en tant que configuration.

    +

    dans sr3, il sera interprété comme -c (config) charger le fichier config.conf, et hoho.conf fait partie d’une option ultérieure.

    +
  • +
  • loglevel none -> loglevel notset (maintenant on passe le paramètre directement au module de journalisation python, none n’est pas défini.)

  • +
  • les messages de journal et la sortie en interactif seront complètement différents.

  • +
  • paramètres abandonnés: use_amqplib, use_pika… remplacé par des bibliothèques d’implémentation +distinctes par protocole. amqp utilise la bibliothèque ‘amqp’ qui n’est ni l’une ni l’autre des choses ci-dessus. +( commit 02fad37b89c2f51420e62f2f883a3828d2056de1 )

  • +
  • laissant tomber on_watch plugins. pour autant que je sache, personne ne les utilise. +La façon don’t v03 fonctionne, ce serait un after_accept pour un watch. +c’est plus logique de cette façon.

  • +
  • les plugins qui accèdent aux internes de sr_retry doivent être réécrits, car la classe est maintenant plugin/retry.py. +la façon de mettre en fil d’attente quelque chose pour une nouvelle tentative dans les plugins actuels consiste +à les ajouter à la fil d’attente ayant échoué. +Ce n’est qu’un problème dans les tests de débit de sr_insects.

  • +
  • do_download et do_send étaient 1er passage aux plugins schemed, je pense qu’ils devraient être déconseillés / remplacés +par do_get et do_put. Ca n’est plus clair si ils sont utiles (télécharger et envoyer des plugins sont +au mauvais niveau d’abstraction)

  • +
  • do_download, do_send, do_get, do_put sont des téléchargements schemed… c’est-à-dire, plutôt que d’empiler de sorte que +tous sont appelés, ils sont enregistrés pour des protocoles particuliers. Dans la v2, par exemple, les plugins accel_* +enregistrent le schéma “download”. Un point d’entrée on_message modifierait le schéma de sorte que la routine do_* +serait invoqué. Dans la v2, la signature d’appel pour tous les plugins est la même (self, parent) mais pour +les cas do_get et do_put, c’est contre-productif. Ayez donc à la place une signature d’appel +identique au protocole intégré get/put… src_file, dst_file, src_offset, dst_offset, len ) +Résolution: il suffit d’implémenter de nouvelles classes de transfert, ne s’intègre pas naturellement dans flowcb.

  • +
  • Dans la v2, les paramètres par défaut du miroir étaient False dans tous les composants, à l’exception de sr_sarra. +mais le réglage du miroir n’a pas été honoré dans shovel, et winnow (bug #358) +ce bogue est corrigé dans sr3, mais vous remarquez alors que la valeur par défaut est incorrecte.

    +

    Dans sr3, la valeur par défaut pour mirror est remplacée par True pour tous les flux, à l’exception de subscribe, qui +est le comportement le moins surprenant étant donné que la valeur par défaut est False dans la v2.

    +
  • +
  • dans la v2, le téléchargement ne vérifie pas la longueur d’un fichier pendant le téléchargement. +dans sr3, c’est le cas. Par exemple, lors de l’utilisation de sftp comme sondage, ls répertorie la taille d’un lien symbolique. +Lorsqu’il télécharge, il obtient le fichier réel, et non le lien symbolique, de sorte que la taille est différente.

  • +
+

Exemple tiré du test de débit

+
     2021-04-03 10:13:07,310 [ERROR] sarracenia.transfer read_writelocal util/writelocal mismatched file length writing FCAS31_KWBC_031412___39224.slink. Message said to expect 135 bytes.  Got 114 bytes.
+
+le fichier est de 114 octets, mais le chemin de liaison est de 135 octets...
+v2 et sr3 téléchargent le fichier et non le lien, mais sr3 produit ce message d’erreur.
+En pensant à celui-ci...  est-ce un bug dans le poll?
+
+
+
    +
  • Dans la v2, si vous supprimez un fichier, puis le recréez, un événement sera créé. +Dans sr3, si vous faites de même, l’ancienne entrée sera dans la cache nodupe et l’événement sera supprimé. +J’ai remarqué cette différence, mais je ne sais pas quelle version est correcte. +cela pourrait être corrigé, si nous décidons que l’ancien comportement est juste.

  • +
+
+
+

Fonctionnalités

+
    +
  • Tous les composants sont maintenant dérivés de la classe flow et exécutent déjà l’algorithme général +conçu comme la base de la v2, mais jamais implémenté en tant que tel.

  • +
  • L’API d’extension est maintenant du python simple sans paramètres magiques. Juste des classes standard, en utilisant un mécanisme +d’importation standard. Le débogage devrait être beaucoup plus simple maintenant car l’interpréteur fournira de +bien meilleurs messages d’erreur au démarrage. Les plugins de style v2 sont maintenant appelés flow callbacks, +et il existe un certain nombre de classes (identity, moth, transfert, peut-être flux) qui permettent l’extension +par une sous-classification simple. Cela devrait faire en sorte que ce soit beaucoup plus facile d’ajouter +des protocoles supplémentaires pour le transport et les messages, ainsi que des algorithmes de somme de +contrôle pour les nouveaux types de données.

  • +
  • La classe sarra.moth fait l’abstraction de l’AMQP, de sorte que le protocole de messagerie devient enfichable.

  • +
  • utiliser le préfixe sarracenia/ (déjà présent) pour remplacer le préfixe sr_ sur les modules.

  • +
  • Un accès API aux flux. (ainsi on peut construire des programmes entièrement nouveaux en python en sous-classant.)

  • +
  • les propriétés/options des classes sont désormais hiérarchiques, de sorte qu’elles peuvent définir le débogage sur des classes spécifiques dans l’application.

  • +
  • sr3 ability pour sélectionner plusieurs composants et configurations sur lesquels on peut opérer.

  • +
  • sr3 list examples est maintenant utilisé pour afficher des exemples distincts de ceux installés.

  • +
  • sr3 show est maintenant utilisé pour afficher la configuration analysée.

  • +
  • les messages de notification sont accusés de réception plus rapidement, ce qui devrait aider au débit.

  • +
  • Les entry_points de plug-in FlowCB sont désormais basées sur des groupes de messages de notification, plutôt que sur des messages individuels, permettant aux gens +d’organiser le travail simultané.

  • +
  • l’intégrité (sommes de contrôle) sont maintenant des plugins.

  • +
  • gather (entrée ? sources de messages de notification) sont désormais des plugins.

  • +
  • ajout du typage des paramètres d’options, afin que les plugins puissent déclarer: taille, durée, chaîne ou liste.

  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Contribution/\303\211volution.html" "b/fr/Contribution/\303\211volution.html" new file mode 100644 index 000000000..867b09db2 --- /dev/null +++ "b/fr/Contribution/\303\211volution.html" @@ -0,0 +1,177 @@ + + + + + + + Changements de conception depuis l’original (2015) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Changements de conception depuis l’original (2015)

+

Depuis 2022/03, la conception n’a pas beaucoup changé, mais la mise en œuvre de SR3 +est totalement différent de la v2. Modifications de conception :

+
    +
  • La prise en charge explicite des applications de base pour les rapports a été supprimée +comme ils n’ont jamais été utilisés, peuvent facilement être réinsérés en tant que rappels +et conventions.

  • +
  • personne n’utilisait de fichiers segmentés, et ils étaient très compliqués, +mais tout le monde les trouve fantastiques en théorie. Nécessité de ré-implémenter +après la refactorisation SR3.

  • +
  • la mise en miroir était un cas d’utilisation que nous devions aborder, nous devions ajouter des métadonnées, +et évoluer un peu.

  • +
  • les concepts de routage de cluster ont été supprimés (cluster_from, cluster_to, etc…) +Cela a gêné les analystes plus qu’il ne les a aidé. Super facile à +implémenter en utilisant des rappels de flux, si jamais nous voulons les récupérer.

  • +
  • l’algorithme Flow est apparu comme un concept +fédérateur pour toutes les différentes composantes initialement envisagées. Dans les premiers travaux +sur la v2, nous ne savions pas si tous les composants fonctionneraient de la même manière, ils ont donc +été écrits séparément de zéro. Beaucoup de copier/coller de code parmi les points d’entrée.

  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git "a/fr/Explication/Aper\303\247u.html" "b/fr/Explication/Aper\303\247u.html" new file mode 100644 index 000000000..e7ac276bd --- /dev/null +++ "b/fr/Explication/Aper\303\247u.html" @@ -0,0 +1,375 @@ + + + + + + + Aperçu — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Aperçu

+

MetPX-Sarracenia est une boîte à outils de gestion des publications/abonnements pour la publication de données en temps réel.

+

Sarracenia ajoute une couche de protocole de mise en fil d’attente de messages d’annonce de disponibilité de fichiers +aux serveurs de fichiers et Web pour piloter des flux de travail qui transfèrent et transforment les données en continu +dans un contexte en temps réel et critique.

+

L’un des principaux objectifs de la boîte à outils est de relier les processus afin qu’ils évitent d’avoir +à interroger (interroger en continu, répertorier, puis filtrer) des serveurs ou des répertoires. Sarracenia +peut également être utilisé pour mettre en œuvre un sondage initial en amont, qui est toujours gagnant car +les tâches au-delà de l’identification initiale du fichier à traiter peuvent être pilotées par des notifications, +qui sont nettement moins chers (en i/o et en traitement) que l’interrogation même des répertoires locaux.

+

Cette couche de gestion fournit des méthodes simples pour obtenir le parallélisme dans les transferts de fichiers, +la robustesse face aux défaillances, l’équilibrage de charge entre les étapes d’un flux de travail, et +prend en charge de nombreux modes de défaillance, de sorte que les développeurs d’applications n’ont pas besoin de le faire. +Des centaines de ces flux peuvent être composés ensemble en grandes pompes de données et +exploité à l’aide de méthodes courantes familières aux administrateurs système Linux.

+

Vidéo de conception de 2015: Sarracenia in 10 Minutes Video

+
+

Aperçu Détaillé

+

MetPX-Sarracenia est un fichier de configuration et un service piloté par ligne de commande pour +télécharger les fichiers au fur et à mesure qu’ils sont mis à disposition. On peut s’abonner à un +serveur Web Sarracenia activé (appelé pompe de données) et sélectionner les données à diffuser à partir de celui-ci, +en utilisant Linux, Mac ou Windows. Plus que cela:

+
    +
  • Cela évite aux gens d’avoir à interroger le serveur Web pour savoir si leurs données sont +là (peut être 100x moins de travail pour le client et le serveur).

  • +
  • c’est plus facile de télécharger en utilisant true pub / sub. Il reçoit des notifications +exactement quand le fichier est prêt.

  • +
  • C’est naturellement parallèle: quand un processus ne suffit pas, il suffit d’en ajouter d’autres. Ils +partagera les mêmes sélections de manière transparente.

  • +
  • On peut enchaîner plusieurs pompes de données ensemble, afin que les gens puissent maintenir des +arborescences copiées indépendantes en temps réel pour la redondance des services et également pour le réseau pour des +raisons de topologie (pour desservir des réseaux privés, par exemple).

  • +
  • Plusieurs copies signifient une disponibilité découplée. Un serveur en panne n’affecte pas +l’ensemble du réseau d’APIS imbriquées. Les pompes de données forment des mailles, là où les données sont +transféré afin que chacun puisse en avoir une copie s’il le souhaite. C’est un moyen très simple +d’y parvenir.

  • +
  • Il peut également pousser des arbres (en utilisant un expéditeur au lieu d’un abonné), ce qui est bien +pour les transferts entre les démarcations réseau (firewall).

  • +
  • En utilisant uniquement la configuration, les fichiers peuvent être renommés sur place et la structure +du répertoire peut être complètement modifiée.

  • +
  • Avec l’API de plugin, on peut transformer l’arbre ou les fichiers dans l’arbre. +L’arbre de sortie ne peut avoir aucune ressemblance avec l’arbre d’entrée.

  • +
  • L’API du plugin peut être utilisée pour mettre en œuvre des flux de travail efficaces pilotés par les données, +réduire ou éliminer l’interrogation des répertoires et des tâches planifiées qui imposent des +charges lourdes et augmentatent la latence de transfert.

  • +
  • Les flux de travail en plusieurs étapes sont naturellement mis en œuvre en plus de +mettre en relation les producteurs et les consommateurs. La transformation est un consommateur au sein de la +pompe de données, tandis que les consommateurs externes accèdent aux produits finaux. Les files d’attente entre les composants +assure la coordination de flux.

  • +
  • Vous pouvez configurer un poll pour que n’importe quel site Web agisse comme une pompe de données Sarracenia. +Ainsi, le flux peut fonctionner même sans pompe Sarracenia pour commencer.

  • +
  • Sarracenia est robuste. Ca fonctionne 24h/24 et 7j/7 et prend des dispositions étendues pour être un +participant civilisé dans les flux de données de mission critiques :

    +
    +
      +
    • Lorsqu’un serveur est en panne, il utilise un backoff exponentiel pour éviter ses conséquences.

    • +
    • Lorsqu’un transfert échoue, il est placé dans une fil d’attente de nouvelles tentatives. Les autres transferts se +poursuivent et le transfert échoué est réessayé ultérieurement lorsque les flux en temps réel le permette.

    • +
    • La fiabilité est réglable pour de nombreux cas d’utilisation.

    • +
    +
    +
  • +
  • Puisque Sarracenia s’occupe des pannes transitoires et des files d’attente, votre application +ne traite que des cas normaux.

  • +
  • Il utilise des protocoles de fil d’attente de messages (actuellement AMQP et / ou MQTT) pour envoyer des avis fichiers +et les transferts de fichiers peuvent être effectués via SFTP, HTTP ou tout autre site Web +service.

  • +
  • Il ne dépend d’aucune technologie propriétaire. L’utilisation est entièrement gratuite et peut être utilisé +à n’importe quel fins.

  • +
  • Un exemple de mise en œuvre suivant les World Meteorological Organizations +essaye de remplacer le Système mondial de télécommunications (GTS) par des solutions modernes.

  • +
+

À la base, Sarracenia expose une arborescence de dossiers accessibles sur le Web (WAF), en utilisant n’importe quel +serveur HTTP standard (testé avec apache) ou serveur SFTP, avec d’autres types de serveurs possible via des modules. +Les applications météorologiques sont en temps réel souple (anglais: soft real-time), où les données +doivent être transmises le plus rapidement possible jusqu’au prochain saut, et les minutes, peut-être +les secondes, comptent. Les technologies web push standard, ATOM, RSS, etc…. sont en fait des +technologies de sondage qui, lorsqu’elles sont utilisées dans des applications à faible latence, +consomment beaucoup de bande passante et surcharge les serveurs et réseaux inutilement. Pour ces raisons +précises, ces normes stipulent un intervalle minimal de sondage de cinq minutes. La messagerie AMQP +(Advanced Message Queueing Protocol) pousse réellement les notifications et rend l’envoi en temps réel +beaucoup plus efficace.

+../../_images/sr3_flow_example.svg +

Les sources de données publient leurs produits, les pompes extraient les données en utilisant HTTP ou SFTP via arborescence de dossiers (WAF), puis annoncent cette arborescence aux clients en aval. +Lorsque les clients téléchargent des données, ils peuvent écrire un rapport au serveur. Les serveurs sont configurés pour renvoyer ces messages de rapport du +client par l’intermédiaire de la fonction à la source. : La source peut voir le chemin au complet pris par les données pour arriver jusqu’à chaque client. Dans le +cas des applications de commutation traditionnelles, les sources ne voient que ce qu’elles ont livré au premier maillon d’une chaîne. Au-delà de ce premier maillon, le +routage est opaque, et le traçage du cheminement des données nécessitent l’aide des administrateurs de chacun des systèmes. Avec la transmission de rapport de Sarracenia, le réseau de commutaiton est relativement transparent pour les sources. +Le diagnostic est alors grandement simplifié.

+

Pour les gros fichiers / haute performance, les fichiers sont segmentés à l’ingestion s’ils sont suffisamment gros pour que cela en vaille la peine. +Chaque fichier peut traverser le réseau de pompage de données indépendamment, et le réassemblage du fichier initial ne se fait qu’à la fin du processus de transfert. Un fichier de taille suffisante annoncera +la disponibilité de plusieurs segments pour le transfert, des fils multiples ou des nœuds de transfert prendront ces segments et les transféreront. Plus il y a de segments disponibles, plus le niveau de parallèlisme du transfert est élevé. Dans de nombreux cas, Sarracenia gère le parallélisme et l’utilisation du +réseau sans intervention explicite de l’utilisateur.Les pompes de données ne doivent ni stocker ni transférer des fichiers entiers, la taille maximale de fichier qui peut voyager à travers le réseau est maximisée.

+
    +
  • REMARQUE: Pour la v03, la fonctionnalité de segmentation a été supprimée temporairement. Prévu pour retour dans la version 3.1.

  • +
+
+
+

Implémentations

+

Une partie de Sarracénia définit un message d’annonce avec AMQP comme transport. +Il y a des implémentations multiples qui acceptent ses messages d’annonce:

+
    +
  • Sarracenia elle-même (http://github.com/MetPX/sarracenia) +une implémentation de référence complète en Python >= 3.4. +Il fonctionne sous Linux, Mac et Windows.

  • +
  • sarrac ( https://github.com/MetPX/sarrac) est une implémentation en +C de l’insertion de données (post & watch.) C’est Linux uniquement. Il +y a aussi une libcshim pour pouvoir implémenter de manière transparente +l’insertion de données avec cet outil, et libsarra permet aux programmes +C de poster directement. Il y a aussi du code consommateur (sr_cpump, +pour lire les files d’attente) mais pas de téléchargement jusqu’à présent. +Ce sous-ensemble est destiné à être utilisé là où les environnements +python3 ne sont pas pratiques (certains environnements HPC).

  • +
  • node-sarra ( https://github.com/darkskyapp/node-sarra) Une implémentation embryonnaire pour node.js.

  • +
  • ecpush ( https://github.com/TheTannerRyan/ecpush ) un simple client in Go ( http://golang.org )

  • +
  • PySarra ( https://github.com/JohnTheNerd/PySarra ) un client archi-simple en python3.

  • +
  • dd_subscribe ( https://github.com/MetPX/sarracenia) client en python2 (Le prédécesseur de Sarracénia.) Toujours compatible.

  • +
+

D’autres clients sont les bienvenues.

+
+

Pourquoi ne pas simplement utiliser Rsync ?

+

Il existe un certain nombre d’outils de réplication d’arbres qui sont largement +utilisés, pourquoi en inventer un autre ? RSync, +par exemple, est un outil fabuleux, et nous avons Il est fortement recommandé +pour de nombreux cas d’utilisation. mais il y a des moments où la Sarracenia peut aller +72 fois plus rapide que rsync : Étude de cas : HPC Mirroring Use Case

+

Rsync et d’autres outils sont basés sur la comparaison (traitant d’une source et d’une destination +unique) Sarracénie, bien qu´elle n’utilisent pas la multidiffusion, est orienté vers une livraison +à plusieurs récepteurs, en particulier lorsque la source ne sait pas qui sont tous les +récepteurs (pub/sub.) La synchronisation rsync est typiquement faite en marchant un à un. +L’intervalle de synchronisation est intrinsèquement limité à la fréquence +à laquelle on peut traverser (sonder?) l’arbre de fichiers (dans les grands arbres, cela peut être long). +La Sarracenia évite les promenades dans les arbres de fichiers en demandant +aux sources de données de signaler directement aux lecteurs par des messages, réduisant ainsi +les frais généraux de plusieurs ordres de grandeur.`Lsyncd <https://github.com/axkibe/lsyncd>`_ +est un outil qui exploite les fonctionnalités INOTIFY de Linux. pour atteindre le même genre +de rapidité de détection the changement, et il pourrait être plus approprié, mais il n’est +évidemment pas portable, et reste très lente en comparaison avec les avis émis directement +par les sources. De plus, faire faire cela par le système de fichiers est considéré comme +lourd et moins général qu’explicite passage de messages via middleware, qui gère également +les logs de manière simple.

+

Un des objectifs de Sarracenia est d’être de bout en bout. Rsync est point-à-point, +ce qui signifie qu’il ne prend pas en charge la transitivité des transferts +de données entre plusieurs pompes de données qui est désiré. D’autre part, le +premier cas d’utilisation de la Sarracenia est la distribution du nouveaux +fichiers. Au départ, les mises à jour des dossiers n’étaient pas courantes. +ZSync est beaucoup plus proche dans l’esprit +de ce cas d’utilisation. Sarracenia divise les fichiers en block de facon similaire, +bien que généralement beaucoup plus grand (50M est un bon choix), que les blocs +Zsync (typiquement 4k), plus propice à l’accélération. Utilisation d’une +annonce par bloc de somme de contrôle permet d’accélérer les transferts plus +facilement.

+

L’utilisation du bus de messages AMQP permet l’utilisation de transferts de +tiers partis, flexibles, une surveillance simple à l’échelle du système et +l’intégration d’autres caractéristiques telles que la sécurité à l’intérieur +du flux.

+

Une autre considération est que Sarracenia n´implante aucun transport. Il est +agnostique au protocole utilisé pour le transfert des données. Il peut +annoncer des URLs de protocole arbitraire, et on peut rajouter des plugins +pour fonctionner avec des nouveaux protocoles, ou substituer des téléchargeurs +accélérés pour traiter les transferts avec des protocoles déjà connus. +Les pilotes de transfert intégrés incluent des accellerateurs binaires +et des critères accordables pour les utiliser.

+

Caveat La segmentation des fichiers a été supprimée. FIXME

+
+
+

Pas de FTP ?

+

Les protocoles de transport entièrement pris en charge par Sarracenia sont +http(s) et SFTP (SSH File Transfer Protocol). Dans de nombreux cas, lorsque +des données publiques sont échangées, FTP +est une lingua franca qui est utilisée. L’avantage principal étant la simplicité relative, +l’accès aux programmes, ce qui est très simple avec Sarracenia. +De nos jours, avec l’augmentation des préoccupations en matière de sécurité, et +l´arrivée d´instructions de cryptage danse les processeurs centrales +et les noyaux multiples quelque on a, en quelque sort, une surabondance de processeurs, +et il n’est plus très logique de ne pas crypter le trafic. De plus, pour +Sarracenia utilise des plages d’octets, qui sont les suivantes +fournis par les serveurs SFTP et HTTP, mais pas FTP. Nous ne pouvons donc pas +soutenir le fichier partitionnement sur FTP. Ainsi, bien que le FTP fonctionne +en quelque sorte, ce n’est pas maintenant, ni ne le fera jamais. +être, pleinement soutenu.

+
+
+

Références et liens

+

D’autres logiciels, quelque peu similaires, aucun endossement ou jugement ne devrait être tiré de ces liens :

+
    +
  • +
    Local Data Manager LDM comprend un réseau,

    et il souhaite fondamentalement échanger avec d’autres systèmes LDM. Ce paquet était +instructif, au début des années 2000, il y a eu un effort appelé NLDM qui mettait la +messagerie météorologique en couches sur un protocole TCP/IP standard. Cet effort est mort, cependant, +mais l’inspiration de garder le domaine (météo) séparé de la couche de transport (TCP/IP) +était une motivation importante pour MetPX.

    +
    +
    +
  • +
  • +
    Automatic File Distributor - du service météorologique allemand.

    Achemine les fichiers à l’aide du protocole de transport choisi par l’utilisateur. Philosophiquement proche de MetPX Sundew.

    +
    +
    +
  • +
  • Corobor - commutateur WMO commercial

  • +
  • Netsys -commutateur WMO commercial

  • +
  • IBLSoft -commutateur WMO commercial

  • +
  • Variété de moteurs de transferts: Standard Networks Move IT DMZ, Softlink B-HUB & FEST, +Globalscape EFT Server, Axway XFB, Primeur Spazio, Tumbleweed Secure File Transfer, Messageway.

  • +
  • Quantum à propos des sockets web HTML5. Une bonne discussion +des raisons pour lesquelles le push web traditionnel est horrible, montrant comment les sockets web +peuvent aider. AMQP est une solution de socket pure qui a les mêmes avantages que les +webockets pour l’efficacité. Note : la compagnie derrière KAAZING a écrit la pièce… pas désintéressé.

  • +
  • Rsync moteur de transfert.

  • +
  • Lsyncd moteur de transfert.

  • +
  • ZSync ( optimised rsync over HTTP. ) moteur de transfer.

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/AssurerLaLivraison.html b/fr/Explication/AssurerLaLivraison.html new file mode 100644 index 000000000..83d48eead --- /dev/null +++ b/fr/Explication/AssurerLaLivraison.html @@ -0,0 +1,307 @@ + + + + + + + Assurer la livraison (inflight) — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Assurer la livraison (inflight)

+

Le fait de ne pas établir correctement les protocoles de complétion de fichiers est +une source commune d’incohérences intermittentes, difficile de diagnostiquer. +Pour des transferts de fichiers fiables, Il est essentiel que l’expéditeur et +le destinataire s’entendent sur la façon de représenter un fichier qui n’est pas complet. +L’option inflight (c’est-à-dire qu’un fichier est en vol entre l’expéditeur et +le destinataire) s´offre pour accommoder différentes situations :

+
+

Tableau de Inflight

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Protocoles d’assurance de la livraison (par ordre de préférence)

Méthode

Description

Application

NONE

Fichier envoyé avec le bon nom +message`sr3_post(7) <sr3_post.7.rst>`_ +AMQP après que le transfert.

+
+
    +
  • moins d´aller-retours

  • +
  • plus efficace / vite

  • +
+
+

Envoyer à Sarracenia, et +publié quand le fichier est complet

+
+

(Meilleur quand disponible) +défaut pour sr_sarra. +défaut sur sr_subscribe et sender +quand post_broker est spécifié.

+
+

.tmp +(Suffixe)

avec un suffixe .tmp. +Lorsqu’il est complet, renommé au fin +Le suffixe réel est réglable.

+
+
+
-voyages aller-retour supplémentaires

pour renommer (un peu plus lent)

+
+
+
+

Envoi à la plupart des autres systèmes +(.tmp intégré) +Utiliser pour envoyer à Sundew.

+
+
(généralement un bon choix)
    +
  • défaut quand il n´y a pas de +post_broker

  • +
+
+
+

tmp/ +(subdir) +/repertoie

Fichier placés dans un sous-répertoire +ou répertoire. Déplacé au fin de +transfert +Même performance que Suffixe

Envoi à des systèmes qui n´acceptent +les suffixes

. +(Préfixe)

la convention Linux pour masquer les +fichiers. renommé au fin de transfert +Préfixer les noms par ‘.’ +Même performance que Suffixe

Envoi à des systèmes qui n´acceptent +les suffixes

entier +(mtime)

+

fileAgeMin

+

Âge minimum (temps de modification) +du fichier avant que le transfer soit +considéré Complèté. (fileAgeMin pareil)

+

Retarde tous les avis +Vulnérable aux pannes de réseau. +Vulnérable aux horloges en désaccord

+

Dernier choix, ne garantit un délai +que si aucun autre moyen peut servir

+

Réception de ceux qui ne coopèrent pas

+
+

(choix acceptable pour PDS)

+
+
+

Par défaut ( quand aucune option inflight n’est donnée), si le post_broker est défini, +alors une valeur de NONE est utilisée parce qu’on suppose qu’elle est livrée à un autre +courtier. S´il n´y a pas de post_broker est définie, la valeur de ‘.tmp’ est supposée être +la meilleure option.

+

NOTES :

+
+

Sur les versions de sr_sender antérieures à 2.18, la valeur par défaut était AUCUNE, mais +était documentée par ‘.tmp’’. Pour assurer la compatibilité avec les versions ultérieures, +il est probablement préférable d’écrire explicitement le réglage inflight.

+

inflight a été renommé de l’ancienne option lock en janvier 2017. Pour la compatibilité avec +les versions plus anciennes, peuvent utiliser lock, mais le nom est obsolète.

+

L’ancien logiciel PDS (qui précède MetPX Sundew) ne supporte que le FTP. Le protocole d’achèvement +utilisé par PDS était d’envoyer le fichier avec la permission 000 dans un premier temps, puis chmod à un fichier +fichier lisible. Ceci ne peut pas être implémenté avec le protocole SFTP, et n’est pas supporté du tout. +par Sarracenia.

+
+
+
+

Erreurs de configuration fréquentes

+

Réglage de NONE lors de l’envoi à Sundew.

+
+

Le réglage correct ici est ‘.tmp’. Sans cela, presque tous les fichiers passeront correctement, +mais les dossiers incomplets seront parfois ramassés par Sundew.

+
+

utilisant la méthode mtime pour recevoir de Sundew ou Sarracenia

+
+

L’utilisation de mtime est un dernier recours. Cette approche injecte du retard +et ne devrait être utilisée que lorsque qu´on n’a aucune influence +pour que l’autre extrémité du transfert utilise une meilleure méthode.

+

mtime est vulnérable aux systèmes dont les horloges diffèrent (fichiers incomplets). +mtime est vulnérable aux transferts lents, où les fichiers incomplets peuvent être +ramassés à cause d’un problème de réseautage interrompant ou retardant les transferts.

+
+

utilisant NONE lors de la livraison à une destination autre que Sarracenia

+
+

NONE doit être utilisé seulement lorsqu’il existe d’autres moyens de déterminer si un fichier +est livré. Par exemple, lors de l’envoi à une autre pompe, l’expéditeur informera +le destinataire le fichier est complet en publiant l´avis à ce courtier après +sa livraison, il n’y a donc aucun danger d’être ramassé trop tôt.

+

Lorsqu’il est mal-utilisé, il arrive que des fichiers incomplets soient traitée +par la réception.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Concepts.html b/fr/Explication/Concepts.html new file mode 100644 index 000000000..d64aee47f --- /dev/null +++ b/fr/Explication/Concepts.html @@ -0,0 +1,679 @@ + + + + + + + Concepts généraux de Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Concepts généraux de Sarracenia

+

Les pompes Sarracenia forment un réseau. Le réseau utilise +des courtiers amqp ( broker ) Message Queueing Protocol (MQP) +pour modéré les transferts de fichiers entre les pompes. On +envoie les avis de nouveau fichiers dans un sens et les rapports de succès ou +trouble dans la direction opposée. Les administrateurs configurent les chemins +d’accès à travers lesquels les données circulent. Chaque pompe agit de façon +indépendante, en gérant les activités des moteurs de transfert +qu’il peut atteindre, sans connaissance de l’ensemble du réseau. Les +emplacements de pompes et les directions du flux de circulation sont +choisis pour travailler avec les débits autorisés. Idéalement, aucune +exception de règle de pare-feu et nécessaire.

+

Sarracenia ne transporte pas de données. Il s’agit d’une couche de gestion pour +coordonner les activités de l’utilisation d´engins de transport. Donc, pour +obtenir une pompe fonctionnelle, les mécanismes de transport réels doivent +également être mis en place, le cas d’utilisation +le plus commun est RabbitMQ. +Les deux mécanismes actuelles sont le web et SFTP. +Dans le cas le plus simple, tous les composants se trouvent sur le site +Web du même serveur, mais cela n’est pas nécessaire. Le courtier pourrait +être sur un serveur différent de l´origine et la destination d’un transfert.

+

La meilleure façon d’effectuer des transferts de données est d’éviter les +sondages (examination récurrente de répertoires afin de détecter des +changements de fichiers.) C’est plus efficace si les rédacteurs peuvent +être amenés à émettre des messages d’annonce en format sr3_post appropriés. De même, +lors de la livraison, il est idéal si les destinataires utilisent +sr_subscribe, et un plugin on_file pour déclencher leur traitement ultérieur, +de sorte que le fichier est qui leur a été remis sans sondage. C’est la façon +la plus efficace de travailler, mais… il est entendu que pas tous les logiciels +ne seront coopératifs. Pour démarrer le flot en Sarracenia dans ces cas, +ca prend des outils de sondage: sr_poll (à distance), et sr3_watch (locale.)

+

D’une manière générale, Linux est la principale cible de déploiement et la +seule plate-forme sur laquelle les configurations de serveur sont déployées. +D’autres plates-formes sont utilisées en configuration client. Ceci +n´est pas une limitation, c’est juste ce qui est utilisé et testé. +Implémentations de la pompe sur Windows devrait fonctionner, ils ne +sont tout simplement pas testés.

+

Une pompe de données Sarracénia peut être implanté avec un seul serveur, ou bien une grappe, avec des rôles identiques ou spécialisés. +Voir Options de déploiement <ConsiderationDeployments.rst> pour plus de détails. À l´intérieur d´une pompe, les conceptes décrites +dans les sections suivantes s’appliquent.

+
+

L’algorithme de Flux

+

Tous les composants qui s’abonnent (subscribe, sarra, sarra, sender, shovel, winnow) +partagent un code substantiel et ne diffèrent que par leur reglages de défaut. +Chaque composant suit le même algorithme général, l’algorithme de Flux. +Les étapes de l’algorithme Flux sont les suivantes :

+
    +
  • Rassembler une liste de messages d’annonce

  • +
  • Filtrez-les avec des clauses d’accept/reject

  • +
  • Travailler sur les messages d’annonce acceptés

  • +
  • Afficher le travail accompli pour le prochain flux

  • +
+

En plus de detail:

+ + + + + + + + + + + + + + + + + + + +
Tableau 1 : Algorithme pour tous les composants

PHASE

DESCRIPTION

gather

Obtenez de l’information sur une liste initiale de fichiers

+

A partir: d’une fil d’attente, un répertoire, +un script de polling.

+

Sortie: worklist.incoming rempli de messages d’annonce.

+

Chaque message d’annonce est un dictionnaire python

+

Filter

Réduire la liste de fichiers sur lesquels agir.

+

Appliquer les clauses accept/reject.

+

callbacks after_accept +déplacer les messages d´annonce de worklist.incoming à +worklist.rejected

+

Ceux a éxécuter: flowcb/nodupe.py (suppresion des doublons.)

+

work

Traitez le message en le téléchargeant ou en l’envoyant.

+

exécuter le transfert (télécharger ou envoyer.)

+

Exécuter after_work

+

post

Publier l’annonce des téléchargements/envois de fichiers à +post_broker ou se débarrasser de la tache (à file/retry… )

+

Les principaux composants de l’implémentation python de Sarracenia implémentent +tous le même algorithme décrit ci-dessus. L’algorithme comporte différents endroits +où un traitement personnalisé peut être inséré (à l’aide de flowCallbacks) ou +dériver des classes de flux, d’intégrité ou de transfert.

+

Les composants ont juste des paramètres par défaut différents:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tableau 2 : Utilisation de l’algorithme de flux par chaque composant

Composant

Utilisation de l’algorithme

options clé

subscribe

+
+

Télécharger un +fichier d’une pompe

+

défaut mirror=False +(True pour tous les +autres)

+
+

Gather = gather.message

+

Filter

+

Work = Télécharger

+

Post = facultatif

+

flowMain subscribe

+

mirror off +(dans les autres

+
+

composants, c’est +mirror on )

+
+

download on

+

sarra

+

Utilisé sur des +pompes.

+

Télécharge un fichier

+

Publié un message par +la suite aux consom- +mateurs.

+

Abonnés de la pompe +locale puissent +télécharer à leur +tour

+

Gather = gather.message

+

Work = Télécharger

+

Post = publier

+

flowMain sarra

+

mirror on +download on

+

poll

+

Trouver des fichiers +sur d’autre serveurs +pour publier.

+

Utilise has_vip* +(voir ci-dessous)

+

Gather +if has_vip: poll

+

Filter

+
+
if has_vip:

Work = nil

+

Post = oui

+
+
+

flowMain poll

+

pollUrl

+

shovel

+

Déplacer les messages +de place.

+

Gather = gather.message

+

Filter (shovel cache=off)

+

Work = nil

+

Post = oui

+

acceptUnmatched True

+

nodupe_ttl 0 +download off

+

callback gather.message

+

callback post.message

+

winnow

+

Déplacer les messages +de place.

+

Suppression de +doublons à l’aide +du cache et d’un VIP +partagé

+

Gather = gather.message

+

Filter (shovel cache=on)

+

Work = nil

+

Post = oui

+

acceptUnmatched true

+

nodupe_ttl 300

+

callback gather.message

+

callback post.message

+

post/watch

+

Trouve un fichier sur +un serveur local +pour le publier

+

Gather = gather.file

+

Filter

+

Work = nil

+

Post = oui

+
+

Message?, File?

+
+

path /file/a/afficher

+

sleep -1 # pour post

+

sleep 5 # pour watch

+

callback gather.file

+

callback post.message

+

sender

+

Enovoyer des fichiers

+

Optionnel +Publie un message après +l’envoie

+

Gather = gather.message

+

Filter

+

Do = sendfile

+

Outlet = facultatif

+

flowMain sender

+

sendTo

+
+

Dans la colonne de gauche, on peut voir le nom et la description générale de chaque composant. +dans la colonne du milieu, on voit à quoi s’appliquent les différentes phases de l’algorithme Flow. +A droite, on voit comment exprimer, dans un fichier de configuration de flux générique, le composant. +La plupart des composants peuvent utiliser la classe de flux parent, mais ceux qui ont besoin +de plus utilise des sous-classes de flux. Ces cas sont configurés à l’aide de l’option flowMain.

+

Les composants sont facilement composés à l’aide de courtiers AMQP, qui créent +des réseaux élégants de communiquer des processus séquentiels. (CSP dans +le sens Hoare )

+
+
+

Multiple processes: Instances, Singletons and VIP

+

L’algorithme de flux ne se limite pas à un seul processus. De nombreux processus +peuvent exécuter la même configuration de flux. Pour les composants sarra, sender, shovel +et subscribe, on définit le paramètre instance au nombre de processus à exécuter +et à consommer à partir de la queue configurée qu’ils partagent.

+

Les composantes de poll, de post, et de watch, en revanche, sont limitées à un +processus unique sur un serveur donné. Dans le tableau ci-dessus, il y a une note +à propos de has_vip. Lorsque plusieurs serveurs participent à une +configuration, la directive vip peut être utilisée pour que les serveurs +coopérent pour fournir un service unique (où un seul serveur est +actif à la fois.) Pour la plupart des composants, la directive VIP dans un +fichier de configuration définit une adresse IP virtuelle qu’un serveur doit +activement avoir pour que ce composant soit actif. Si Sarracenia +détecte que l’interface n’est pas présente sur le serveur, le composant +s’exécutera en mode passif.

+

Pour presque tous les composants, le mode passif signifie qu’aucun traitement n’est effectué. +Le nœud vérifiera passivement s’il a obtenu le +Vip. Si le nœud n’a pas le vip, il restera en stand-by indéfiniment.

+

L’exception à cela est le sondage (poll), qui fonctionne différemment. Dans le sondage, +lorsque vous n’avez pas le vip, la boucle algorithmique suivante +continue:

+
    +
  • gather

  • +
  • filter

  • +
  • after_accept

  • +
+

Le gather (et/ou le sondage) du sondage s’abonne à l’échange ou d’autres +participants qui ont le vip publient et met à jour sa cache à partir des messages d´annonce, pour +éviter que les autres sondages interrogent le même point de terminaison pour la +même liste de fichiers.

+
+
+

Corréspondance des concepts AMQP avec Sarracenia

+

Une chose que l’on peut dire sans risque est qu’il faut comprendre un peu l’AMQP +pour travailler avec Sarracenia. L’AMQP est un sujet vaste et intéressant en +soi. On ne tente pas de toute expliquer ici. Cette section fournit juste +un peu de contexte, et introduit seulement les concepts de base nécessaires à la +compréhension et/ou à l’utilisation de la Sarracenia. Pour plus d’informations +sur l’AMQP lui-même, un ensemble de liens est maintenu à l’adresse suivante +le site web Metpx web site +mais un moteur de recherche révèlera aussi une richesse matérielle.

+../../_images/amqp_notion_de_flux.svg +

Un serveur AMQP s’appelle un courtier. Le mot Courtier est parfois utilisé pour +faire référence au logiciel, d’autres fois serveur exécutant le logiciel de +courtage (même confusion que serveur web). ci-dessus, le vocabulaire de +l’AMQP est en orange, et les termes de Sarracenia sont en bleu. Il y a +de nombreuses et différentes implémentations de logiciels de courtage. Nous +utilisons rabbitmq. Nous n’essayons pas d´être spécifique au rabbitmq, mais +les fonctions de gestion diffèrent d’une implémentation à l’autre.

+
+
Les Queues (files d´attentes) sont généralement prises en charge de manière transparente, mais vous avez besoin de connaître
    +
  • Un consommateur/abonné crée une fil d’attente pour recevoir des messages d’annonce.

  • +
  • Les files d’attente des consommateurs sont liées aux échanges (langage AMQP).

  • +
+
+
+

Un exchange est un entremeteur entre publisher et les files d´attentes du +consumer

+
+
    +
  • Un message d’annonce arrive d’une source de données.

  • +
  • l´avis passe à travers l’échange, est-ce que quelqu’un est intéressé par ce message d’annonce?

  • +
  • dans un échange basé sur un topic, le thème du message d’annonce fournit la clé d’échange.

  • +
  • intéressé : comparer la clé de message d’annonce aux liaison des queues de consommateurs.

  • +
  • le message d’annonce est acheminé vers les files d’attente des consommateurs intéressés, ou supprimé s’il n’y en a pas.

  • +
  • n’existe pas dans MQTT, utilisé comme racine de la hiérarchie des thèmes.

  • +
+
+
+
Plusieurs processus peuvent partager une queue, d´ou ils prélève les messages d’annonce à tour de rôle.
    +
  • Ceci est fortement utilisé pour sr_sarra et sr_subscribe multiples instances.

  • +
  • Le même concept est disponible en tant qu’abonnements partagés dans MQTT.

  • +
+
+
Comment décider si quelqu’un est intéressé.
    +
  • Pour la Sarracenia, nous utilisons (standard AMQP) échanges thématiques.

  • +
  • Les abonnés indiquent les thèmes qui les intéressent et le filtrage se fait côté serveur/courtier.

  • +
  • Les thèmes sont juste des mots-clés séparés par un point. wildcards : # correspond à n’importe quoi, * correspond à un mot.

  • +
  • Nous créons la hiérarchie des thèmes à partir du nom du chemin d’accès (mappage à la syntaxe AMQP).

  • +
  • La résolution et la syntaxe du filtrage des serveurs sont définies par l’AMQP. (. séparateur, # et * caractères génériques)

  • +
  • Le filtrage côté serveur est grossier, les messages d’annonce peuvent être filtrés après le téléchargement en utilisant regexp

  • +
+
+
+
+

AMQP v09 (Rabbitmq) Settings

+

MetPX-Sarracenia n’est qu’un léger revêtement autour des protocoles de +fil d’attente des messages. Pour ceux qui sont familiers avec +les protocoles sous-jacents, voici les mappages:

+
    +
  • Une pompe de données MetPX-Sarracenia est une application python AMQP qui utilise un (rabbitmq). +pour coordonner les transferts de données des clients SFTP et HTTP, et accompagne un +serveur web (apache) et serveur sftp (openssh), souvent sur la même adresse en face de l’utilisateur.

  • +
  • Une pompe de données MetPX-Sarracenia peut également fonctionner avec rabbitmq remplacé par +un courtier MQTT tel que mosquitto.org (mais certaines administrations doivent être gérées manuellement).

  • +
  • Dans la mesure du possible, nous utilisons leur terminologie et leur syntaxe. +Si quelqu’un connaît l’AMQP, il comprend. Si ce n’est pas le cas, ils peuvent faire des recherches.

    +
    +
      +
    • Les utilisateurs configurent un courtier, au lieu d’une pompe.

    • +
    • par convention, le serveur virtuel par défaut’/’ est toujours utilisé. (n’a pas encore ressenti le besoin d’utiliser d’autres serveurs virtuels)

    • +
    • les utilisateurs peuvent explicitement choisir leurs noms files d’attente.

    • +
    • les utilisateurs définissent subtopic,

    • +
    • les sujets avec séparateur de points sont transformés au minimum, plutôt qu’encodés.

    • +
    • La fil d’attente est définie sur durable afin que les messages ne soient pas perdus lors des redémarrages du broker.

    • +
    • nous utilisons des en-têtes de message (langage AMQP pour les paires clé-valeur) plutôt que d’encoder en JSON ou dans un autre format de charge utile.

    • +
    • expire combien de temps pour conserver une fil d’attente inactive ou un échange.

    • +
    +
    +
  • +
  • +
    réduire la complexité par le biais de conventions.
      +
    • n’utiliser qu’un seul type d’échanges (Topic), prendre soin des fixations.

    • +
    • +
      conventions de nommage pour les échanges et les files d’attente.
        +
      • les échanges commencent par x. +- xs_Weather - l’échange pour la source (utilisateur amqp) nommé Weather pour poster des messages. +- xpublic – central utilisé pour la plupart des abonnés.

      • +
      • les files d’attente commencent par q

      • +
      +
      +
      +
    • +
    +
    +
    +
  • +
+
+
+

MQTT (version =5) Paramètres

+

MQTT est en fait un meilleur match à Sarracenia que AMQP, car il est +entièrement basé sur des thèmes hiérarchiques. Alors que les sujets +ne sont qu’un parmi une variété de choix pour les méthodes de routage dans AMQP.

+
+
    +
  • Dans MQTT, le séparateur de thème est / au lieu de .

  • +
  • Le caractère générique de la rubrique MQTT # est le même que dans AMQP (correspond au reste du sujet)

  • +
  • Le caractère générique de la rubrique MQTT + est le même que l’AMQP * (correspond à un thème).

  • +
  • Un « Échange » AMQP est mappé à la racine de l’arborescence des thèmes MQTT,

  • +
  • Une « fil d’attente » AMQP est représentée dans MQTT par client-id et un abonnement partagé. Remarque : Les abonnements partagés ne sont présents que dans MQTTv5.

    +
      +
    • AMQP: Une fil d’attente nommée queuename est liée à un échange xpublic avec clé: v03.observations …

    • +
    • Abonnement MQTT: thème $shared/queuename/xpublic/v03/observations …

    • +
    +
  • +
  • Les connexions sont normalement clean_sesssion = 0, pour récupérer les messages lorsqu’une connexion est rompue.

  • +
  • MQTT QoS==1 est utilisé pour s’assurer que les messages sont envoyés au moins une fois et éviter les frais généraux +de ne s’assurer qu’une seule fois.

  • +
  • AMQP prefetch mappé à MQTT receiveMaximum

  • +
  • expire a la même signification en MQTT que dans AMQP.

  • +
+
+

MQTT v3 manque d’abonnements partagés et la logique de récupération est très différente. Sarracenia ne prend en charge que la v5.

+
+
+
+

Le flux à travers des Pompes

+../../_images/sr3_exemple_de_flux.svg +

Une description du flux conventionnel de messages par le biais d’échanges sur une pompe :

+
    +
  • Les abonnés se lient généralement à l’échange public pour obtenir le flux de données principal.

  • +
  • Un utilisateur user aura deux échanges :

    +
      +
    • xs_user l’échange où Alice poste ses notifications de fichiers et ses messages de rapports

    • +
    • xr_user l’échange où user lit ses messages de rapport

    • +
    • Remarque: les échanges « user » seront le nom d’utilisateur spécifié par l’utilisateur. Pas explicitement « xs_user » ou « xr_user ».

    • +
    +
  • +
  • Habituellement, le composant sarra lira à partir de xs_user, récupérera les données correspondant au +message post des utilisateurs, puis les rendra disponibles sur la pompe, en les ré-annonçant sur l’échange xpublic.

  • +
  • Les administrateurs auront accès à un échange xreport pour obtenir une surveillance à l’échelle du système. +L’utilisateur n’aura pas l’autorisation de le faire, il ne peut regarder que xr_user, qui aura les messages +de rapport spécifiques pour l’utilisateur uniquement.

  • +
+

Le but de ces conventions est d’encourager un moyen d’exploitation raisonnablement sûr. Si un message +provient d’xs_user, le processus de lecture est chargé de s’assurer qu’il est marqué comme provenant +de l’utilisateur sur ce cluster. Cela empêche certains types de spoofing d’identité, car les messages +ne peuvent être publiés que par les propriétaires appropriés.

+
+
+

Utilisateurs et rôles

+

Les noms d’utilisateur pour l’authentification des pompes sont significatifs +dans la mesure où ils sont visibles par tous. Ils sont utilisés dans le chemin +du répertoire sur les arbres publics, ainsi que pour authentifier le courtier. +Ils doivent être compréhensibles. Ils ont souvent une portée plus large qu’une +personne, peut-être les appeler “Comptes”. Il peut être élégant de configurer +les mêmes noms d’utilisateur pour une utilisation dans les moteurs de transport.

+

Tous les noms de compte doivent être uniques, mais rien n’évitera les conflits +lorsque les sources proviennent de différents réseaux de pompes, et des clients +à différentes destinations. Dans la pratique, les conflits de noms sont les +suivants adressée par routage pour éviter que deux sources différentes’ avec +le même nom aient leur nom. les offres de données combinées sur un seul arbre. +D’autre part, les conflits de noms ne sont pas toujours une erreur. +L’utilisation d’un nom de compte source commun sur différents clusters peut +être utilisée pour implémenter des dossiers qui sont partagés entre les deux +comptes portant le même nom.

+

Les utilisateurs de pompe sont définis avec l’option declare. Chaque option +commence avec l’option declare suivi du rôle spécifié, et enfin le nom +de l’utilisateur qui a ce rôle. rôle peut en être un de :

+
+
subscriber

Un subscriber ( abonné ) est un utilisateur qui ne peut s’abonner qu’aux messages de données et de rapport. Interdiction d’injecter des données. +Chaque abonné reçoit un xs_<user> named exchange sur la pompe, où si un utilisateur est nommé Acme, +l’échange correspondant sera xs_Acme. Cet échange est l’endroit où un sr_subscribe sr_subscribe +enverra ses messages de rapport.

+

Par convention/défaut, l’utilisateur anonyme est créé sur toutes les pompes pour permettre l’abonnement sans +un compte spécifique.

+
+
source

Un utilisateur autorisé à s’abonner ou à générer des données. Une source ne +représente pas nécessairement une personne ou un type de données, mais plutôt +une organisation responsable des données produites. Ainsi, si une organisation +recueille et met à disposition dix types de données avec un seul interlocuteur +email ou numéro de téléphone pour des questions sur les données et leur +disponibilité, alors tous les ces activités de recouvrement pourraient +utiliser un seul compte “source”.

+

Chaque source a un échange xs_<user> pour l’injection de messages de données, +et, similaire à un abonné, pour envoyer des messages de rapport sur le +traitement et la réception des données.

+

Chaque source est en mesure de visualiser tous les messages pour les données +qu’elle a injectées, mais l’endroit où tous ces messages sont disponibles +varie en fonction de la configuration de l’administrateur du routage des +rapports. Ainsi, une source peut injecter des données sur la pompe A, mais +peut s’abonner à des rapports sur une pompe différente. Les rapports +correspondant aux données que la source injectée est écrite en échange xr_<user>.

+

Lors de l´injection initiale des données, le chemin est modifié par Sarracenia +pour préparer une partie supérieure fixe de l’arborescence des répertoires. +Le premier niveau d’annuaire est le jour de l’ingestion dans le réseau en +format AAAAMMJJ. Le répertoire de deuxième niveau est le nom de la source. +Donc pour une utilisatrice Alice, s’injecter le 4 mai 2016, la racine de +l’arborescence du répertoire est : 20160504/Alice. Notez que tous les on +s’attend à ce que les pompes fonctionnent dans le fuseau horaire UTC.

+

Il y a des annuaires quotidiens parce qu’il y a une durée de vie à l’échelle +du système pour les données, elle est supprimée.

+

Puisque tous les clients verront les répertoires, et donc les configurations +des clients les incluront. il serait sage de considérer le nom du compte +public, et relativement statique.

+

Les sources déterminent qui peut accéder à leurs données, en spécifiant à +quelle grappe envoyer les données.

+
+
feeder

un utilisateur autorisé à s’abonner ou à générer des données, mais considéré +comme représentant une pompe. Cet utilisateur local de pompe serait utilisé +pour exécuter des processus tels que sarra, le routage des rapports report +avec des shovels, etc….

+
+
Admin

Un utilisateur autorisé à gérer la pompe locale. C’est le vrai rabbitmq-server +administrateur. L’administrateur exécute sr_audit pour créer/supprimer des échanges, +utilisateurs, ou nettoyer les files d’attente inutilisées, etc.

+
+
+

Exemple d’un fichier admin.conf valide complet, pour un hôte nommé blacklab

+
cluster blacklab
+admin amqps://hbic@blacklab/
+feeder  amqps://feeder@blacklab/
+declare source goldenlab
+declare subscriber anonymous
+
+
+

Un credentials.conf correspondant ressemblerait à:

+
amqps://hbic:hbicpw@blacklab/
+amqps://feeder:feederpw@blacklab/
+amqps://goldenlab:puppypw@blacklab/
+amqps://anonymous:anonymous@blacklab/
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/ConsiderationsDeployments.html b/fr/Explication/ConsiderationsDeployments.html new file mode 100644 index 000000000..c1f557be7 --- /dev/null +++ b/fr/Explication/ConsiderationsDeployments.html @@ -0,0 +1,466 @@ + + + + + + + Considérations relatives au déploiement — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Considérations relatives au déploiement

+

Les pompes de données Sarracenia sont souvent placées dans des réseaux de conceptions près des points de démarcation, pour fournir +un point de démarcation au niveau de l’application pour permettre l’analyse de sécurité et limiter la visibilité +dans différentes zones. Les pompes peuvent avoir tous les services incorporés sur un seul serveur, +ou généralement le broker principal est sur un nœud dédié, et les nœuds qui effectuent le transfert de données +sont appelés Moteurs de transport (Transport Engines). Dans les déploiements hautes performances, chaque moteur de transport +peut avoir un courtier local pour s’occuper des transferts et des transformations locales.

+
+

Engins de transport

+

Les engins de transport sont les serveurs de données interrogés par les abonnés, +par les utilisateurs finaux ou d’autres pompes. Les abonnés lisent les avis +et récupèrent les données correspondantes, en utilisant le protocole indiqué. +Le logiciel pour servir les données peut être SFTP ou HTTP (ou HTTPS). En +configurant les serveurs pour l’utilisation, veuillez consulter la documentation +des serveurs eux-mêmes. Notez également que les protocoles additionnels peuvent +être activés par l’utilisation des plugins do_ plugins, tels que décrit dans +le Guide de programmation.

+
+

IPv6

+

Une pompe d’échantillonnage a été implémentée sur un petit VPS avec IPv6 activé. +Un client lointain connecté au courtier rabbitmq en utilisant IPv6, et +l’abonnement au httpd apache httpd a travaillé sans problèmes. It just works. +Il n’y a pas de différence entre IPv4 et IPv6. Sarracenia est agnostiques +aux adresses IP.

+

On s’attend à utiliser des noms d’hôtes, puisque l’utilisation d’adresses IP +brisera le certificat. Utilisation pour la sécurisation de la couche de +transport (TLS, aka SSL) Pas de test des adresses IP dans les URLs (dans +l’une ou l’autre IP)) a été réalisée.

+
+
+
+

Plans de Pompes

+

Il existe de nombreux arrangements différents dans lesquels la Sarracenia peut +être utilisée.

+
+
Dataless

où l’on ne fait que de la Sarracenia en plus d’un courtier sans moteur de +transfert local. Ceci est utilisé, par exemple pour exécuter sr_winnow sur +un site pour fournir des sources de données redondantes.

+
+
Autonome

la plus évidente, exécuter toute la pile sur un seul serveur, openssh et +un serveur web ainsi que le courtier et Sarra lui-même. Réalise une pompe de +données complète, mais sans redondance.

+
+
Commutateurs/Routage

Où, afin d’atteindre des performances élevées, un cluster de nœuds autonomes +sont placés derrière les nœuds suivants un équilibreur de charge. +L’algorithme de l’équilibreur de charge n’est que round-robin, sans tentative +d’association d´une source donnée avec un noeud donné. Ceci a pour effet de +pomper différentes parties de fichiers volumineux à travers différents nœuds. +Ainsi on verra des parties de fichiers annoncés par une telle pompe, à être +réassemblés par les abonnés.

+
+
Diffusion des données

Lorsque, afin de servir un grand nombre de clients, plusieurs serveurs +identiques, chacun d’entre eux ayant un système d’exploitation complet, +miroitent des données

+
+
FIXME :

Ok, j’ai ouvert la grande gueule, il faut maintenant travailler sur les exemples.

+
+
+
+

Dataless ou S=0

+

Une configuration qui n’inclut que le courtier AMQP. Cette configuration peut +être utilisée lorsque les utilisateurs ont accès à de l’espace disque aux +deux extrémités et n’ont besoin que d’un médiateur. Voici la configuration +de hpfx.science.gc.ca, où l’espace disque HPC fournit l’espace de stockage +de sorte que la pompe ne pas besoin de pompes ou de pompes déployées pour +fournir une HA redondante aux centres de données distants.

+

… note:

+
FIXME : exemple de configuration des pelles, et sr_winnow (avec sortie
+vers xpublic) pour permettre dans le CPS pour obtenir des données de edm
+ou dor.
+
+
+

Notez que si une configuration peut être sans données, elle peut toujours +utiliser rabbitmq clustering pour les besoins de haute disponibilité +(voir ci-dessous).

+
+
+

Dataless vannée

+

Un autre exemple de pompe sans données serait de fournir une sélection de +produits à partir de deux pompes en amont en utilisant sr_winnow. Le sr_winnow +est alimenté par des pelles provenant de sources en amont. les clients locaux +se connectent simplement à cette pompe locale. sr_winnow prend le soin de ne +présenter que les produits du premier serveur à les rendres disponibles. On +configurerait sr_winnow pour la sortie vers l’échange xpublic sur la pompe.

+

Les abonnés locaux ne font que pointer vers la sortie de sr_winnow sur la +pompe locale. Ce est la manière dont les aliments sont mis en œuvre dans +les centres de prévision des intempéries de ECCC, où ils peut télécharger +des données à partir de n’importe quel centre national qui produit les +données en premier.

+
+
+

Dataless Avec Sr_poll

+

Le programme sr_poll peut sonder (vérifier si les produits sur un serveur +distant sont prêts ou modifiés.) Pour chaque produit, il émet une avis sur la +pompe locale. On pourrait utiliser sr_subscribe n’importe où, écoutez les +annonces et obtenez les produits (à condition que l’option avoir les +informations d’identification pour y accéder)

+
+
+

Autonome

+

Dans une configuration autonome, il n’y a qu’un seul nœud dans la configuration. +Il exécute tous les composants et n’en partage aucun avec d’autres nœuds. +Cela signifie le courtier et les services de données tels que sftp et sftp. +apache sont sur le seul nœud.

+

Une utilisation appropriée serait une petite installation d’acquisition de +données non 24x7, pour prendre la responsabilité des données. La mise en file +d’attente et la transmission en dehors de l’instrument. Il est redémarré +lorsque l’occasion se présente. Il s’agit simplement d’installer et de +configurer tout un moteur de flux de données, un courtier et le package. +sur un seul serveur. Les systèmes ddi sont généralement configurés de cette +façon.

+
+
+

Commutateurs/Routage

+

Dans la configuration de commutation/routage, il y a une paire de machines +qui font tourner un seul courtier pour un pool de moteurs de transfert. Ainsi, +chaque transfert engine´s vue de l’espace fichier est local, mais les files +d’attente sont les suivantes globale à la pompe.

+

Note : Sur de tels clusters, tous les nœuds qui exécutent un composant avec +l’option le même fichier de configuration crée par défaut un *queue +identique. Cibler les même courtier, il force la fil d’attente à être +partagée. S’il faut l’éviter, l’utilisateur peut écraser la valeur par +défaut queue_name en y rajoutant ${HOSTNAME}. Chaque nœud aura +sa propre fil d’attente, qui ne sera partagée que par les instances du nœud. +ex : nom_de_files d’attente q_${BROKER_USER}.${PROGRAM}.${CONFIG}.${HOSTNAME}. )

+

Souvent, il y a un trafic interne de données acquises avant qu’elles ne +soient finalement publiées. En tant que moyen de mise à l’échelle, souvent +les moteurs de transfert auront également des courtiers pour gérer le +trafic local, et ne publient les produits finaux qu´au coutier princiapal. +C’est ainsi que les systèmes ddsr sont généralement +configurés.

+
+
+
+

Considérations de sécurité

+

Cette section a pour but de donner un aperçu à ceux qui ont besoin d’effectuer un examen de sécurité. +de l’application avant la mise en œuvre.

+
+

Client

+

Toutes les informations d’identification utilisées par l’application sont stockées. +dans le fichier ~/.config/sr3/credentials.conf, et ce fichier est forcé à 600 permissions.

+
+
+

Serveur/courtier

+

L’authentification utilisée par les moteurs de transport est indépendante de celle utilisée pour les courtiers. Une sécurité +l’évaluation des courtiers rabbitmq et des différents moteurs de transfert en service est nécessaire pour évaluer +la sécurité globale d’un déploiement donné.

+

La méthode de transport la plus sûre est l’utilisation de SFTP avec des clés plutôt que des mots de passe. Sécurisé +le stockage des clés sftp est couvert dans la documentation de divers clients SSH ou SFTP. Les lettres de créance +ne fait que pointer vers ces fichiers clés.

+

Pour la Sarracenia elle-même, l’authentification par mot de passe est utilisée pour communiquer avec le courtier de l’AMQP, +donc l’implémentation du transport de socket crypté (SSL/TLS) sur tout le trafic des courtiers est très forte. +recommandé.

+

Les utilisateurs de Sarracenia sont en fait des utilisateurs définis sur des courtiers rabbitmq. +Chaque utilisateur Alice, sur un courtier auquel elle a accès :

+
+
    +
  • a un échange xs_Alice_Alice, où elle écrit ses messages et lit ses journaux.

  • +
  • a un échange xr_Alice xr_Alice, où elle lit ses messages de rapport.

  • +
  • peut configurer (lire et reconnaître) les files d’attente nommées qs_Alice_.* pour lier les échanges.

  • +
  • Alice peut créer et détruire ses propres files d’attente, mais pas celles des autres.

  • +
  • Alice ne peut écrire qu’à son échange (xs_Alice),

  • +
  • Les échanges sont gérés par l’administrateur, et non par n’importe quel utilisateur.

  • +
  • Alice ne peut poster que les données qu’elle publie (elle se référera à elle).

  • +
+
+

Alice ne peut pas créer d’échanges ou d’autres files d’attente qui ne figurent pas ci-dessus.

+

Rabbitmq fournit la granularité de la sécurité pour restreindre les noms de +mais pas leurs types. Ainsi, étant donné la possibilité de créer une fil d’attente nommée q_Alice, +une Alice malveillante pourrait créer un échange nommé q_Alice_xspecial, et ensuite configurer +Les files d’attente pour s’y lier, et établir un usage séparé du courtier non lié à la Sarracenia.

+

Pour éviter de telles utilisations abusives, sr_audit est un composant qui est +invoqué régulièrement à la recherche de mauvaise utilisation et de le nettoyer.

+
+
+

Validation des entrées

+

Les utilisateurs tels qu’Alice publient leurs messages sur leur propre échange +(xs_Alice). Les processus qui lisent à partir de les échanges d’utilisateurs +ont une responsabilité en matière de validation. Le processus qui lit xs_Alice +(probablement un sr_sarra) écrasera tout en-tête source ou cluster écrit +dans le message avec les valeurs correctes de le cluster courant, et +l’utilisateur qui a posté le message.

+

L’algorithme de la somme de contrôle utilisé doit également être validé. +L’algorithme doit être connu. De même, si il y a un en-tête malformé d’une +certaine sorte, il doit être rejeté immédiatement. Seuls les messages bien +formés doit être transmise pour traitement ultérieur.

+

Dans le cas de sr_sarra, la somme de contrôle est recalculée lors du +téléchargement des données s’assure qu’il correspond au message. S’ils ne +correspondent pas, un message d’erreur est publié. Si l’option +recompute_checksum est True, la somme de contrôle nouvellement calculée est +placée dans le message. Selon le niveau de confiance entre une paire de +pompes, le niveau de validation peut être détendue pour améliorer +les performances.

+

Une autre différence avec les connexions inter-pompes, c’est qu’une pompe +agit nécessairement comme un agent pour l’ensemble de la pompe sur les +pompes à distance et toutes les autres pompes pour lesquelles la pompe +est transférée. Dans ce cas, la validation est un peu différent:

+
+
    +
  • La source va varier. (les chargeurs peuvent représenter d’autres +utilisateurs, donc n’écrasez pas)

  • +
  • Il faut s’assurer que le cluster n’est pas un cluster local (car cela +indique soit une boucle, une mauvaise utilisation).

  • +
+
+

Si le message échoue le test de cluster non-local, il doit +être rejeté et enregistré (FIXME: publié? hmm? à clarifier)

+
+

Note

+
+
FIXME:
    +
  • if the source is not good, and the cluster is not good… cannot report back. so just log locally?

  • +
+
+
+
+
+
+

Accès au système privilégié

+

Les utilisateurs ordinaires (sources et abonnés) exploitent sarra dans le cadre +de leurs propres permissions sur le système, comme une commande scp. Le compte +administrateur de la pompe fonctionne également sous un compte utilisateur linux +normal et, exige des privilèges uniquement sur le courtier AMQP, mais rien sur +le système d’exploitation sous-jacent. Il est pratique d’accorder à +l’administrateur de la pompe les privilèges sudo pour la commande rabbitmqctl.

+

Il peut s’agir d’une seule tâche qui doit fonctionner avec des privilèges : +nettoyer la base de données, ce qui est une tâche facilement script vérifiable +qui doit être exécuté sur une base régulière. Si toute l’acquisition se fait +via sarra, alors tout ce qui suit les fichiers appartiendront à l’administrateur +de la pompe (la compte sarra), et un accès privilégié (root) n’est pas +nécessaire pour cela non plus.

+
+
+
+

Glossaire

+

La documentation sur la Sarracenia utilise un certain nombre de mots d’une +manière particulière. Ce glossaire devrait faciliter la compréhension du +reste de la documentation.

+
+
Source

Quelqu’un qui veut envoyer des données à quelqu’un d’autre. Pour ce faire, +ils font des avis pour annoncés des arbres de fichiers a copier du point +de départ vers une ou plusieurs pompes dans le réseau. Les sources +produisent des avis qui indiquent aux autres exactement où se +trouvent les fichier et comment les télécharger, et les +Sources disent où ils veulent que le fichier pour se rend.

+

Les sources utilisent des programmes comme sr_post.1, +sr_watch.1, et sr_poll(1) créer +leurs avis.

+
+
Abonnés

sont ceux qui examinent les annonces au sujet des fichiers disponibles ; et +téléchargent les fichiers qui les intéressent.

+

Les abonnés utilisent sr_subscribe(1)

+
+
Afficher, Avis, Notification, publication,

Ce sont des messages AMQP construits par sr_post, sr_poll, sr_poll, ou +sr3_watch pour laisser les utilisateurs savoir qu’un fichier particulier est +prêt. Le format de ces messages AMQP est le suivant décrit par la page manuel +sr_post(7) . Tous ces les mots sont utilisés de façon +interchangeable. Les avis à chaque étape préservent l´origine d’origine +du fichier, de sorte que les rapports de disposition puissent y être +réacheminés.

+
+
Rapports

Ce sont des messages AMQP (au format sr_post(7) format) +construits par les consommateurs de messages, pour indiquer ce qu’une pompe +ou un abonné donné a fait avec un fichier. Ils s’écoulent conceptuellement +dans la direction opposée de dans un réseau, pour revenir à la source.

+
+
Pompe ou courtier

Une pompe est un hôte exécutant Sarracenia, un serveur rabbitmq AMQP (appelé broker +en langage AMQP) La pompe a des utilisateurs administratifs et gère le courtier AMQP. +en tant que ressource dédiée. Une sorte de moteur de transport, comme un apache. +ou un serveur openssh, est utilisé pour supporter les transferts de fichiers. SFTP, et +HTTP/HTTPS sont les protocoles qui sont entièrement pris en charge par la Sarracenia. Pompes +copier des fichiers à partir de quelque part, et les écrire localement. Ils ont ensuite ré-annoncé l’initiative du +de la copie locale à ses pompes voisines, et aux abonnés utilisateurs finaux, ils peuvent +obtenir les données de cette pompe.

+
+
+
+

Note

+

Pour les utilisateurs finaux, une pompe et un courtier, c’est la même chose +à tout fins pratique. Mais, lorsque les administrateurs de pompes configurent +des clusters multi-hôtes, cependant, une pompe peut être exécuté sur deux +hôtes, et le même courtier pourrait être utilisé par de nombreux moteurs de +transport. La grappe entière serait considérée comme une pompe. Ainsi, le +deux mots n´ont pas toujours les même sens.

+
+
+
Pompes Dataless

Il y a des pompes qui n’ont pas de moteur de transport, elles servent de +médiateur des transferts pour d’autres serveurs, en mettant les messages +à la disposition des clients et des clients dans leur zone réseau.

+
+
Transferts Dataless

Parfois, les transferts à travers les pompes se font sans utiliser l’espace +local sur la pompe.

+
+
Réseau de pompage

Un certain nombre de serveurs d’interconnexion exécutant la pile sarracenia. +Chaque pile détermine la façon dont il achemine les articles vers le saut +suivant, de sorte que la taille ou l’étendue entière du réseau peut ne pas +être connu de ceux qui y mettent des données.

+
+
Cartes réseau

Chaque pompe devrait fournir une carte du réseau pour informer les +utilisateurs de la destination connue qu’ils devraient faire de la publicité +pour envoyer à. FIXME non défini jusqu’à présent.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Glossaire.html b/fr/Explication/Glossaire.html new file mode 100644 index 000000000..388d64a78 --- /dev/null +++ b/fr/Explication/Glossaire.html @@ -0,0 +1,331 @@ + + + + + + + Glossaire — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Glossaire

+

La documentation de Sarracenia utilise un certain nombre de mots d’une manière particulière. +Ce glossaire devrait faciliter la compréhension du reste de la documentation.

+
+

AMQP

+

AMQP est le Advanced Message Queuing Protocol, qui a émergé dans l’industrie du trading financier et a progressivement +mûri. Les implémentations sont apparues pour la première fois en 2007, et il existe maintenant plusieurs implémentations open source. L’implémentation AMQP +n’est pas la plomberie JMS. JMS standardise l’API utilisée par les programmeurs, mais pas le protocole on-the-wire. Ainsi +en règle générale, on ne peut pas échanger de messages entre des personnes utilisant différents fournisseurs JMS. L’AMQP normalise +l’interopérabilité, et fonctionne efficacement comme une cale d’interopérabilité pour JMS, sans être +limité à Java. AMQP est neutre sur le plan linguistique et neutre sur le plan du message. Il existe de nombreux déploiements utilisant +Python, C++ et Ruby. On pourrait adapter très facilement les protocoles WMO-GTS pour fonctionner sur AMQP. +Les fournisseurs JMS sont très orientés Java.

+
    +
  • www.amqp.org - Définir AMQP

  • +
  • www.openamq.org - Implémentation originale GPL implementation de JPMorganChase

  • +
  • www.rabbitmq.com - Une autre implementation gratuite. Celle qu’on utilise et qui nous convient

  • +
  • Apache Qpid - Une autre implémentation gratuite

  • +
  • Apache ActiveMQ - C’est un fournisseur JMS avec un pont AMQP. Ils préfèrent leur propre protocole openwire.

  • +
+

Sarracenia s’appuie fortement sur l’utilisation de courtiers et d’échanges thématiques, qui occupaient une place importante +dans les efforts de normalisation de l’AMQP avant la version 1.0, ou ils ont été supprimés. On espère que ces concepts seront réintroduits à un moment donné. Jusqu’à +cette fois-là, l’application s’appuiera sur des agents de messages standard antérieurs à la version 1.0, tels que rabbitmq.

+
+
+

Contre-Pression (anglais: Back Pressure)

+

Lorsqu’un nœud de pompage de données connaît une latence élevée, il est préférable de ne pas importer plus de données +à un rythme élevé et aggraver la surcharge. Au lieu de cela, il faut s’abstenir d’accepter des messages +à partir du nœud afin que ceux en amont maintiennent des files d’attente, et d’autres nœuds moins occupés peuvent prendre +plus de la charge. La lenteur du traitement affecte l’ingestion de nouveaux messages en est un exemple +d’appliquer une contre-pression à un flux de transfert.

+

Exemple d’absence de contre-pression : la bibliothèque paho-python-mqtt v3 a actuellement des accusés de réception +intégré à la bibliothèque, de sorte que l’accusé de réception se produit sans le contrôle de l’utilisateur, et il y a +aucun nombre maximal de messages pouvant se trouver dans la bibliothèque, avant que l’application ne les +voie. Si l’application s’arrête, tous ces messages, encore invisibles par l’application, +sont perdus. Dans MQTT v5, il existe un paramètre receiveMaximum qui limite au moins le nombre +des messages que la bibliothèque mettra en fil d’attente pour l’application, mais idéalement, +la bibliothèque python obtiendrait des accusés de réception contrôlés par l’application, comme la bibliothèque Java l’a déjà fait.

+
+
+

Pompes sans Données (anglais: Dataless Pumps)

+

Il y a des pompes qui n’ont pas de moteur de transport, elles ne font que servir de médiateur +entre transferts pour d’autres serveurs, en mettant les messages à la disposition des clients et +de serveurs dans leur zone de réseau.

+
+
+

Transferts sans Données (Dataless Transfers)

+

Parfois, les transferts à travers les pompes se font sans utiliser d’espace local sur la pompe.

+
+
+

Latence (anglais: Latency)

+

Temps écoulé entre l’insertion des données dans un réseau (l’heure à laquelle le message relatif à un fichier est publié pour la première fois) +au moment où il est mis à disposition sur un point final. Nous voulons minimiser la latence dans les transferts, +et une latence élevée peut indiquer des problèmes de configuration ou de capacité.

+
+
+

MQTT

+

Le Message Queue Telemetry Transport (MQTT) version 5 est un deuxième protocole de fil d’attente de messages avec toutes les fonctionnalités +nécessaire pour soutenir les modèles d’échange de données de sarracenia.

+ +
+
+

Cartes Réseau (anglais: Network Maps)

+

Chaque pompe doit fournir une carte du réseau pour informer les utilisateurs de la destination connue +qu’ils devraient faire de la publicité pour envoyer à. FIXME non défini jusqu’à présent.

+
+
+

Poste, Notice, Notification, Avis, Annonce

+

Il s’agit de messages AMQP créés par post, poll ou watch pour permettre aux utilisateurs +de savoir qu’un fichier particulier est prêt. Le format de ces messages AMQP est +décrit par la page de manuel sr_post(7). Tous ces +mots sont utilisés de manière interchangeable. Les annonces à chaque étape préservent la +source d’origine de la publication, afin que les messages de rapport puissent être réacheminés +à la source.

+
+
+

Pompe

+

Une pompe est un hôte exécutant Sarracenia, soit un serveur AMQP rabbitmq, soit un MQTT +comme mosquitto. Le middleware de mise en fil d’attente des messages s’appelle un broker. +La pompe a des utilisateurs administratifs et gère le courtier MQP +en tant que ressource dédiée. Une sorte de moteur de transport, comme un serveur apache, +ou un serveur openssh est utilisé pour prendre en charge les transferts de fichiers. SFTP, et +HTTP / HTTPS sont les protocoles qui sont entièrement pris en charge par sarracenia. Les pompes +copient des fichiers de quelque part et les écrit localement. Ils font ensuite de la publicité à nouveau pour la +copie locale à ses pompes voisines, et les utilisateurs qui sont abonnés peuvent +obtenir les données de cette pompe.

+
+

Note

+

Pour les utilisateurs finaux, une pompe et un courtier sont la même chose pour toutes les buts pratiques. +Toutefois, lorsque les administrateurs de pompe configurent des clusters multi-hôtes, un +Broker peut s’exécuter sur deux hôtes, et le même broker peut être utilisé par +de nombreux moteurs de transport. L’ensemble du cluster serait considéré comme une pompe. Ainsi, +deux mots ne sont pas toujours les mêmes.

+
+
+
+

Réseau de Pompage (anglais: Pumping Network)

+

Un certain nombre de serveurs d’interconnexion exécutant la stack sarracenia. Chaque stack +détermine la façon dont il achemine les éléments vers le suivant, de sorte que la taille ou l’étendue entière +du réseau peut ne pas être connu à ceux qui y mettent des données.

+
+
+

Messages de Rapport (anglais: Report messages)

+

Il s’agit de messages AMQP (dans le format sr_post(7)), avec le champ _report_ +inclus) construit par les consommateurs de messages, pour indiquer ce qu’une pompe de données +ou l’abonné a décidé de faire avec un message. Ils s’écoulent conceptuellement dans le +direction opposée des notifications dans un réseau, pour revenir à la source. +Dans les documents de la phase de conception originale de 2015, les rapports étaient appelés log messages. +Cela a été modifié pour réduire la confusion avec les données dans les fichiers journaux de l’application.

+
+
+

Source

+

Quelqu’un qui veut envoyer des données à quelqu’un d’autre. Ils le font en annonçant un +arborescences de fichiers copiés du point de départ vers une ou plusieurs pompes +dans le réseau. Les sources publicitaires produites indiquent aux autres exactement où +et comment télécharger les fichiers, et les sources doivent dire où elles veulent les +données auxquelles accéder.

+

Les sources utilisent le post, +sr_watch.1, et +sr_poll(1) les composants à créer +leurs publicités.

+
+
+

Abonnés (anglais: Subscribers)

+

sont ceux qui examinent les publicités sur les fichiers disponibles, et +téléchargent les fichiers qui les intéressent.

+

Les abonnés utilisent subscribe(1)

+
+
+

Sundew

+

MetPX Sundew est l’ancêtre de Sarracenia. +Il s’agit d’une pompe de données orientée TCP/IP WMO 386 pure. Les fichiers de configuration se ressemblent, +mais les algorithmes et les concepts de routage sont très différents. MetPX est une push-only +méthode de distribution de fichiers, qui a implémenté des sockets WMO 386, des sockets AM et +d’autres technologies obsolètes. Il ne fait pas pub / sub. +Plus d’histoire here

+
+
+

WMO

+

L’Organisation météorologique mondiale, est une partie des Nations Unies qui fait de la surveillance, prévision et alerte +environnementale et de temps de chaque pays en tant que membres. Depuis de nombreuses décennies, il y a +un échange en temps réel de données météorologiques entre les pays, souvent même en temps de guerre. Les normes +qui couvrent ces échanges sont :

+
    +
  • Manuel sur le système mondial des télécommunications: Manuel 386 de WMO. La référence standard pour ce +domaine. (une copie probablement périmée est ici.) Essayez https://www.wmo.int pour la dernière version.

  • +
+

Habituellement, ces liens sont appelés collectivement les CGV. Les normes sont très anciennes, et une modernisation +de processus est en cours depuis une dizaine d’années ou deux. Certains travaux en cours sur le remplacement de GTS sont ici:

+ +

Les discussions autour de ce sujet sont des moteurs importants pour Sarracenia.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/GuideLigneDeCommande.html b/fr/Explication/GuideLigneDeCommande.html new file mode 100644 index 000000000..b76f75786 --- /dev/null +++ b/fr/Explication/GuideLigneDeCommande.html @@ -0,0 +1,2601 @@ + + + + + + + Guide De Ligne De Commande — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Guide De Ligne De Commande

+
+

SR3 - Tout

+

sr3 est un outil de ligne de commande pour gérer les configurations +Sarracenia individuellement ou en groupe. Pour l’utilisateur actuel, +il lit sur tous les fichiers de configuration, des fichiers d’état et consulte la table de processus pour déterminer +l’état de tous les composants. Il effectue ensuite la modification demandée.

+
+

sr3 options action [ composant/configuration … ]

+
+

Les composants sr3 sont utilisés pour publier et télécharger des fichiers à partir de sites Web ou de serveurs de fichiers +qui fournissent des sr3_post(7). Ces sites +publient des messages pour chaque fichier dès qu’il est disponible. Les clients se connectent à un +broker (souvent le même que le serveur) et s’abonnent aux notifications. +Les notifications sr3_post fournissent de véritables notifications push pour les dossiers accessibles sur le Web (WAF), +et sont beaucoup plus efficaces que l’interrogation périodique des répertoires ou le style ATOM/RSS +de notifications. Sr_subscribe peut être configuré pour publier des messages après leur téléchargement, +les mettre à la disposition des consommateurs pour un traitement ultérieur ou des transferts.

+

sr3 peut également être utilisé à des fins autres que le téléchargement (par exemple, pour +fourniture à un programme externe) en spécifiant le -n (égal à : download off) +supprimer le comportement de téléchargement et publier uniquement l’URL sur la sortie standard. La +sortie standard peut être redirigée vers d’autres processus dans le style de filtre de texte UNIX classique.

+

Les composants de sarracenia sont des groupes de valeurs choisi par défaut sur l’algorithme principal, +pour réduire la taille des composants individuels. Les composants sont les suivants :

+
+
    +
  • cpump - copier des messages d’une pompe a une autre (une implémentation C d’un shovel.)

  • +
  • flow - flux générique sans comportement par défaut. Bonne base pour créer un composant défini par l’utilisateur

  • +
  • poll - interroger un serveur Web ou de fichiers non sarracenia pour créer des messages à traiter.

  • +
  • post & watch - créer des messages pour les fichiers à traiter.

  • +
  • sarra - télécharger le fichier d’un serveur distant vers le serveur local et les republier pour d’autres.

  • +
  • sender - envoyer des fichiers d’un serveur local à un serveur distant.

  • +
  • shovel - copier des messages, uniquement, pas des fichiers.

  • +
  • watch - créer des messages pour chaque nouveau fichier qui arrive dans un répertoire ou à un chemin défini.

  • +
  • winnow - copier des messages, en supprimant les doublons.

  • +
+
+

Tous ces composants acceptent les mêmes options, avec les mêmes effets. +Il existe également des sr3_cpump(1). qui est une version C qui implémente un +sous-ensemble des options ici, mais là où elles sont implémentées, elles ont le même effet.

+

La commande sr3 prend généralement deux arguments : une action suivie d’une liste +de fichiers de configuration. Lorsqu’un composant est appelé, une opération et un +fichier de configuration sont spécifiés. Si la configuration est omise, cela signifie que +l’action s’applique à toutes les configurations. L’action est l’une des suivantes :

+
+
    +
  • foreground: exécuter une seule instance au premier plan, écrivant le journal à l´erreur standard.

  • +
  • restart: arrêter puis démarrer la configuration.

  • +
  • sanity: recherche les instances qui se sont plantées ou ont bloqué et les redémarre.

  • +
  • start: démarrer la configuration

  • +
  • status: vérifier si la configuration est en cours d’exécution.

  • +
  • stop: arrêter la configuration.

  • +
+
+

Les actions restantes gèrent les ressources (échanges, files d’attente) utilisées par le composant sur +le courtier ou pour gérer les configurations.

+
+
    +
  • cleanup: supprime les ressources du composant sur le serveur

  • +
  • declare: crée les ressources du composant sur le serveur.

  • +
  • add: copie une configuration à la liste des configurations disponibles.

  • +
  • list: Énumérer toutes les configurations disponibles.

  • +
  • list plugins: Énumérer toutes les plugins disponibles.

  • +
  • list examples: Énumérer toutes les exemples disponibles.

  • +
  • show voir une version interpreté d’un fichier de configuration.

  • +
  • edit: modifier une configuration existante.

  • +
  • remove: Supprimer une configuration

  • +
  • disable: marquer une configuration comme non éligible à l’exécution.

  • +
  • enable: marquer une configuration comme éligible à l’exécution.

  • +
  • convert: convertir une configuration de la version2 à la version3

  • +
+
+

Par exemple: sr_subscribe foreground dd exécute une instance du composant sr_subscribe en avant plan +en se servant de la configuration dd.

+

L’action foreground est utilisée lors de la construction d’une +configuration ou pour le débogage. L’instance foreground sera exécutée +indépendamment des autres instances qui sont en cours d’exécution. +Si des instances sont en cours d’exécution, il partage la même fil d’attente +d’avis avec eux. Un utilisateur arrête l’instance foreground en +utilisant simplement <ctrl-c> sur linux. ou utilise d’autres moyens pour tuer le processus.

+

Une fois qu’une configuration a été affinée, start lance le composant en tant que service d’arrière-plan +(démon ou flotte de démons dont le numéro est contrôlé par l’option instances). +Si plusieurs configurations et composants doivent être exécutés ensemble, l’ensemble de la flotte +peut être contrôlé de la même manière à l’aide de la commande sr3(1)..

+

Pour que les composants roulent tous en meme temps,sur Linux on peut utiliser l’intégration +systemd , comme décrit dans +Admin Guide . Sur Windows, il est possible de configurer un service, +comme décrit dans Windows user manual

+

Les actions cleanup, declare, peuvent être utilisées pour gérer les +ressources sur le courtier rabbitmq. Les ressources sont soit des files d’attente, +soit des échanges. declare crée les ressources.

+

Les actions add, remove, list, edit, enable & disable sont utilisées pour gérer la liste +de configurations et plugins. On peut voir toutes les configurations disponibles en utilisant l´action list. +et les plugins disponibles avec list plugins. +En utilisant l’option edit, on peut travailler sur une configuration particulière. +Une configuration disabled ne sera pas démarrée ou redémarrée par les actions start +ou restart. Cela peut être utilisé pour mettre une configuration temporairement de côté.

+

L’option convert est utilisé pour traduire une configuration écrite avec des options de la version2 +de Sarracenia, avec des options de la version3. Le fichier de configuration de la version2 doit etre +placé dans le réportoire ~/.config/sarra/composant/v2_config.conf et la version traduite sera placé +dans le répertoire ~/.config/sr3/composant/v3_config.conf. Pas exemple, cette action serais invoqué avec +sr3 convert composant/config.

+
+

ACTIONS

+
+

declare

+

Appeler la fonction correspondante pour chacune des configurations:

+
$ sr3 declare
+  declare: 2020-09-06 23:22:18,043 [INFO] root declare looking at cpost/pelle_dd1_f04
+  2020-09-06 23:22:18,048 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan00 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,049 [INFO] sarra.moth.amqp __putSetup exchange declared: xcvan01 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,049 [INFO] root declare looking at cpost/veille_f34
+  2020-09-06 23:22:18,053 [INFO] sarra.moth.amqp __putSetup exchange declared: xcpublic (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,053 [INFO] root declare looking at cpost/pelle_dd2_f05
+  ...
+  2020-09-06 23:22:18,106 [INFO] root declare looking at cpost/pelle_dd2_f05
+  2020-09-06 23:22:18,106 [INFO] root declare looking at cpump/xvan_f14
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f14.23011811.49631644 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup um..: pfx: v03, exchange: xcvan00, values: #
+  2020-09-06 23:22:18,110 [INFO] sarra.moth.amqp __getSetup binding q_tfeed.sr_cpump.xvan_f14.23011811.49631644 with v03.# to xcvan00 (as: amqp://tfeed@localhost/)
+  2020-09-06 23:22:18,111 [INFO] root declare looking at cpump/xvan_f15
+  2020-09-06 23:22:18,115 [INFO] sarra.moth.amqp __getSetup queue declared q_tfeed.sr_cpump.xvan_f15.50074940.98161482 (as: amqp://tfeed@localhost/)
+
+
+

Déclare les files d’attente et les échanges liés à chaque configuration. +On peut également l’appeler avec --users, afin qu’il déclare les utilisateurs ainsi que les échanges et les files d’attente:

+
$ sr3 --users declare
+  2020-09-06 23:28:56,211 [INFO] sarra.rabbitmq_admin add_user permission user 'ender' role source  configure='^q_ender.*|^xs_ender.*' write='^q_ender.*|^xs_ender.*' read='^q_ender.*|^x[lrs]_ender.*|^x.*public$'
+  ...
+
+
+

La fourniture d’un ou de plusieurs flux ne déclarera que les utilisateurs spécifiés dans le(s) flux:

+
$ sr3 --users declare subscribe/dd_amis
+  ...
+  declare: 2024-05-17 20:02:18,548 434920 [INFO] sarracenia.rabbitmq_admin add_user permission user 'tfeed@localhost' role feeder  configure=.* write=.* read=.*
+  ...
+
+
+
+
+

dump

+

imprimer les trois structures de données utilisées par sr. Il existe trois listes :

+
    +
  • processus considérés comme liés à sr.

  • +
  • configurations présentes

  • +
  • contenu des fichiers d’état.

  • +
+

dump est utilisé pour le débogage ou pour obtenir plus de détails que ce qui est fourni par status:

+
Running Processes
+     4238: name:sr_poll.py cmdline:['/usr/bin/python3', '/home/peter/src/sarracenia/sarra/sr_poll.py', '--no', '1', 'start', 'pulse']
+     .
+     .
+     .
+Configs
+   cpost
+       veille_f34 : {'status': 'running', 'instances': 1}
+
+States
+   cpost
+       veille_f34 : {'instance_pids': {1: 4251}, 'queue_name': None, 'instances_expected': 0, 'has_state': False, 'missing_instances': []}
+
+Missing
+
+
+

C’est assez long, et donc un peu trop d’informations à regarder à l’état brut. +Généralement utilisé en conjonction avec des filtres Linux, tels que grep. +par exemple:

+
$ sr3 dump  | grep stopped
+  WMO_mesh_post : {'status': 'stopped', 'instances': 0}
+  shim_f63 : {'status': 'stopped', 'instances': 0}
+  test2_f61 : {'status': 'stopped', 'instances': 0}
+
+$ sr3 dump  | grep disabled
+  amqp_f30.conf : {'status': 'disabled', 'instances': 5}
+
+
+

fournit une méthode simple pour déterminer quelles configurations sont dans un état particulier. +Autre exemple, si sr3 status signale que l’expéditeur/tsource2send_f50 est partiel, alors +on peut utiliser dump pour obtenir plus de détails:

+
$ sr3 dump | grep sender/tsource2send_f50
+  49308: name:sr3_sender.py cmdline:['/usr/bin/python3', '/usr/lib/python3/dist-packages/sarracenia/instance.py', '--no', '1', 'start', 'sender/tsource2send_f50']
+  q_tsource.sr_sender.tsource2send_f50.58710892.12372870: ['sender/tsource2send_f50']
+
+
+
+
+

foreground

+

exécuter une seule instance d’une configuration unique en tant que processus interactif de journalisation à la sortie stderr/terminal actuelle. +pour le débogage.

+

list

+

montre à l’utilisateur les fichiers de configuration présents

+
$ sr3 list
+  User Configurations: (from: /home/peter/.config/sarra )
+  cpost/pelle_dd1_f04.conf         cpost/pelle_dd2_f05.conf         cpost/veille_f34.conf
+  cpump/xvan_f14.conf              cpump/xvan_f15.conf              poll/f62.conf
+  post/shim_f63.conf               post/t_dd1_f00.conf              post/t_dd2_f00.conf
+  post/test2_f61.conf              sarra/download_f20.conf          sender/tsource2send_f50.conf
+  shovel/rabbitmqtt_f22.conf       subscribe/amqp_f30.conf          subscribe/cclean_f91.conf
+  subscribe/cdnld_f21.conf         subscribe/cfile_f44.conf         subscribe/cp_f61.conf
+  subscribe/ftp_f70.conf           subscribe/q_f71.conf             subscribe/rabbitmqtt_f31.conf
+  subscribe/u_sftp_f60.conf        watch/f40.conf                   admin.conf
+  credentials.conf                 default.conf
+  logs are in: /home/peter/.cache/sarra/log
+
+
+

La dernière ligne indique dans quel répertoire se trouvent les fichiers journaux.

+

list examples montre également les modèles de configuration inclus disponibles comme points de départ avec l’action add

+
$ sr3 list examples
+  Sample Configurations: (from: /home/peter/Sarracenia/development/sarra/examples )
+  cpump/cno_trouble_f00.inc        poll/aws-nexrad.conf             poll/pollingest.conf
+  poll/pollnoaa.conf               poll/pollsoapshc.conf            poll/pollusgs.conf
+  poll/pulse.conf                  post/WMO_mesh_post.conf          sarra/wmo_mesh.conf
+  sender/ec2collab.conf            sender/pitcher_push.conf         shovel/no_trouble_f00.inc
+  subscribe/WMO_Sketch_2mqtt.conf  subscribe/WMO_Sketch_2v3.conf    subscribe/WMO_mesh_CMC.conf
+  subscribe/WMO_mesh_Peer.conf     subscribe/aws-nexrad.conf        subscribe/dd_2mqtt.conf
+  subscribe/dd_all.conf            subscribe/dd_amis.conf           subscribe/dd_aqhi.conf
+  subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf       subscribe/dd_cmml.conf
+  subscribe/dd_gdps.conf           subscribe/dd_ping.conf           subscribe/dd_radar.conf
+  subscribe/dd_rdps.conf           subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+  subscribe/ddc_normal.conf        subscribe/downloademail.conf     subscribe/ec_ninjo-a.conf
+  subscribe/hpfx_amis.conf         subscribe/local_sub.conf         subscribe/pitcher_pull.conf
+  subscribe/sci2ec.conf            subscribe/subnoaa.conf           subscribe/subsoapshc.conf
+  subscribe/subusgs.conf           watch/master.conf                watch/pitcher_client.conf
+  watch/pitcher_server.conf        watch/sci2ec.conf
+
+
+$ sr3 add dd_all.conf
+  add: 2021-01-24 18:04:57,018 [INFO] sarracenia.sr add copying: /usr/lib/python3/dist-packages/sarracenia/examples/subscribe/dd_all.conf to /home/peter/.config/sr3/subscribe/dd_all.conf
+$ sr3 edit dd_all.conf
+
+
+

Les actions add, remove, list, edit, enable & disable sont utilisées pour gérer la liste +des configurations. On peut voir toutes les configurations disponibles en utilisant l’action list. +Pour afficher les plugins disponibles, utilisez list plugins. À l’aide de l’option edit, +on peut travailler sur une configuration particulière. Un disabled met une configuration de côté +(en ajoutant .off au nom) afin qu’elle ne soit pas démarrée ou redémarrée par +les actions start, foreground ou restart.

+
+
+

show

+

Afficher tous les paramètres de configuration (le résultat de toutes les analyses… ce que les composants du flux voient réellement)

+
% sr3 show subscribe/q_f71
+2022-03-20 15:30:32,507 1084652 [INFO] sarracenia.config parse_fil download_f20.conf:35 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept"
+2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file tsource2send_f50.conf:26 obsolete v2:"on_message msg_rawlog" converted to sr3:"logEvents after_accept"
+2022-03-20 15:30:32,508 1084652 [INFO] sarracenia.config parse_file rabbitmqtt_f22.conf:6 obsolete v2:"on_message msg_log" converted to sr3:"logEvents after_accept"
+
+Config of subscribe/q_f71:
+{'_Config__admin': 'amqp://bunnymaster@localhost/ None True True False False None None',
+ '_Config__broker': 'amqp://tsource@localhost/ None True True False False None None',
+ '_Config__post_broker': None,
+ 'accelThreshold': 0,
+ 'acceptSizeWrong': False,
+ 'acceptUnmatched': False,
+ 'admin': 'amqp://bunnymaster@localhost/ None True True False False None None',
+ 'attempts': 3,
+ 'auto_delete': False,
+ 'baseDir': None,
+ 'baseUrl_relPath': False,
+ 'batch': 1,
+ 'bindings': [('xs_tsource_poll', ['v03', 'post'], ['#'])],
+ 'broker': 'amqp://tsource@localhost/ None True True False False None None',
+ 'bufsize': 1048576,
+ 'byteRateMax': None,
+ 'cfg_run_dir': '/home/peter/.cache/sr3/subscribe/q_f71',
+ 'component': 'subscribe',
+ 'config': 'q_f71',
+ 'currentDir': None,
+ 'debug': False,
+ 'declared_exchanges': [],
+ 'declared_users': {'anonymous': 'subscriber', 'eggmeister': 'subscriber', 'ender': 'source', 'tfeed': 'feeder', 'tsource': 'source', 'tsub': 'subscriber'},
+ 'delete': False,
+ 'destfn_script': None,
+ 'directory': '//home/peter/sarra_devdocroot/recd_by_srpoll_test1',
+ 'discard': False,
+ 'documentRoot': None,
+ 'download': True,
+ 'durable': True,
+ 'env_declared': ['FLOWBROKER', 'MQP', 'SFTPUSER', 'TESTDOCROOT'],
+ 'exchange': 'xs_tsource_poll',
+ 'exchangeDeclare': True,
+ 'exchangeSuffix': 'poll',
+ 'expire': 1800.0,
+ 'feeder': ParseResult(scheme='amqp', netloc='tfeed@localhost', path='/', params='', query='', fragment=''),
+ 'fileEvents': {'create', 'link', 'modify', 'delete', 'mkdir', 'rmdir' },
+ 'file_total_interval': '0',
+ 'filename': 'WHATFN',
+ 'fixed_headers': {},
+ 'flatten': '/',
+ 'hostdir': 'fractal',
+ 'hostname': 'fractal',
+ 'housekeeping': 300,
+ 'imports': [],
+ 'inflight': None,
+ 'inline': False,
+ 'inlineByteMax': 4096,
+ 'inlineEncoding': 'guess',
+ 'inlineOnly': False,
+ 'instances': 1,
+ 'identity_arbitrary_value': None,
+ 'identity_method': 'sha512',
+ 'logEvents': {'after_work', 'after_accept', 'on_housekeeping'},
+ 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s',
+ 'logLevel': 'info',
+ 'logReject': False,
+ 'logRotateCount': 5,
+ 'logRotateInterval': 86400,
+ 'logStdout': True,
+ 'log_flowcb_needed': False,
+ 'masks': ['accept .* into //home/peter/sarra_devdocroot/recd_by_srpoll_test1 with mirror:True strip:.*sent_by_tsource2send/'],
+ 'messageAgeMax': 0,
+ 'messageCountMax': 0,
+ 'messageDebugDump': False,
+ 'messageRateMax': 0,
+ 'messageRateMin': 0,
+ 'message_strategy': {'failure_duration': '5m', 'reset': True, 'stubborn': True},
+ 'message_ttl': 0,
+ 'mirror': True,
+ 'msg_total_interval': '0',
+ 'fileAgeMax': 0,
+ 'nodupe_ttl': 0,
+ 'overwrite': True,
+ 'permCopy': True,
+ 'permDefault': 0,
+ 'permDirDefault': 509,
+ 'permLog': 384,
+ 'plugins_early': [],
+ 'plugins_late': ['sarracenia.flowcb.log.Log'],
+ 'post_baseDir': None,
+ 'post_baseUrl': None,
+ 'post_broker': None,
+ 'post_documentRoot': None,
+ 'post_exchanges': [],
+ 'post_topicPrefix': ['v03', 'post'],
+ 'prefetch': 25,
+ 'pstrip': '.*sent_by_tsource2send/',
+ 'queueBind': True,
+ 'queueDeclare': True,
+ 'queueName': 'q_tsource_subscribe.q_f71.76359618.62916076',
+ 'queue_filename': '/home/peter/.cache/sr3/subscribe/q_f71/subscribe.q_f71.tsource.qname',
+ 'randid': 'cedf',
+ 'randomize': False,
+ 'realpathPost': False,
+ 'rename': None,
+ 'report': False,
+ 'reset': False,
+ 'resolved_qname': 'q_tsource_subscribe.q_f71.76359618.62916076',
+ 'retry_ttl': 1800.0,
+ 'settings': {},
+ 'sleep': 0.1,
+ 'statehost': False,
+ 'strip': 0,
+ 'subtopic': [],
+ 'timeCopy': True,
+ 'timeout': 300,
+ 'timezone': 'UTC',
+ 'tls_rigour': 'normal',
+ 'topicPrefix': ['v03', 'post'],
+ 'undeclared': ['msg_total_interval', 'file_total_interval'],
+ 'users': False,
+ 'v2plugin_options': [],
+ 'v2plugins': {'plugin': ['msg_total_save', 'file_total_save']},
+ 'vhost': '/',
+ 'vip': []}
+
+%
+
+
+
+
+

convert

+

Conversion d’une configuration : les deux formats sont acceptés, ainsi que les fichiers d’inclusion (.inc)

+
$ sr3 convert poll/sftp_f62
+  2022-06-14 15:00:00,762 1093345 [INFO] root convert converting poll/sftp_f62 from v2 to v3
+
+$ sr3 convert poll/sftp_f62.conf
+  2022-06-14 15:01:11,766 1093467 [INFO] root convert converting poll/sftp_f62.conf from v2 to v3
+
+$ sr3 convert shovel/no_trouble_f00.inc
+  2022-06-14 15:03:29,918 1093655 [INFO] root convert converting shovel/no_trouble_f00.inc from v2 to v3
+
+
+

Pour écraser une configuration SR3 existante, utilisez l’option –wololo. +Lors de l’écrasement de plusieurs configurations SR3 est voulu, il faut également +utiliser –dangerWillRobinson=n dans le mode normale… où n est le nombre de configurations à convertir.

+
+
+

start

+

lancer tous les composants configurés:

+
$ sr3 start
+  gathering global state: procs, configs, state files, logs, analysis - Done.
+  starting...Done
+
+
+
+
+

stop

+

arrêter tous les processus:

+
$ sr3 stop
+  gathering global state: procs, configs, state files, logs, analysis - Done.
+  stopping........Done
+  Waiting 1 sec. to check if 93 processes stopped (try: 0)
+  All stopped after try 0
+
+
+
+
+

status

+

Exemple d’état OK (sr3 est en cours d’exécution)

+
fractal% sr3 status
+status:
+Component/Config                         Processes   Connection        Lag                              Rates
+                                         State   Run Retry  msg data   Queued  LagMax LagAvg  Last  %rej     pubsub messages   RxData     TxData
+                                         -----   --- -----  --- ----   ------  ------ ------  ----  ----     ------ --------   ------     ------
+cpost/veille_f34                         run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/pelle_dd1_f04                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   31.3%  0 Bytes/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/pelle_dd2_f05                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   31.3%  0 Bytes/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/xvan_f14                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+cpump/xvan_f15                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+poll/f62                                 run     1/1     0 100%   0%      0    0.08s    0.04s  1.4s  0.0%  2.0 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+post/shim_f63                            stop    0/0          -          -         -     -     -          -        -
+post/test2_f61                           stop    0/0     0 100%   0%      0    0.02s    0.01s  0.4s  0.0%  8.1 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+sarra/download_f20                       run     3/3     0 100%  10%      0   13.17s    5.63s  1.8s  0.0%  5.4 KiB/s   4 msgs/s  1.7 KiB/s  0 Bytes/s
+sender/tsource2send_f50                  run   10/10     0 100%   9%      0    1.37s    1.08s  1.9s  0.0%  8.1 KiB/s   5 msgs/s  0 Bytes/s  1.7 KiB/s
+shovel/pclean_f90                        run     3/3   136 100%   0%      0    0.00s    0.00s  0.6s  0.0%  4.0 KiB/s   5 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/pclean_f92                        run     3/3     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/rabbitmqtt_f22                    run     3/3     0 100%   0%      0    0.89s    0.67s  1.5s  0.0%  8.1 KiB/s   5 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/t_dd1_f00                         run     3/3     0 100%   0%    124   23.15s    4.50s  0.1s 55.0%  3.9 KiB/s   9 msgs/s  0 Bytes/s  0 Bytes/s
+shovel/t_dd2_f00                         run     3/3     0 100%   0%     83   11.82s    3.50s  0.1s 49.2%  3.6 KiB/s   8 msgs/s  0 Bytes/s  0 Bytes/s
+subscribe/amqp_f30                       run     3/3     0 100%  12%      0   18.79s    9.22s  0.1s  0.0%  3.3 KiB/s   4 msgs/s  1.9 KiB/s  0 Bytes/s
+subscribe/cclean_f91                     run     3/3   145 100%   0%      1    0.00s    0.00s  0.4s  0.0%  2.3 KiB/s   6 msgs/s  0 Bytes/s  0 Bytes/s
+subscribe/cdnld_f21                      run     3/3     0 100%  17%     12    7.20s    2.81s  0.7s  0.0%  2.3 KiB/s   3 msgs/s  1.7 KiB/s  0 Bytes/s
+subscribe/cfile_f44                      run     3/3     0 100%   6%      1    3.32s    0.32s  0.4s  0.0%  2.3 KiB/s   6 msgs/s  1.7 KiB/s  0 Bytes/s
+subscribe/cp_f61                         run     3/3     0 100%   3%      0    6.42s    3.49s  1.6s  0.0%  4.2 KiB/s   6 msgs/s 635 Bytes/s  0 Bytes/s
+subscribe/ftp_f70                        run     3/3     0 100%   8%      0    1.18s    0.83s  0.2s  0.0%  1.8 KiB/s   3 msgs/s  1.8 KiB/s  0 Bytes/s
+subscribe/q_f71                          run     3/3     0 100%   0%      0    1.62s    0.57s  0.0s  0.0%  1.2 KiB/s   3 msgs/s  1.2 KiB/s  0 Bytes/s
+subscribe/rabbitmqtt_f31                 run     3/3     0 100%  11%      0    4.27s    1.95s  1.2s  0.0%  4.2 KiB/s   6 msgs/s 637 Bytes/s  0 Bytes/s
+subscribe/u_sftp_f60                     run     3/3     0 100%   1%      0    2.69s    2.23s  1.3s  0.7%  4.2 KiB/s   6 msgs/s 644 Bytes/s  0 Bytes/s
+watch/f40                                run     1/1     0 100%   0%      0    0.10s    0.05s  1.9s  0.0%  4.2 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s
+winnow/t00_f10                           run     1/1     0 100%   0%      0   12.31s    4.33s  3.5s 50.0%  3.2 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s
+winnow/t01_f10                           run     1/1     0 100%   0%      0   11.59s    3.76s  0.1s 50.5%  4.2 KiB/s   4 msgs/s  0 Bytes/s  0 Bytes/s
+      Total Running Configs:  25 ( Processes: 64 missing: 0 stray: 0 )
+                     Memory: uss:2.4 GiB rss:3.3 GiB vms:6.2 GiB
+                   CPU Time: User:39.62s System:4.42s
+       Pub/Sub Received: 103 msgs/s (80.6 KiB/s), Sent:  63 msgs/s (32.8 KiB/s) Queued: 221 Retry: 281, Mean lag: 2.32s
+          Data Received: 32 Files/s (11.9 KiB/s), Sent: 5 Files/s (1.7 KiB/s)
+fractal%
+
+
+

L’état au complet

+
fractal% sr3 --full status
+status:
+Component/Config                         Processes   Connection        Lag                              Rates                                        Counters (per housekeeping)                                                    Data Counters                                           Memory                             CPU Time
+                                         State   Run Retry  msg data   Queued  LagMax LagAvg  Last  %rej     pubsub messages   RxData     TxData       subBytes   Accepted   Rejected  Malformed   pubBytes    pubMsgs     pubMal     rxData    rxFiles     txData    txFiles    Since       uss        rss        vms       user     system
+                                         -----   --- -----  --- ----   ------  ------ ------  ----  ----     ------ --------   ------     ------        -------   --------   --------  ---------    -------     ------      -----      -----    -------     ------  -------      -----        ---        ---        ---       ----     ------
+cpost/veille_f34                         run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    2.4 MiB    5.9 MiB   15.2 MiB        0.03       0.08
+cpump/pelle_dd1_f04                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   83.5%  0 Bytes/s  11 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes    1.4 Kim    1.1 Kim     0 msgs    0 Bytes   230 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    3.9 MiB    6.8 MiB   17.2 MiB        0.12       0.11
+cpump/pelle_dd2_f05                      run     1/1     0 100%   0%      0    0.00s    0.00s n/a   83.5%  0 Bytes/s  11 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes    1.4 Kim    1.1 Kim     0 msgs    0 Bytes   230 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  121.40s    3.9 MiB    7.0 MiB   17.2 MiB        0.17       0.06
+cpump/xvan_f14                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   12.90s    3.0 MiB    4.7 MiB   16.1 MiB        0.01       0.01
+cpump/xvan_f15                           run     1/1     0 100%   0%      0    0.00s    0.00s n/a    0.0%  0 Bytes/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes      1 msg     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files    8.63s    3.0 MiB    4.6 MiB   16.1 MiB        0.01       0.02
+poll/f62                                 run     1/1     0 100%   0%      0    0.16s    0.05s  0.8s  0.0%  1.3 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs    4.4 Kim     0 msgs  151.1 KiB   420 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.56s   45.1 MiB   59.0 MiB  229.3 MiB        2.62       0.32
+post/shim_f63                            stop    0/0          -          -         -     -     -          -        -        -       -          -          -          -          -          -          -          -          -          -          -    0 Bytes    0 Bytes    0 Bytes        0.00       0.00
+post/test2_f61                           stop    0/0     0 100%   0%      0    0.03s    0.02s  0.5s  0.0% 11.5 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs     0 msgs     0 msgs    6.7 KiB    14 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files    0.58s    0 Bytes    0 Bytes    0 Bytes        0.00       0.00
+sarra/download_f20                       run     3/3     0 100%   0%      0   10.17s    2.87s  1.1s  0.0%  1.1 KiB/s   0 msgs/s  1.1 KiB/s  0 Bytes/s   27.1 KiB    47 msgs     0 msgs     0 msgs   36.0 KiB    47 msgs     0 msgs   65.6 KiB   47 Files    0 Bytes    0 Files   57.57s  132.5 MiB  192.4 MiB  280.1 MiB        2.72       0.40
+sender/tsource2send_f50                  run   10/10     0 100%   9%      0    1.37s    0.52s  1.3s  0.0%  5.5 KiB/s   3 msgs/s  0 Bytes/s  1.3 KiB/s  326.0 KiB   421 msgs     0 msgs     0 msgs  326.6 KiB   421 msgs     0 msgs    0 Bytes    0 Files  152.7 KiB  421 Files  118.97s  425.6 MiB  562.8 MiB    2.2 GiB        7.80       0.93
+shovel/pclean_f90                        run     3/3   310 100%   0%      0   82.03s   75.72s  0.7s  0.0%  5.6 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s  111.4 KiB   120 msgs     0 msgs     0 msgs   99.9 KiB   111 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   38.02s  127.4 MiB  169.0 MiB  249.6 MiB        2.37       0.27
+shovel/pclean_f92                        run     3/3     0 100%   0%      0   82.49s   76.06s 19.1s  0.0%  1.7 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s   99.9 KiB   111 msgs     0 msgs     0 msgs  103.0 KiB   111 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.28s  126.4 MiB  168.5 MiB  249.2 MiB        2.04       0.21
+shovel/rabbitmqtt_f22                    run     3/3     0 100%   0%      0    1.25s    0.54s  1.3s  0.0%  5.5 KiB/s   3 msgs/s  0 Bytes/s  0 Bytes/s  326.0 KiB   421 msgs     0 msgs     0 msgs  326.0 KiB   421 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.77s  126.0 MiB  167.6 MiB  248.7 MiB        2.12       0.20
+shovel/t_dd1_f00                         run     3/3     0 100%   0%      3   23.15s    3.06s  0.1s 82.3%  3.4 KiB/s  14 msgs/s  0 Bytes/s  0 Bytes/s  231.6 KiB    1.6 Kim    1.3 Kim     0 msgs  168.4 KiB   293 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.19s  127.3 MiB  171.0 MiB  250.1 MiB        2.77       0.32
+shovel/t_dd2_f00                         run     3/3     0 100%   0%      0   11.82s    2.61s  0.2s 82.2%  3.3 KiB/s  13 msgs/s  0 Bytes/s  0 Bytes/s  225.7 KiB    1.6 Kim    1.3 Kim     0 msgs  166.6 KiB   290 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files  118.33s  127.3 MiB  170.6 MiB  250.1 MiB        2.74       0.39
+subscribe/amqp_f30                       run     3/3     0 100%  39%      0   18.79s    6.44s  0.7s  0.0%  1.5 KiB/s   2 msgs/s  1.2 KiB/s  0 Bytes/s  181.0 KiB   237 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  146.8 KiB  237 Files    0 Bytes    0 Files  117.95s  126.2 MiB  168.0 MiB  248.9 MiB        2.20       0.27
+subscribe/cclean_f91                     run     3/3   213 100%   0%      0    0.00s    0.00s  0.5s 17.4%  2.7 KiB/s   6 msgs/s  0 Bytes/s  0 Bytes/s   35.5 KiB    92 msgs    16 msgs     0 msgs    0 Bytes     0 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   13.37s  126.1 MiB  167.9 MiB  248.8 MiB        2.53       0.36
+subscribe/cdnld_f21                      run     3/3     0 100%  41%      0   10.43s    3.13s  0.1s  0.0%  1.4 KiB/s   2 msgs/s  1.4 KiB/s  0 Bytes/s  167.1 KiB   262 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  162.7 KiB  262 Files    0 Bytes    0 Files  117.90s  131.6 MiB  190.7 MiB  277.6 MiB        3.39       0.42
+subscribe/cfile_f44                      run     3/3     0 100%  40%      0    3.32s    0.30s  0.0s  0.0%  1.5 KiB/s   4 msgs/s  1.4 KiB/s  0 Bytes/s  178.3 KiB   509 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  161.8 KiB  509 Files    0 Bytes    0 Files  118.09s  130.9 MiB  188.5 MiB  278.0 MiB        2.62       0.30
+subscribe/cp_f61                         run     3/3     0 100%  12%      0    6.42s    2.09s  0.1s  0.0%  2.8 KiB/s   3 msgs/s 597 Bytes/s  0 Bytes/s  326.6 KiB   421 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  123 Files    0 Bytes    0 Files  118.23s  125.9 MiB  166.8 MiB  248.9 MiB        2.48       0.31
+subscribe/ftp_f70                        run     3/3     0 100%  39%      0    1.75s    0.77s  0.1s  0.0%  1.3 KiB/s   2 msgs/s  1.4 KiB/s  0 Bytes/s  158.4 KiB   340 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  159.8 KiB  340 Files    0 Bytes    0 Files  118.04s  126.1 MiB  167.4 MiB  248.8 MiB        2.22       0.36
+subscribe/q_f71                          run     3/3     0 100%  31%      0    3.12s    1.18s  5.7s  0.0%  1.2 KiB/s   3 msgs/s  1.1 KiB/s  0 Bytes/s  142.7 KiB   396 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs  124.6 KiB  396 Files    0 Bytes    0 Files  118.49s  135.0 MiB  191.9 MiB  928.8 MiB        4.30       0.68
+subscribe/rabbitmqtt_f31                 run     3/3     0 100%   8%      0    4.27s    1.10s  1.1s  0.0%  2.8 KiB/s   3 msgs/s 598 Bytes/s  0 Bytes/s  326.0 KiB   421 msgs     0 msgs     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  123 Files    0 Bytes    0 Files  118.15s  126.2 MiB  167.9 MiB  248.9 MiB        2.22       0.27
+subscribe/u_sftp_f60                     run     3/3     0 100%   9%      0    2.69s    1.71s  1.2s  0.2%  2.8 KiB/s   3 msgs/s 599 Bytes/s  0 Bytes/s  326.6 KiB   421 msgs      1 msg     0 msgs    0 Bytes     0 msgs     0 msgs   69.0 KiB  122 Files    0 Bytes    0 Files  117.93s  126.4 MiB  167.8 MiB  249.2 MiB        2.05       0.33
+watch/f40                                run     1/1     0 100%   0%      0    0.06s    0.02s  1.6s  0.0%  3.0 KiB/s   0 msgs/s  0 Bytes/s  0 Bytes/s    0 Bytes     0 msgs    74 msgs     0 msgs  169.5 KiB   203 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   57.21s   46.3 MiB   65.3 MiB  311.8 MiB        1.95       0.14
+winnow/t00_f10                           run     1/1     0 100%   0%      0    6.38s    3.47s  0.2s 51.7%  1.7 KiB/s   2 msgs/s  0 Bytes/s  0 Bytes/s   66.8 KiB   116 msgs    60 msgs     0 msgs   32.3 KiB    56 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   57.79s   42.5 MiB   56.2 MiB   83.3 MiB        0.77       0.12
+winnow/t01_f10                           run     1/1     0 100%   0%      0    9.75s    2.53s  0.1s 51.3%  1.8 KiB/s   2 msgs/s  0 Bytes/s  0 Bytes/s   67.2 KiB   117 msgs    60 msgs     0 msgs   32.8 KiB    57 msgs     0 msgs    0 Bytes    0 Files    0 Bytes    0 Files   56.37s   42.3 MiB   55.7 MiB   83.2 MiB        0.81       0.12
+      Total Running Configs:  25 ( Processes: 64 missing: 0 stray: 0 )
+                     Memory: uss:2.5 GiB rss:3.4 GiB vms:7.4 GiB
+                   CPU Time: User:53.06s System:7.00s
+       Pub/Sub Received: 99 msgs/s (63.2 KiB/s), Sent:  53 msgs/s (29.3 KiB/s) Queued: 3 Retry: 523, Mean lag: 4.54s
+          Data Received: 18 Files/s (9.3 KiB/s), Sent: 3 Files/s (1.3 KiB/s)
+fractal% sr3 --full status
+
+
+

La première rangée catégorise les informations pour les lignes suivantes:

+
    +
  • Processes (Processus): Indique le nombre d’instances et si il y en manquent.

  • +
  • Connection (Connexion): Indique l’état de la connexion externe.

  • +
  • Lag (Délai): Le sévérité du délai expériencé ou la durée de vie des données par le temps que le téléchargement soit complété.

  • +
  • Last (Dernier): combien de temps a passé depuis le dernier transfert de fichier ou message.

  • +
  • Rates (Taux): La vitesse des transferts, plusieurs métriques sont communiqués.

  • +
  • Counters (Compteurs): La base des calculs des taux, se réinitialise à chaque interval de housekeeping.

  • +
  • Memory (Mémoire): L’utilisation de la mémoire des processus dans une configuration particulière.

  • +
  • CPU time (temps de traitement): Le temps de traitement des processus dans une configuration particulière.

  • +
+

Les dernières trois catégories sont seulement listés avec l’option –full est fourni.

+

La deuxième rangée donne des détails sur les en têtes de chacune des catégories.

+

Les configurations sont répertoriées sur la gauche. Pour chaque configuration, l’état. +sera :

+
    +
  • stopped: aucun processus n’est en cours d’exécution.

  • +
  • running: tout les processus sont en cours d’exécution.

  • +
  • partial: certains processus sont en cours d’exécution.

  • +
  • disabled: configuré pour ne pas s’exécuter.

  • +
+

Les colonnes à droite donnent plus d’informations, détaillant le nombre de processus en cours d’exécution à partir du nombre attendu. +Par exemple, 3/3 signifie 3 processus ou instances sont trouvés à partir des 3 attendus. +Expected liste combien de processus devraient être exécutés à partir d’une configuration même si ils sont arrêtés.

+

La colonne Retry indique le nombre de messages de notifications stockés dans la queue Retry locale. Celle-ci indique quelles chaînes ont de la difficultés à traiter les données.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Entête

But

State

Statut d’une configuration particulière: stop|run|disa|part|wVip|fore

Run

Nombre de processus ou instances en marche en comparaison au nombre attendu. 3/10 3 processes running of 10 expected.

Retry

Le nombre de messages dans la queue retry, indiquant des problèmes avec le transfert.

msg

Le pourcentage du temps connecté à un broker pour publier ou s’abonner à des messages.

data

Le pourcentage du temps connecté à une source de données.

LagQueued

Le nombre de messages en attente sur le courtier (serveur à distance.)

LagMax

La durée maximale d’un message à la reception (avant un téléchargement).

LagAvg

La durée moyenne d’un message à la reception (avant un téléchargement).

Last

Combien de temps a passé depuis le dernier téléchargement (ou message.)

pubsub

Le débit des messages pub/sub téléchargés en bytes/seconde.

messages

Le débit des messages pub/sub téléchargés en message/seconde.

%rej

Le pourcentage des messages téléchargés rejeté (indication du statut du filtre d’abonnement (subscription).

RxData

Le montant de données téléchargées (pas en format message)

TxData

Le montant de données envoyées (pas en format message)

subBytes

Compteur d’octets de messages pub/sub reçus.

Accepted

Compteur du nombre de messages acceptés.

Rejected

Compteur du nombre de messages rejetés.

Malformed

Compteur de messages qui ont été rejetés parce qu’ils n’ont pas pu être compris.

pubBytes

Compteur d’octets de messages en cours de publication.

pubMsgs

Compteur de messages en cours de publication.

pubMal

Compteur de messages dont la publication a échoué.

rxData

Compteur d’octets de fichiers en cours de téléchargement.

rxFiles

Compteur de fichiers en cours de téléchargement.

txData

Compteur d’octets de fichiers envoyés.

txFiles

Compteur de fichiers envoyés.

Since

Combien de secondes depuis la dernière réinitialisation du compteur (base pour les calculs de taux.)

uss

unique set size (utilisation de la mémoire des instances.) Mémoire physique unique réelle utilisée par les processus.

rss

resident set size (utilisation de la mémoire des instances) Mémoire physique réelle utilisée, y compris partagée.

vms

taille de la mémoire virtuelle de tous les partages et physiques et swap alloués ensemble.

user

temps d’utilisation CPU de l’utilisateur

system

temps de CPU utilisé par le système

+

À la fin de la liste, nous avons la somme des valeurs mentionnées précédemment.

+
+
+
+
+

La ceuillette de messages

+

La plupart des composants Metpx Sarracenia boucle sur la ceuillette et/ou +réception de messages AMQP. Habituellement, les messages d’intérêt sont +dans le format d´une avis sr_post(7), annonçant la disponibilité +d’un fichier en publiant l’URL pour l´accéder (ou une partie de celle-ci). +Les messages AMQP sont publiés avec un exchange comme destinataire. Sur un courtier (serveur AMQP.) L’exchange +délivre des messages aux files d’attente. Pour recevoir de messages, +on doit fournir les informations d’identification pour se connecter au +courtier (message AMQP). Une fois connecté, un consommateur doit créer +une fil d’attente pour retenir les messages en attente. Le consommateur +doit ensuite lier la fil d’attente à une ou plusieurs échanges de manière +à ce qu’il mette dans sa fil d’attente.

+

Une fois les liaisons (anglais: bindings) établies, le programme peut +recevoir des messages. Lorsqu’un message est reçu, un filtrage +supplémentaire est possible en utilisant des expressions régulières sur +les messages AMQP. Après qu’un message a passé avec succès ce processus +de sélection et d’autres validations internes, le processus peut exécuter +un script de plugin on_message pour traiter le message davantage +de façon spécialisé. Si ce plugin retourne False comme résultat, le +message est rejeté. Si c’est vrai, le traitement du message se poursuit.

+

Les sections suivantes expliquent toutes les options pour régler cette +partie “consommateur” de les programmes de Sarracenia.

+
+

Réglage de Broker

+

broker [amqp|mqtt]{s}://<user>:<password>@<brokerhost>[:port]/<vhost>

+

Un URI AMQP est utilisé pour configurer une connexion à une pompe à messages +(AMQP broker). Certains composants de Sarracenia définissent une valeur par +défaut raisonnable pour cette option. Vous fournissez l’utilisateur normal, +l’hôte, le port des connexions. Dans la plupart des fichiers de configuration, +le mot de passe est manquant. Le mot de passe n’est normalement inclus que dans +le fichier credentials.conf.

+

L´application Sarracenia n’a pas utilisé vhosts, donc vhost devrait toujours être /.

+

pour plus d’informations sur le format URI de l’AMQP : ( https://www.rabbitmq.com/uri-spec.html))

+

soit dans le fichier default.conf, soit dans chaque fichier de configuration spécifique. +L’option courtier indique à chaque composante quel courtier contacter.

+

broker [amqp|mqtt]{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

+
+
::

(par défaut : Aucun et il est obligatoire de le définir)

+
+
+

Une fois connecté à un courtier AMQP, l’utilisateur doit lier une fil d’attente. +à l´*exchange* et aux thèmes (topics) pour déterminer les messages intérêsseants.

+
+
+

Configuration de fil d´attente

+

Une fois connecté à un courtier AMQP, l’utilisateur doit créer une fil d’attente.

+

Mise en fil d’attente sur broker :

+
    +
  • queue <nom> (par défaut : q_<brokerUser>.<programName>.<configName>.<configName>)

  • +
  • expire <durée> (par défaut : 5m == cinq minutes. À OUTREPASSER)

  • +
  • message_ttl <durée> (par défaut : Aucun)

  • +
  • prefetch <N> (par défaut : 1)

  • +
+

Habituellement, les composants devinent des valeurs par défaut raisonnables pour +toutes ces valeurs et les utilisateurs n’ont pas besoin de les définir. Pour +les cas moins habituels, l’utilisateur peut avoir besoin a remplacer les valeurs +par défaut. La fil d’attente est l’endroit où les avis sont conservés +sur le serveur pour chaque abonné.

+
+

[ queue|queue_name|qn <name>]

+

Par défaut, les composants créent un nom de fil d’attente qui doit être unique. Le +queue_name par défaut créé par les composants et suit la convention suivante :

+
+

q_<brokerUser>.<programName>.<configName>.<random>.<random>

+
+

Ou:

+
    +
  • brokerUser est le nom d’utilisateur utilisé pour se connecter au courtier (souvent: anonymous )

  • +
  • programName est le composant qui utilise la fil d’attente (par exemple sr_subscribe )

  • +
  • configName est le fichier de configuration utilisé pour régler le comportement des composants

  • +
  • random n’est qu’une série de caractères choisis pour éviter les affrontements de plusieurs +personnes qui utilisent les mêmes configurations

  • +
+

Les utilisateurs peuvent remplacer la valeur par défaut à condition qu’elle commence par q_<brokerUser>.

+

Lorsque plusieurs instances sont utilisées, elles utilisent toutes la même fil d’attente, pour du multi-tasking simple. +Si plusieurs ordinateurs disposent d’un système de fichiers domestique partagé, le +queue_name est écrit à :

+
+

~/.cache/sarra/<programName>/<configName>/<programName>_<configName>_<brokerUser>.qname

+
+

Les instances démarrées sur n’importe quel nœud ayant accès au même fichier partagé utiliseront la +même fil d’attente. Certains voudront peut-être utiliser l’option queue_name comme méthode plus explicite +de partager le travail sur plusieurs nœuds.

+
+
+
+

AMQP QUEUE BINDINGS

+

Une fois qu’on a une fil d’attente, elle doit être liée à un échange (exchange.) +Les utilisateurs ont presque toujours besoin de définir ces options. Une +fois qu’une fil d’attente existe sur le courtier, il doit être lié (bound) à +une échange. Les liaisons (bindings) définissent ce que l’on entend par +les avis que le programme reçoit. La racine du thème +est fixe, indiquant la version du protocole et le type de l’arborescence. +(mais les développeurs peuvent l’écraser avec le topic_prefix. +option.)

+

Ces options définissent les messages (notifications URL) que le programme reçoit :

+
+
    +
  • exchange <name> (défaut: xpublic)

  • +
  • exchangeSuffix <name> (défaut: None)

  • +
  • topic_prefix <amqp pattern> (défaut: 03 – developer option)

  • +
  • subtopic <amqp pattern> (pas de défaut, doit apparaitre apres exchange)

  • +
+
+
+

subtopic <amqp pattern> (default: #)

+

Dans les publications d’un échange, le paramètre de sous-thème restreint la sélection de produits. +Pour donner une valeur correcte au sous-thème, on a le choix de filtrer en utilisant subtopic +avec seulement le wildcard limité d’AMQP et à longueur limitée à 255 octets codés, ou l’expression +régulière la plus puissante basés sur les mécanismes accept/reject décrits ci-dessous. +La différence étant que le Le filtrage AMQP est appliqué par le courtier lui-même, ce qui évite +que les avis ne soient livrés au client du tout. Les modèles accept/reject s’appliquent +aux messages envoyés par le courtier à l’abonné. En d’autres termes, accept/reject sont +des filtres côté client, alors que subtopic est le filtrage côté serveur.

+

Il est préférable d’utiliser le filtrage côté serveur pour réduire le nombre +de avis envoyées au client à un petit sur-ensemble de ce qui est pertinent, +et n’effectuer qu’un réglage fin avec l’outil mécanismes côté client, économisant +la bande passante et le traitement pour tous.

+

topic_prefix est principalement d’intérêt pendant les transitions de version +de protocole, où l’on souhaite spécifier une version sans protocole par défaut +des messages auxquels s’abonner, ou bien pour manipuler des rapports de disposition, +au lieu d’avis.

+

Habituellement, l’utilisateur spécifie un échange et plusieurs options de sous-thèmes. +subtopic est ce qui est normalement utilisé pour indiquer les messages d’intérêt. +Pour utiliser le sous-thème pour filtrer les produits, faites correspondre la +chaîne de sous-thèmes avec le chemin relatif dans l´arborescence de répertoires sur le serveur.

+

Par exemple, en consommant à partir de DD, pour donner une valeur correcte au sous-thème, on peut +Parcourez notre site Web http://dd.weather.gc.ca et notez tous les annuaires. +d’intérêt. Pour chaque arborescence de répertoires d’intérêt, écrivez un subtopic. +comme suit :

+
+

subtopic directory1.*.subdirectory3.*.subdirectory5.#

+
+
ou:
+      *                correspond a un seul nom de repertoire
+      #                correspond à toute arborescence de répertoires restante
+
+
+

On peut utiliser plusieurs liaisons à plusieurs échanges comme cela:

+
exchange A
+subtopic directory1.*.directory2.#
+
+exchange B
+subtopic *.directory4.#
+
+
+

Cela va déclarer deux liaisons différentes à deux échanges différents et deux arborescences de fichiers différentes. +Alors que la liaison par défaut consiste à se lier à tout, certains courtiers pourraient ne pas permettre aux +clients à définir des liaisons, ou on peut vouloir utiliser des liaisons existantes. +On peut désactiver la liaison de fil d’attente comme cela:

+
subtopic None
+
+
+

(False, ou off marchera aussi.)

+
+
+
+

Client-side Filtering

+

Nous avons sélectionné nos messages via exchange, subtopic et subtopic. +Le courtier met les messages correspondants dans notre fil d’attente (queue). +Le composant télécharge ces messages.

+

Les clients Sarracenia implémentent un filtrage plus flexible côté client +en utilisant les expressions régulières.

+
+

Brief Introduction to Regular Expressions

+

Les expressions régulières sont un moyen très puissant d’exprimer les correspondances de motifs. +Ils offrent une flexibilité extrême, mais dans ces exemples, nous utiliserons seulement un +petit sous-ensemble : Le point (.) est un joker qui correspond à n’importe quel caractère +unique. S’il est suivi d’un nombre d’occurrences, il indique le nombre de lettres +qui correspondent. Le caractère * (astérisque), signifie un nombre quelconque d’occurrences. +alors :

+
+
    +
  • .* signifie n’importe quelle séquence de caractères de n’importe quelle longueur. +En d’autres termes, faire correspondre n’importe quoi.

  • +
  • cap.* signifie toute séquence de caractères commençant par cap.

  • +
  • .*CAP.* signifie n’importe quelle séquence de caractères avec CAP quelque part dedans.

  • +
  • .*CAP signifie toute séquence de caractères qui se termine par CAP.

  • +
  • Dans le cas où plusieurs portions de la chaîne de caractères pourraient correspondre, la plus longue est sélectionnée.

  • +
  • .*?CAP comme ci-dessus, mais non-greedy, ce qui signifie que le match le plus court est choisi.

  • +
  • noter que l’implantaions de regexp en C n’inclu pas le greediness, alors certains expressions +ne seront pas interpretés pareilles par les outils implanté en C: sr_cpost, sr_cpump, où libsrshim.

  • +
+
+

Veuillez consulter diverses ressources Internet pour obtenir de plus amples renseignements:

+
+
+
+
+

accept, reject and accept_unmatch

+
    +
  • accept <expression régulière (regexp)> (facultatif)

  • +
  • reject <expression régulière (regexp)> (facultatif)

  • +
  • acceptUnmatched <booléen> (par défaut: True)

  • +
  • baseUrl_relPath <booléen> (par défaut: False)

  • +
+

Les options accept et reject traitent des expressions régulières (regexp). +La regexp est appliquée à l’URL du message pour détecter une correspondance.

+

Si l’URL du message d’un fichier correspond à un motif reject, on informe +le courtier que le message a été consommé et on abandonne son traitement.

+

Celui qui correspond à un motif accept est traité par le composant.

+

Dans de nombreuses configurations, les options accept et reject +sont spécifiés ensembles, et avec l’option directory. Ils relient +ensuite les messages acceptés à la valeur directory sous laquelle +ils sont spécifiés.

+

Après que toutes les options accept / reject sont traitées normalement. +l’accusé de réception du message tel qu’il a été consommé et ignoré. Pour +outrepasser ce comportement de défaut, définissez accept_unmatch à True.

+

Les accept/rejet sont interprétés dans l’ordre qu´ils apparaissent +dans le fichier de configuration. Chaque option est traitée en ordre +de haut en bas. par exemple :

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+

Dans la séquence #1, tous les fichiers se terminant par ‘gif’ sont rejetés. +Dans la séquence #2, l’option accept .* (regexp qui veut dire accepte tout) est +rencontré avant la déclaration de rejet, de sorte que le rejet n’a aucun effet.

+

Il est préférable d’utiliser le filtrage côté serveur pour réduire le nombre +de avis envoyées au composant à un petit sur-ensemble de ce qui est +pertinent, et n’effectuer qu’un réglage fin avec les mécanismes accept/reject +côté client, économisant la bande passante et le traitement pour tous.

+

Plus de détails sur la façon d’appliquer les directives suivent:

+

Normalement, le chemin d’accès relatif (baseUrl_relPath est False, ajouté au répertoire de base) pour +les fichiers téléchargés seront définis en fonction de l’en-tête relPath inclus +dans le message. Toutefois, si baseUrl_relPath est défini, le relPath du message va +être précédé des sous-répertoires du champ baseUrl du message.

+
+
+
+

Convention d´appellation de files d´attente

+

Alors que dans la plupart des cas, une bonne valeur de nom de fil d´attente (en anglais: queue) est +générée par l’application, dans certains cas, c´est nécessaire de remplacer ces choix par une +spécification utilisateur explicite. Pour ce faire, il faut connaître les règles de nommage des files d’attente :

+
    +
  1. les noms de fil d’attente commencent par q_.

  2. +
  3. ceci est suivi de <amqpUserName> (le propriétaire/utilisateur du nom d’utilisateur du courtier de la fil d’attente).

  4. +
  5. suivi d’un deuxième tiret de soulignement ( _ )

  6. +
  7. suivi d’une chaîne de caractères au choix de l’utilisateur.

  8. +
+

La longueur totale du nom de la fil d’attente est limitée à 255 octets de caractères UTF-8.

+
+
+
+

PUBLICATION (POST)

+

Comme de nombreux composants consomment un flux de messages, de nombreux composants +(souvent les mêmes) produisent également un flux de sortie de messages. Pour créer des fichiers +disponible pour les abonnés, une affiche publie les annonces à un AMQP ou +Serveur MQTT, également appelé broker. L’option post_broker définit toutes les +informations d’identification pour se connecter au courtier de sortie AMQP.

+

post_broker [amqp|mqtt]{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

+

Une fois connecté au courtier de source AMQP, le programme génère des notifications après que +le téléchargement d’un fichier a eu lieu. Pour générer la notification et l’envoyer au +courtier au saut suivant, l’utilisateur définit ces options :

+
    +
  • post_baseDir <path> (facultatif)

  • +
  • post_topicPrefix <pfx> (par défaut: ‘v03’)

  • +
  • post_exchange <name> (par défaut: xpublic)

  • +
  • post_baseUrl <url> (OBLIGATOIRE)

  • +
+

FIXME : Des exemples de ce à quoi ils servent, de ce qu’ils font…

+
+

Convention d´appellation des EXCHANGES

+
    +
  1. Les noms d’échange commencent par x

  2. +
  3. Les échanges qui se terminent par public sont accessibles (pour lecture) par tout utilisateur authentifié.

  4. +
  5. Les utilisateurs sont autorisés à créer des échanges avec le modèle: xs_<amqpUserName>_<whatever>. ces échanges ne peuvent être écrits que par cet utilisateur.

  6. +
  7. Le système (sr_audit ou administrateurs) crée l’échange de xr_<amqpUserName> comme un lieu d’envoi de rapports pour un utilisateur. Il n’est lisible que par cet utilisateur.

  8. +
  9. Les utilisateurs administratifs (rôles d’administrateur ou de feeder) peuvent publier ou s’abonner n’importe où.

  10. +
+

Par exemple, xpublic n’a pas xs_ et un modèle de nom d’utilisateur, de sorte qu’il ne peut être publié que par les utilisateurs administrateurs ou feeder. +Puisqu’il se termine en public, tout utilisateur peut s’y lier pour s’abonner aux messages publiés. +Les utilisateurs peuvent créer des échanges tels que xs_<amqpUserName>_public qui peuvent être écrits par cet utilisateur (selon la règle 3), +et lu par d’autres (selon la règle 2.) Une description du flux conventionnel de messages à travers les échanges sur une pompe. +Les abonnés se lient généralement à l’échange xpublic pour obtenir le flux de données principal. Il s’agit de la valeur par défaut dans sr_subscribe.

+

Un autre exemple, un utilisateur nommé Alice aura au moins deux échanges :

+
+
    +
  • xs_Alice l’exhange où Alice poste ses notifications de fichiers et signale les messages (via de nombreux outils).

  • +
  • xr_Alice l’échange d’où Alice lit ses messages de rapport (via sr_shovel).

  • +
  • Alice peut créer un nouvel échange en publiant simplement dessus (avec sr3_post ou sr_cpost) s’il répond aux règles de nommage.

  • +
+
+

Habituellement, un sr_sarra exécuté par un administrateur de pompe lira à partir d’un échange tel que xs_Alice_mydata +pour récupérer les données correspondant au message post d’Alice, et les rendre disponibles sur la pompe, +en le ré-annonçant sur l’échange xpublic.

+
+
+
+

SONDAGE (POLLING)

+

On peut faire le même travail que post, sauf que les fichiers sont sur un serveur distant. +Dans le cas d’un sondage (en anglais: poll), l’URL de la publication sera générée à partir de l´option pollUrl, +avec le chemin d’accès du produit (path « fichier correspondant »). Il y en a une publication +par fichier. La taille du fichier est prise dans le répertoire « ls »… mais sa somme +de contrôle ne peut pas être déterminée, alors la stratégie de calcul de est ¨cod¨ qui signifie +que ca devrait être calculé lors du transfert.

+

Pour définir la fréquence de sondage, on se sert de scheduled_, tel que:

+
scheduled_interal 30m
+
+
+

pour sonder à toute les trente minutes, ou bien:

+
scheduled_hour 1,13,19
+scheduled_minute 27
+
+
+

pour sonder trois fois par jour à 1h27, 13h27 et 19h27.

+

Par défaut, sr_poll envoie son message de publication au courtier avec l’échange par défaut +(le préfixe xs_ suivi du nom d’utilisateur du courtier). Le post_broker est obligatoire. +Il peut être incomplet s’il est bien défini dans le fichier credentials.conf.

+

Référez sr3_post(1) - pour comprendre l’ensemble du processus de notification. +Référez sr3_post(7) - pour comprendre le format complet de notification.

+
+
Ces options définissent les fichiers pour lesquels l’utilisateur souhaite être averti et où

il sera placé, et sous quel nom.

+
+
+
    +
  • path <path> (par défaut: .)

  • +
  • accept <regexp pattern> [rename=] (doit être défini)

  • +
  • reject <regexp pattern> (facultatif)

  • +
  • permDefault <integer> (par défaut: 0o400)

  • +
  • fileAgeMax <duration> (par défaut 30d)

  • +
+

fileAgeMax doit être inférieur à nodupe_ttl lors de l’utilisation de la suppression des doublons, +pour éviter la réingestion de fichiers obsolètes une fois partie du cache nodupe.

+

L’option path définit où obtenir les fichiers sur le serveur. +Combiné avec les options accept / reject, l’utilisateur peut sélectionner +les fichiers d’intérêt et leurs répertoires de résidence.

+

Les options accept et reject utilisent des expressions régulières (regexp) pour trouver +une correspondance avec l’URL. +Ces options sont traitées séquentiellement. +L’URL d’un fichier qui correspond à un modèle reject n’est pas publiée. +Les fichiers correspondant à un modèle accept sont publiés.

+

Le répertoire peut avoir des modèles. Ces modèles pris en charge concernent la date/l’heure. +Ils sont fixes…

+

${YYYY} année actuelle +${MM} mois actuel +${JJJ} julian actuelle +${YYYYMMDD} date actuelle

+

${YYYY-1D} année actuelle - 1 jour +${MM-1D} mois actuel - 1 jour +${JJJ-1D} julian actuelle - 1 jour +${YYYYMMDD-1D} date actuelle - 1 jour

+
ex.   path /mylocaldirectory/myradars
+      path /mylocaldirectory/mygribs
+      path /mylocaldirectory/${YYYYMMDD}/mydailies
+
+
+      accept    .*RADAR.*
+      reject    .*Reg.*
+      accept    .*GRIB.*
+      accept    .*observations.*
+
+
+

L’option permDefault permet aux utilisateurs de spécifier un masque d’autorisation octal numérique +de style Linux:

+
permDefault 040
+
+
+

signifie qu’un fichier ne sera pas publié à moins que le groupe ait l’autorisation de lecture +(sur une sortie ls qui ressemble à : —r—–, comme une commande chmod 040 <fichier> ). +Les options permDefault spécifient un masque, c’est-à-dire que les autorisations doivent être +au moins ce qui est spécifié.

+

Comme pour tous les autres composants, l’option vip peut être utilisée pour indiquer +qu’un poll doit être actif sur seulement un seul nœud d’un cluster.

+

Les fichiers qui sont plus vieux que fileAgeMax sont ignorés. Cela +peut être modifié à n’importe quelle limite de temps spécifiée dans les configurations en utilisant +l’option fileAgeMax <duration>. Par défaut, dans les composants +autre que poll, cette option est désactivé en étant défini à zéro (0). Comme il s’agit d’une +option de durée, les unités sont en secondes par défaut, mais il est possible de definir l’option +en utilisant des minutes, heures, jours ou des semaines. Dans la composante de poll, fileAgeMax +est défini à 30 jours par défaut.

+
+

Sondage avancé (Advanced Polling)

+

Le poll intégré liste les répertoires distants et analyse les lignes renvoyées par les structures +paramiko.SFTPAttributes (similaires à os.stat) pour chaque fichier répertorié. +Il existe une grande variété de personnalisations disponibles car les ressources à poller +très différentes :

+
    +
  • on peut implémenter un rappel sarracenia.flowcb avec une routine poll +pour prendre en charge ces services, ce qui remplace le poll par défaut.

  • +
  • Certains serveurs ont des résultats non standard quand ils listent des fichiers, de sorte que l’on peut +sur-classer un rappel sarracenia.flowcb.poll avec le point d’entrée on_line pour normaliser leurs +réponses en utilisant quand même le flux de poll intégré.

  • +
  • Il existe de nombreux serveurs http qui fournissent des formats très différents quand ils +listent de fichiers, donc parfois, au lieu de reformater individuellement chaque ligne +ligne, il faut refaire l’analyse de la page au complet. Le point d’entrée on_html_page +dans sarracenia.flowcb.poll peut être modifié en la sur-classant également.

  • +
  • Il existe d’autres serveurs qui fournissent des services différents qui ne sont pas inclus +dans le poll par défaut. On peut implémenter une classe sarracenia.transfer supplémentaire +pour mieux comprendre le poll.

  • +
+

La sortie d’un poll est une liste de messages créés à partir des noms de fichiers +et les enregistrements SFTPAttributes, qui peuvent ensuite être filtrés par éléments +après gather dans l’algorithme.

+
+
+
+

COMPOSANTS

+

Tous les composants effectuent une combinaison de poll, de consommation et de publication. +avec des variations qui permettent soit la transmission d’annonces, ou soit aux +transferts de données. Les composants appliquent tous le seul même algorithme, +il suffit de commencer à partir de différents paramètres par défaut pour correspondre +à un cas d’utilisation commun.

+
+

CPUMP

+

cpump* est une implémentation du composant shovel en C. +Sur une base individuelle, il devrait être plus rapide qu’un seul téléchargeur python, +avec certaines limitations.

+
+
    +
  • ne télécharge pas de données, ne fait que diffuser des messages. (pelle, et non abonné)

  • +
  • s’exécute en tant qu’instance unique (pas d’instances multiples).

  • +
  • ne prend en charge aucun plugin.

  • +
  • ne prend pas en charge vip pour la haute disponibilité.

  • +
  • différentes bibliothèques d’expressions régulières : POSIX vs python.

  • +
  • ne supporte pas regex pour la commande strip (pas de regex non-greedy).

  • +
+
+

Cela peut donc généralement, mais pas toujours, servir de substitution à l`shovel`_ et à winnow.

+

L’implémentation C peut être plus facile à mettre à disposition dans des environnements spécialisés, +comme l’informatique de haute performance, car il y a beaucoup moins de dépendances que la version python. +Cela utilise également beaucoup moins de mémoire pour un rôle donné. Normalement la version python +est recommandé, mais il y a des cas où l’utilisation de l’implémentation C est raisonnable.

+

sr_cpump se connecte à un broker (souvent le même que le courtier de post) +et s’abonne aux notifications d’intérêt. Si _suppress_duplicates_ est actif, +à la réception d’un message, il recherche le champ integity du message dans la cache. Si le message est +trouvé, le fichier est déjà passé, de sorte que la notification est ignorée. Si ce n’est pas le cas, alors +le fichier est nouveau, et la sum est ajoutée à la cache et la notification est publiée.

+
+
+

FLOW

+

Flow est la classe parent à partir de laquelle tous les autres composants, à l’exception de cpost et cpump, sont construits. +Flow n’a pas de comportement intégré. Les paramètres peuvent le faire agir comme n’importe quel autre composant python, +ou il peut être utilisé pour créer des composants définis par l’utilisateur. Généralement utilisé avec l’option flowMain +pour exécuter une sous-classe de flux définie par l’utilisateur.

+
+
+

POLL

+

poll est un composant qui se connecte à un serveur distant pour +vérifier divers répertoires pour certains fichiers. Lorsqu’un fichier est +présent, modifié, ou créé dans le répertoire distant, le programme +informe qu’il y a nouveau produit.

+

Le protocle de notification est défini ici sr3_post(7)

+

poll se connecte à un broker. À toutes les secondes de scheduled_interval (où bien +à des moment spécifié par scheduled_hour et scheduled_minute), il se connecte à

+

une pollUrl (sftp, ftp, ftps). Pour chacun des path définis, les contenus sont listés.

+

Le poll est seulement destinée à être utilisée pour les fichiers récemment modifiés. +L’option fileAgeMax élimine les fichiers trop anciens. Lorsqu’un fichier correspondant +à un modèle donné est trouvé by accept, poll crée un message de notification pour ce produit.

+

Le message est ensuite verifié dans la cache dupliqué (limité en temps par l’option +nodupe_ttl) pour empêcher la publication de fichiers qui ont déjà été vus.

+

poll peut être utilisé pour acquérir des fichiers distants en conjonction avec un sarra qui est +abonné aux notifications d’un post, pour les télécharger et les republier à partir d’une pompe de données.

+

L’option de pollUrl spécifie ce qui est nécessaire pour se connecter au serveur distant

+

pollUrl protocol://<user>@<server>[:port]

+
+
::

(par défaut : Aucun et il est obligatoire de le définir )

+
+
+

La pollUrl doit être définie avec le minimum d’informations requises… +sr_poll utilise le paramètre pollUrl non seulement lors du poll, mais aussi +dans messages sr3_post produits.

+

Par exemple, l’utilisateur peut définir :

+

pollUrl ftp://myself@myserver

+

Et compléter les informations nécessaires dans le fichier d’informations d’identification (credentials) avec la ligne :

+

ftp://myself:mypassword@myserver:2121 passive,binary

+

Poll rassemble des informations sur les fichiers distants, pour créer des messages à leur sujet. +La méthode gather intégrée utilise les protocoles sarracenia.transfer. Actuellement sftp, ftp et http +sont implémentés.

+
+

Scans répétés et VIP

+

Lorsque plusieurs serveurs coopèrent pour polller un serveur distant, +le paramètre vip est utilisé pour décider quel serveur il faut réellement poller. +Tous les serveurs participants s’abonnent à l’endroit où poll est publié, +et utilisent les résultats pour remplir la cache de suppression des doublons, afin que +que si l’adresse VIP se déplace, les serveurs alternatifs ont des indications actuelles +de ce qui a été affiché.

+
+
+
+

POST or WATCH

+

sr3_post affiche la disponibilité d’un fichier en créant une annonce. +Contrairement à la plupart des autres composants de sarracenia qui agissent comme des démons, +sr3_post est une invocation qui poste et se termine en une seul fois. +Pour mettre les fichiers à la disposition des abonnés, sr3_post envoie les annonces +à un serveur AMQP ou MQTT, également appelé broker.

+

Il existe de nombreuses options pour la détection des modifications dans les répertoires, pour +une discussion détaillée des options dans Sarracenia, voir DetectFileReady.html

+

Cette page de manuel concerne principalement l’implémentation de python, +mais il y a aussi une implémentation en C, qui fonctionne presque pareille. +Différences:

+
+
    +
  • les plugins ne sont pas pris en charge dans l’implémentation C.

  • +
  • L’implémentation C utilise des expressions régulières POSIX, la grammaire python3 est légèrement différente.

  • +
  • lorsque l’option sleep (utilisée uniquement dans l’implémentation C) est définie sur > 0, +cela transforme sr_cpost en un démon qui fonctionne comme un watch.

  • +
+
+

Le composant watch est utilisé pour surveiller les répertoires à la recherche de nouveaux fichiers. +Cela est équivalent à poster (ou cpost) avec l’option sleep réglée sur >0.

+

L’option [-pbu|–post_baseUrl url,url,…] spécifie l’emplacement a partir d’ou +les abonnés pourront télécharger. Il y a généralement un message par fichier. +Format de l’argument de l’option post_baseUrl

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+file:
+
+
+

Lorsque plusieurs URL sont données sous forme de liste séparée par des virgules à post_baseUrl, les +url fournies sont utilisées dans le style round-robin, pour fournir une forme d’équilibrage de charge.

+

L’option [-p|–path path1 path2 .. pathN] spécifie le chemin d’accès des fichiers +à annoncer. Il y a généralement un message par fichier. +Format de l’argument de l’option path

+
/absolute_path_to_the/filename
+or
+relative_path_to_the/filename
+
+
+

L’option -pipe peut être spécifiée pour que sr3_post lise les noms de chemin d’accès à partir de la norme +d’entrée également.

+

Exemple d’invocation de sr3_post:

+
sr3_post -pb amqp://broker.com -pbu sftp://stanley@mysftpserver.com/ -p /data/shared/products/foo
+
+
+

Par défaut, sr3_post lit le fichier /data/shared/products/foo et calcule sa somme de contrôle. +Il crée ensuite un message de publication, se connecte à broker.com en tant qu’utilisateur « invité » +(informations d’identification par défaut) et envoie la publication aux vhost ‘/’ par défaut et à +l’échange par défaut. L’échange par défaut est le préfixe xs_ suivi du nom d’utilisateur du courtier, +où la valeur par défaut ‘xs_guest’. Un abonné peut télécharger le fichier /data/shared/products/foo en +s’authentifiant en tant qu’utilisateur stanley sur mysftpserver.com en utilisant le protocole sftp pour +broker.com en supposant qu’il dispose des informations d’identification appropriées. +La sortie de la commande est la suivante

+
[INFO] Published xs_guest v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' sum=d,82edc8eb735fd99598a1fe04541f558d parts=1,4574,1,0,0
+
+
+

Dans MetPX-Sarracenia, chaque article est publié sous un certain thème. +La ligne du journal commence par ‘[INFO]’, suivie du topic du post. +Les thèmes dans AMQP sont des champs séparés par un point. Notez que les thèmes MQTT utilisent +une barre oblique (/) comme séparateur de thème. Le thème complet commence par +a topicPrefix (voir option), version v03, suivi d’un sous-thème (voir option) +ici la valeur par défaut, et le chemin du fichier séparé par des points +data.shared.products.foo.

+

Le deuxième champ de la ligne du journal est l’avis de message. Il se compose d’un horodatage +20150813161959.854 et l’URL source du fichier dans les 2 derniers champs.

+

Le reste des informations est stocké dans des en-têtes de message AMQP, constitués de paires clé=valeur. +L’en-tête sum=d,82edc8eb735fd99598a1fe04541f558d donne l’empreinte du fichier (ou somme de contrôle). +Ici, d signifie la somme de contrôle md5 effectuée sur les données et 82edc8eb735fd99598a1fe04541f558d +est la valeur de la somme de contrôle. Le parts=1,4574,1,0,0 indique que le fichier est disponible en 1 partie de 4574 octets +(la taille du fichier.) Le 1,0,0 restant n’est pas utilisé pour les transferts de fichiers avec une seule partie.

+

un autre exemple:

+
sr3_post -pb amqp://broker.com -pbd /data/web/public_data -pbu http://dd.weather.gc.ca/ -p bulletins/alphanumeric/SACN32_CWAO_123456
+
+
+

Par défaut, sr3_post lit le fichier /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(concaténation du chemin d’accès post_baseDir et relatif de l’URL source pour obtenir le chemin d’accès au fichier local) +et calcule sa somme de contrôle. Il crée ensuite un message de publication, se connecte à broker.com en tant qu’utilisateur « invité » +(informations d’identification par défaut) et envoie la publication aux hôtes par défaut ‘/’ et échange ‘xs_guest’.

+

Un abonné peut télécharger le fichier http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 à l’aide de http +sans authentification sur dd.weather.gc.ca.

+
+

Partitionnement de fichiers

+

l’utilisation de l’option blocksize n’a aucun effet dans sr3. Il est utilisé pour faire le partitionnement de fichiers, +et il redeviendra efficace à l’avenir, avec la même sémantique.

+
+
+
+

SARRA

+

sarra est un programme qui s’abonne aux notifications de fichiers, +acquiert les fichiers, et les réannonce à leurs nouveaux emplacements. +Le protocole de notification est défini ici sr3_post(7)

+

sarra se connecte à un broker (souvent le même que le serveur de fichiers distant) +et s’abonne aux notifications d’intérêt. Il utilise les informations de la notification +permettant de télécharger le fichier sur le serveur local sur lequel il s’exécute. +Il publie ensuite une notification pour les fichiers téléchargés sur un courtier (généralement sur le serveur local).

+

sarra peut être utilisé pour acquérir des fichiers auprès de sr3_post(1) +ou watch ou pour reproduire un dossier accessible sur le Web (WAF), +qui annoncent ses produits.

+

sr_sarra est un sr_subscribe(1) aves les préréglages suivants:

+
mirror True
+
+
+
+

Exigences spécifiques de consommation

+

Si les messages sont postés directement à partir d’une source l’échange utilisé est ‘xs_<brokerSourceUsername>’. +Pour se protéger contre les utilisateurs malveillants, les administrateurs doivent définir sourceFromExchange à True. +Ces messages ne peuvent pas contenir un champ cluster ou source d’origine +ou un utilisateur malveillant peut définir les valeurs de manière incorrecte.

+
    +
  • sourceFromExchange <booléan> (défaut: False)

  • +
+

À la réception, le programme définira ces valeurs dans la classe parente (ici +cluster est la valeur de l’option cluster tirée de default.conf) :

+

msg[‘source’] = <brokerUser> +msg[‘from_cluster’] = cluster

+

remplacer toutes les valeurs présentes dans le message. Ce paramètre +doit toujours être utilisé lors de l’ingestion de données provenant d’un +échange d’utilisateurs.

+
+
+
+

SENDER

+

sender est un composant dérivé de subscribe +utilisé pour envoyer des fichiers locaux à un serveur distant à l’aide d’un protocole de transfert de fichiers, principalement SFTP. +sender est un consommateur standard, utilisant tous les paramètres AMQP normaux pour les courtiers, les échanges, +et toutes les files d’attente, et tout les filtres standard côté client avec accept, reject et after_accept.

+

Souvent, un courtier annoncera des fichiers à l’aide d’un protocole distant tel que HTTP, +mais pour l’expéditeur, il s’agit en fait d’un fichier local. Dans de tels cas, on +voir un message : ERROR: The file to send is not local. +Un plugin after_accept convertira l’URL Web en un fichier local:

+
baseDir /var/httpd/www
+flowcb sarracenia.flowcb.tolocalfile.ToLocalFile
+
+
+

Ce plugin after_accept fait partie des paramètres par défaut pour les expéditeurs, mais +doit encore spécifier baseDir pour qu’il fonctionne.

+

Si un post_broker est défini, sender vérifie si le nom du cluster est donné +par l’option to si elle se trouve dans l’un des clusters de destination du message. +Si ce n’est pas le cas, le message est ignoré.

+
+

CONFIGURATION 1 : RÉPLICATION POMPE À POMPE

+

Pour la réplication de la pompe, mirror est défini sur True (valeur par défaut).

+

baseDir fournit le chemin d’accès au répertoire qui, lorsqu’il est combiné avec le chemin relatif +dans la notification sélectionnée, donne le chemin absolu du fichier à envoyer. +La valeur par défaut est Aucun, ce qui signifie que le chemin d’accès dans la notification est le chemin absolu.

+

Dans un subscriber, baseDir représente le préfixe du chemin relatif en amont, +et est utilisé comme modèle pour se faire remplacer dans le répertoire de base actuellement sélectionné +(à partir d’une option baseDir ou directory) dans les champs de message : ‘fileOp’, +qui sont utilisés lors de la mise en miroir de liens symboliques ou de fichiers renommés.

+

La sendTo définit le protocole et le serveur à utiliser pour livrer les produits. +Sa forme est un url partiel, par exemple : ftp://myuser@myhost. +Le programme utilise le fichier ~/.conf/sarra/credentials.conf pour obtenir les détails restants +(mot de passe et options de connexion). Les protocoles pris en charge sont ftp, ftps et sftp. +Si l’utilisateur doit implémenter un autre mécanisme d’envoi, il fournirait le script du plugin +par l’option do_send.

+

Sur le site distant, le post_baseDir sert à la même chose que le +baseDir sur ce serveur. La valeur par défaut est None, ce qui signifie que le chemin d’accès livré +est l’absolu.

+

Maintenant, nous sommes prêts à envoyer le produit… par exemple, si la notification sélectionnée ressemble à ceci :

+

20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product

+

sr_sender effectue la pseudo-livraison suivante :

+

Envoie le fichier locale [baseDir]/relative/path/to/IMPORTANT_product +à sendTo/[post_baseDir]/relative/path/to/IMPORTANT_product +(kbytes_ps est supérieur à 0, le processus tente de respecter +cette vitesse de livraison… ftp,ftps,ou sftp)

+

À ce stade, une configuration de pompe à pompe doit envoyer la notification à distance… +(Si la post_broker n’est pas définie, il n’y aura pas d’affichage… juste la réplication des produits)

+

La notification sélectionnée contiennent toutes les bonnes informations +(attributs de thème et d’en-tête) à l’exception du champ url dans l’avis… dans notre exemple : http://this.pump.com/

+

Par défaut, sr_sender place la sendTo dans ce champ. +L’utilisateur peut l’écraser en spécifiant l’option post_baseUrl. Par exemple:

+

post_baseUrl http://remote.apache.com

+

L’utilisateur peut fournir un script on_post. Juste avant que le message ne soit +publié sur les post_broker et post_exchange, le +on_post script s’appelle… avec l’instance de classe sr_sender comme argument. +Le script peut effectuer ce que vous voulez… s’il renvoie False, le message ne sera pas +publié. Si la valeur est True, le programme poursuivra le traitement à partir de là.

+

FIXME : Exemple de configuration manquant.

+
+
+

CONFIGURATION DE DESTINATION 2 : DIFFUSION DE TYPE METPX-SUNDEW

+

Dans ce type d’utilisation, nous n’aurions généralement pas reposté… mais si le +post_broker et post_exchange (url,**on_post**) sont définis, +le produit sera annoncé (avec son éventuel nouvel emplacement et son nouveau nom). +Réintroduisons les options dans un ordre différent +avec quelques nouveaux pour faciliter l’explication.

+

Il y a 2 différences avec le cas précédent : +les options directory et filename.

+

Le baseDir est le même, tout comme la +sendTo et les options post_baseDir.

+

L’option répertoire définit un autre « chemin relatif » pour le produit +à destination. Il est marqué aux options accept définies après lui. +Si une autre séquence de directory/accept suit dans le fichier de configuration, +le deuxième répertoire est marqué pour les acceptations suivantes et ainsi de suite.

+

Les modèles accept/reject s’appliquent à l’URL de notification du message comme ci-dessus. +Voici un exemple, voici quelques options de configuration ordonnées :

+
directory /my/new/important_location
+
+accept .*IMPORTANT.*
+
+directory /my/new/location/for_others
+
+accept .*
+
+
+

Si la notification sélectionnée est, comme ci-dessus, ceci :

+

20150813161959.854 http://this.pump.com/ relative/path/to/IMPORTANT_product

+

Il a été sélectionné par la première option accept. Le chemin relatif distant devient +/my/new/important_location … et sr_sender effectue la pseudo-livraison suivante :

+

envoie le fichier local [baseDir]/relative/path/to/IMPORTANT_product +à destination/[post_baseDir]/my/new/important_location/IMPORTANT_product

+

Habituellement, cette façon d’utiliser sr_sender n’exigerait pas l’affichage du produit. +Mais si post_broker and post_exchange sont fournis, et url , comme ci-dessus, est défini sur +http://remote.apache.com, alors sr_sender reconstruirait :

+

Thème: v03.my.new.important_location.IMPORTANT_product

+

Notice: 20150813161959.854 http://remote.apache.com/ my/new/important_location/IMPORTANT_product

+
+
+
+

SHOVEL

+

shovel copie les messages sur un courtier (donné par l’option broker) à +un autre (donné par l’option post_broker) soumis au filtrage +par (exchange, subtopic, et éventuellement, accept/reject.)

+

L’option topicPrefix doit être définie sur :

+
+
+
+

shovel est un flux avec les préréglages suivants :

+
+

no-download True +suppress_duplicates off

+
+
+
+

SUBSCRIBE

+

Subscribe est le composant de flux de téléchargement normal, qui se connectera à un courtier, télécharger +les fichiers configurés, puis transférer les messages avec une baseUrl modifiée.

+
+
+

WATCH

+

Surveille un répertoire et publie des messages lorsque les fichiers dans le répertoire changent. +Ses arguments sont très similaires à sr_post <sr_post. +Dans la suite MetPX-Sarracenia, l’objectif principal est d’afficher la disponibilité et modifications +de ses dossiers. Les abonnés utilisent sr_subscribe pour consommer le message et télécharger les fichiers changés.

+

Les messages sont envoyés à un serveur AMQP, également appelé courtier, spécifié avec l’option [ -pb|–post_broker broker_url ].

+

The [-post_baseUrl|–pbu|–url url] option specifies the protocol, credentials, host and port to which subscribers +will connect to get the file.

+

Format of argument to the url option:

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+[ftp|http|sftp]://[user[:password]@]host[:port]/
+or
+file:
+
+
+

L’option[-p|–chemin] indique à sr_watch ce qu’il faut chercher. +Si le path spécifie un répertoire, sr_watches crée un message quand +un fichier dans ce répertoire qui est créé, modifié ou supprimé. +Si le path spécifie un fichier, sr_watch surveille uniquement ce fichier. +Dans l’avis, il est spécifié avec le chemin du produit. +Il y a généralement un message par fichier.

+

Un exemple d’une excution de sr_watch vérifiant un fichier:

+
sr3 --post_baseUrl sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo --post_broker amqp://broker.com start watch/myflow
+
+
+

Ici, sr_watch vérifie les événements sur le fichier /data/shared/products/foo. +Les paramètres par défaut des rapports d’événements si le fichier le fichier est modifié ou supprimé. +Lorsque le fichier est modifié, sr_watch lit le fichier /data/shared/products/foo. +et calcule sa somme de contrôle. Il construit ensuite un message, se connecte à broker.com +en tant qu’utilisateur’guest’ (informations d’identification par défaut). +et envoie le message aux valeurs par défaut vhost ‘/’ et échange ‘xs_stanley’ (échange par défaut)

+

Un abonné peut télécharger le fichier /data/shared/products/foo en se connectant en tant qu’utilisateur stanley. +sur mysftpserver.com en utilisant le protocole sftp à broker.com en supposant qu’il a les informations d’identification appropriées.

+

La sortie de la commande est la suivante:

+
[INFO] v03.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo'
+      source=guest parts=1,256,1,0,0 sum=d,fc473c7a2801babbd3818260f50859de
+
+
+

Dans MetPX-Sarracenia, chaque article est publié sous un certain thème. +Après le ‘[INFO]’, l’information suivante donne le fBtopic* du fichier +publié. Les thèmes dans AMQP sont un champ hierarchique, avec chaque sous-thème séparé par une points. Dans +MetPX-Sarracénie il est constitué d’un topic_prefix par défaut : version V02, d’une action post.., +suivi par subtopic par défaut : le chemin du fichier séparé par des points, ici, data.shared.products.foo.

+

Après la hiérarchie des thèmes vient le corps de l’avis. Il se compose d’un temps 20150813161959.854, +et l’url source du fichier dans les 2 derniers champs.

+

La ligne restante donne des informations qui sont placées dans l’en-tête du message amqp. +Ici, il se compose de source=guest, qui est l’utilisateur amqp, parts=1,256,0,0,0,1.., +qui proposent de télécharger le fichier en 1 partie de 256 octets (la taille réelle du fichier), suivi de 1,0,0,0. +donne le nombre de blocs, le nombre d’octets restants et le nombre d’octets actuel. +bloc. sum=d,fc473c7a2801babbd3818260f50859de mentionne les informations de la somme de contrôle, +ici, d signifie la somme de contrôle md5 effectuée sur les données, et fc473c7a2801babbd3818260f50859de. +est la valeur de la somme de contrôle. Lorsque l’événement sur un fichier est une suppression, sum=R,0 R +signifie de supprimer un fichier.

+

Un autre exemple avec un fichier:

+
sr3 --post_baseDir /data/web/public_data --post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric/SACN32_CWAO_123456 -post_broker amqp://broker.com start watch/myflow
+
+
+

Par défaut, sr3_watch vérifie le fichier /data/web/public_data/bulletins/alphanumériques/SACN32_CWAO_123456 +(concaténer le répertoire base_dir et le chemin relatif de l’url source pour obtenir le chemin du fichier local). +Si le fichier change, il calcule sa somme de contrôle. Il construit ensuite un message, se connecte à broker.com en tant qu’utilisateur’guest’. +(informations d’identification par défaut) et envoie le message aux valeurs par défaut vhost’/’ et exchange’sx_guest’ (échange par défaut)

+

Un abonné peut télécharger le fichier http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_CWAO_123456 en utilisant http. +sans authentification sur dd.weather.gc.ca.

+

Un exemple de vérification d’un répertoire:

+
sr3 -post_baseDir /data/web/public_data -post_baseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric --post_broker amqp://broker.com start watch/myflow
+
+
+

Ici, sr3_watch vérifie la création de fichiers (modification) dans /data/web/public_data/bulletins/alphanumérique. +(concaténer le répertoire base_dir et le chemin relatif de l’url source pour obtenir le chemin du répertoire). +Si le fichier SACN32_CWAO_123456 est créé dans ce répertoire, sr3_watch calcule sa somme de contrôle. +Il construit ensuite un message, se connecte à broker.com en tant qu’utilisateur’guest’. +(informations d’identification par défaut) et envoie le message à exchange’amq.topic’ (échange par défaut)

+

Un abonné peut télécharger le fichier créé/modifié http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_CWAO_123456 en utilisant http. +sans authentification sur dd.weather.gc.ca.

+
+
+

WINNOW

+

winnow est un programme qui s’abonne aux notifications de fichiers, +et réenregistre les notifications, en supprimant les notifications redondantes.

+

La méthode de décider quels messages d´annonce sont redondants varient selon le cas d´usage. +Normalement, les messages comprenned un champs Identity qui avec une somme de contrôle +du ficher, tel que décrit dans sr_post(7) +Il y bien d´autres cas d´usage discutés dans Supprimer les doublons

+

winnow a les options suivante forcés:

+
no-download True
+suppress_duplicates on
+accept_unmatch True
+
+
+

La durée de vie des suppress_duplicates peut être ajustée, mais elle est toujours active.

+

winnow se connecte à un broker (souvent le même que le courtier d’affichage). +et souscrit aux notifications d’intérêt. Sur réception d´un avis, il cherche sa sum +dans son cache. s’il est trouvé, le fichier est déjà passé, de sorte que la notification +est ignorée. Si ce n’est pas le cas, le fichier est nouveau, et le sum est ajouté. +dans le cache et l’avis est affiché.

+

winnow peut être utilisé pour couper les messages de sr3_post, +sr_poll(1) ou sr_watch(1) etc….. C’est +utilisé lorsqu’il y a plusieurs sources de données identiques, de sorte que les +clients ne téléchargent que le fichier une seule fois, à partir de la première +source qui les a publié.

+

winnow peut être utilisé pour couper les messages de post, sr3_post, +sr3_cpost, sr3_cpump, +poll ou watch etc… +C’est utilisé lorsqu’il y a plusieurs sources de données identiques, de sorte que +les clients ne téléchargent que le fichier une seule fois, à partir de la première +source qui les a publié.

+
+
+
+

Configurations

+

Si on a une configuration prête à l’emploi appelée q_f71.conf, elle peut être +ajoutée à la liste des noms connus avec

+
subscribe add q_f71.conf
+
+
+

Dans ce cas-ci, q_f71.conf est inclus avec les exemples fournis, donc add le trouve +dans les exemples et le copie dans le répertoire des configurations actives. +Chaque fichier de configuration gère les consommateurs pour une seule file +d’attente sur le courtier. Pour visualiser les configurations disponibles, +utilisez:

+
$ subscribe list
+
+  configuration examples: ( /usr/lib/python3/dist-packages/sarra/examples/subscribe )
+            all.conf     all_but_cap.conf            amis.conf            aqhi.conf             cap.conf      cclean_f91.conf
+      cdnld_f21.conf       cfile_f44.conf        citypage.conf       clean_f90.conf            cmml.conf cscn22_bulletins.conf
+        ftp_f70.conf            gdps.conf         ninjo-a.conf           q_f71.conf           radar.conf            rdps.conf
+           swob.conf           t_f30.conf      u_sftp_f60.conf
+
+  user plugins: ( /home/peter/.config/sarra/plugins )
+        destfn_am.py         destfn_nz.py       msg_tarpush.py
+
+  general: ( /home/peter/.config/sarra )
+          admin.conf     credentials.conf         default.conf
+
+  user configurations: ( /home/peter/.config/sarra/subscribe )
+     cclean_f91.conf       cdnld_f21.conf       cfile_f44.conf       clean_f90.conf         ftp_f70.conf           q_f71.conf
+          t_f30.conf      u_sftp_f60.conf
+
+
+

On peut ensuite le modifier à l’aide de

+
$ subscribe edit q_f71.conf
+
+
+

(La commande d’édition utilise la variable d’environnement EDITOR, si elle est présente. +Une fois les changements complétés, on peut démarrer la configuration avec

+
$ subscibe foreground q_f71.conf
+
+
+

Que contiennent les fichiers ? Voir la section suivante :

+
    +
  • La barre oblique (/) est utilisée comme séparateur de chemin dans les fichiers de configuration Sarracenia sur tous les +systèmes d’exploitation. L’utilisation de la barre oblique inverse comme séparateur () (tel qu’utilisé dans la +cmd shell de Windows) risque de ne pas fonctionner correctement. Lorsque des fichiers sont lu dans Windows, le chemin d’accès +est immédiatement converti en utilisant la barre oblique. Ceci est pour s’assurer que les options reject, accept, et +strip peuvent filtrer des expressions correctement. C’est pour cela qu’il est toujours important d’utiliser la barre +oblique (/) quand un séparateur est nécessaire.

  • +
+

Example:

+
directory A
+accept X
+
+
+

Place les fichiers correspondant à X dans le répertoire A.

+
+
au lieu::

accept X +directory A

+
+
+

Place les fichiers correspondant à X dans le répertoire de travail actuel, +et le paramètre directory A ne fait rien par rapport à X.

+

Pour fournir une description non fonctionnelle de la configuration ou des +commentaires, utilisez des lignes commençant par #. Toutes les options +sont sensibles aux majuscules et minuscules. Debug n’est pas le même +que debug ou DEBUG. Il s’agit de trois options différentes (dont deux +n’existent pas et n’auront aucun effet, mais devrait générer une +avertissement ´unknown option´).

+

Les options et les paramètres de ligne de commande sont équivalents. Chaque +paramètre de ligne de commande a une version longue correspondante commençant +par’–’. Par exemple, -u est la forme courte de –url. On peut +aussi spécifier cette option dans un fichier de configuration. Pour ce faire, +utilisez la forme longue sans le’–’, suivi de sa valeur séparée par un +espace. Les éléments suivants sont tous équivalents :

+
+
    +
  • url <url>

  • +
  • -u <url>

  • +
  • –url <url>

  • +
+
+

Les réglages sont interprétés dans l’ordre. Chaque fichier est lu de haut en bas. +par exemple :

+

sequence #1:

+
reject .*\.gif
+accept .*
+
+
+

sequence #2:

+
accept .*
+reject .*\.gif
+
+
+

Dans la séquence #1, tous les fichiers se terminant par ‘gif’ sont rejetés. Dans la séquence #2, le +accept .* (qui accepte tout) est lu avant la déclaration du rejet, +donc le rejet n’a aucun effet. Certaines options ont une portée globale, plutôt que d’être +interprété dans l’ordre. Dans ces cas, la dernière déclaration remplace celle qu’il y avait plus tôt dans le fichier..

+

Les options qui doivent être réutilié dans de fichier de config différents peuvent +être groupés dans des fichiers include:

+
+
    +
  • –include <includeConfigPath>

  • +
+
+

The includeConfigPath would normally reside under the same config dir of its +master configs. If a URL is supplied as an includeConfigPATH, then a remote +configuraiton will be downloaded and cached (used until an update on the server +is detected.) See Remote Configurations for details.

+

Environment variables, and some built-in variables can also be put on the +right hand side to be evaluated, surrounded by ${..} The built-in variables are:

+
+
    +
  • ${BROKER_USER} - le nom d’utilisateur pour l’authentification auprès du courtier (par exemple, anonyme)

  • +
  • ${PROGRAM} - le nom du composant (subscribe, shovel, etc…)

  • +
  • ${CONFIG} - le nom du fichier de configuration en cours d’exécution.

  • +
  • ${HOSTNAME} - le hostname qui exécute le client.

  • +
  • ${RANDID} - Un ID aléatoire qui va être consistant pendant la duration d’une seule invocation.

  • +
+
+
+
+

flowCallbacks

+

Sarracenia utilise largement de petits extraits de code python qui personnalisent le +traitement appelé flowCallback. Flow_callbacks définit et utiliser des paramètres supplémentaires

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

Il existe également une forme plus courte pour exprimer la même chose:

+
callback log
+
+
+

Quoi qu’il en soit, le module fait référence au fichier sarracenia/flowcb/msg/log.py dans le +package installé. Dans ce fichier, la classe Log est celle qui a été recherchée pour le point d’entré. +Le fichier log.py inclus dans le package est le suivant:

+
from sarracenia.flowcb import FlowCB
+import logging
+
+logger = logging.getLogger( __name__ )
+
+class Log(Plugin):
+
+  def after_accept(self,worklist):
+      for msg in worklist.incoming:
+          logger.info( "msg/log received: %s " % msg )
+      return worklist
+
+
+

C’est une classe python normale, déclarée comme enfant de la classe sarracenia.flowcb.FlowCB. +Les méthodes (noms de fonction) dans le plugin décrivent quand +ces routines seront appelées. Pour plus de détails, consultez le +Programmer’s Guide

+

Pour ajouter un traitement spécial des messages, créez un module en python +et faites-le inclure des points d’entrée.

+

Il existe également flowCallbackPrepend qui ajoute une classe flowCallback à l’avant +de la liste (qui détermine l’ordre d’exécution relatif entre les classes flowCallback.)

+
+

Options callback

+

les rappels livrés avec metpx-sr3 suivent la convention suivante :

+
    +
  • ils sont placés dans l’arborescence du répertoire sarracenia/flowcb.

  • +
  • le nom de la classe principale est le même que le nom du fichier qui la contient.

  • +
+

nous fournissons donc le sucre syntaxique suivant:

+
callback log    (is equivalent to *flowCallback sarracenia.flowcb.log.Log* )
+
+
+

Il y a de même un callback_prepend à remplir.

+
+
+

Importation d’extensions

+

L’option import fonctionne d’une manière familière aux développeurs Python, +qui les rends disponibles pour une utilisation par le noyau Sarracenia, ou flowCallback. +Les développeurs peuvent ajouter des protocoles supplémentaires pour les messages ou +transfert de fichiers. Par exemple:

+
import torr
+
+
+

serait un nom raisonnable pour un protocole de transfert pour récupérer des +ressources avec le protocole bittorrent. Un squelette d’une telle chose +ressemblerait à ceci

+
import logging
+
+logger = logging.getLogger(__name__)
+
+import sarracenia.transfer
+
+class torr(sarracenia.transfer.Transfer):
+    pass
+
+logger.warning("loading")
+
+
+

Pour plus de détails sur la mise en œuvre des prolongations, consultez le +Programmer’s Guide

+
+
+

Plugins v2 Obsolètes

+

Il existe également un style de plugins plus ancien (v2). Qui sont généralement +précédé du nom du plugin

+
msg_to_clusters DDI
+msg_to_clusters DD
+
+on_message msg_to_clusters
+
+
+

Un paramètre ‘msg_to_clusters’ est nécessaire par le plugin msg_to_clusters +référencés dans le on_message les plugins v2 sont un peu plus +encombrant à écrire. Ils sont inclus ici pour être complets, mais +les utilisateurs doivent utiliser la version 3 (flowCallback ou extensions) +discuté ensuite) lorsque cela est possible.

+

Raisons d’utiliser des plugins de style plus récents:

+
    +
  • La prise en charge de l’exécution des plugins v2 est réalisée à l’aide d’un flowcb +appelé v2wrapper. Il effectue beaucoup de traitement pour conclure +les structures de données v3 pour ressembler à celles de la v2, puis a +pour propager les modifications. C’est un peu cher.

  • +
  • les extensions de style plus récentes sont des modules python ordinaires, contrairement à +v2 qui nécessitent des incantations magiques mineures.

  • +
  • lorsqu’un module v3 (flowCallback ou importé) présente une erreur de syntaxe, +tous les outils de l’interpréteur python s’appliquent, ce qui donne +beaucoup plus de commentaires au codeur. Avec la v2, il +dit seulement qu’il y a quelque chose qui ne va pas, beaucoup plus difficile à déboguer.

  • +
  • l’api v3 est strictement plus puissante que la v2, car elle fonctionne +sur des groupes de messages, plutôt que sur des messages individuels.

  • +
+
+
+

Environment Variables

+

On peut aussi utiliser des variables d´environnement avec la syntax +${ENV} ou ENV est le nom d´une variable d´environnement. S´il faut +définir une variable d´environnement pour une utilisation par Sarracenia, +on peut l´indiquer dans un fichier de configuration:

+
declare env HTTP_PROXY=localhost
+
+
+
+
+

Fichiers journal et Suivi

+
    +
  • +
    debug

    L’option de déboggage debug est identique à l’utilisation de loglevel debug.

    +
    +
    +
  • +
  • logMessageDump (défaut: off) booléen flag +s’il est défini, tous les champs d’un message sont imprimés, plutôt qu’une simple référence url/chemin.

  • +
  • +
    logEvents ( défaut after_accept,after_work,on_housekeeping )

    émettre des messages de journal standard aux points donnés du traitement des messages. +autres valeurs : on_start, on_stop, post, gather, … etc…

    +
    +
    +
  • +
  • +
    loglevel ( défaut: info )

    Le niveau de journalisation exprimé par la journalisation de python. Les valeurs possibles sont : critical, error, info, warning, debug.

    +
    +
    +
  • +
  • –logStdout ( défaut: False ) CAS D’UTILISATION EXPÉRIMENTAL POUR DOCKER

    +
    +

    logStdout désactive la gestion des journaux. Mieux utilisé sur la ligne de commande, car il y a +certains risques de créer des fichiers stub avant que les configurations ne soient complètement analysées

    +
    sr3 --logStdout start
    +
    +
    +

    Tous les processus lancés héritent de leurs descripteurs de fichier du parent, donc toutes les sorties sont comme une session interactive.

    +

    Cela contraste avec le cas normal, où chaque instance prend soin de ses journaux, en tournant et en purgeant périodiquement. +Dans certains cas, on veut que d’autres logiciels s’occupent des logs, comme dans docker, où c’est préférable pour tous +que la journalisation soie en sortie standard.

    +

    Il n’a pas été mesuré, mais il y a une probabilité raisonnable que l’utilisation de logStdout avec de grandes configurations (des dizaines +d’instances/processus configurés) entraînera soit une corruption des journaux, soit limitera la vitesse d’exécution de tous les processus +à écrire à stdout.

    +
    +
  • +
  • +
    log_reject <True|False> ( défaut: False )

    afficher un ligne de journal pour chaque message rejeté. Ceci peut produire des journeaux énorme. +D’habitude utilisé uniquement lors du debogage.

    +
    +
    +
  • +
  • +
    log <dir> ( défaut: ~/.cache/sarra/log ) (sur Linux)

    Répertoire dans lequel stocker les fichiers journaux.

    +
    +
    +
  • +
  • +
    statehost <False|True> ( défaut: False )

    Dans les grands centres de données, l’annuaire de base peut être partagé entre des milliers de +Nœuds. Statehost ajoute le nom du nœud après le répertoire de cache pour le rendre +unique à chaque nœud. Ainsi, chaque nœud a ses propres fichiers d’état et journaux. +Par exemple, sur un nœud nommé goofy, ~/.cache/sarra/log/ devient ~/.cache/sarra/goofy/log.

    +
    +
    +
  • +
  • +
    logRotateCount <max_logs> ( défaut: 5 )

    Nombre maximal de journaux archivés.

    +
    +
    +
  • +
  • +
    logRotateInterval <duration>[<time_unit>] ( défaut: 1 jour == 86,400 secondes )

    La durée de l’intervalle avec une unité de temps optionnelle (c’est-à-dire 5m, 2h, 3d)

    +
    +
    +
  • +
  • +
    permLog ( défaut: 0600 )

    Bits d’autorisation à définir sur les fichiers journaux.

    +
    +
    +
  • +
+

Voir le Subscriber Guide <../How2Guides/subscriber.html> pour une discussion plus détaillée sur les +options de journalisations et de techniques.

+
+
+

IDENTIFICATION (CREDENTIALS)

+

Normalement, on ne spécifie pas de mots de passe dans les fichiers de +configuration. Ils sont plutôt placés dans le fichier d´information d´identifcation:

+
edit ~/.config/sr3/credentials.conf
+
+
+

Pour chaque url spécifié qui nécessite un mot de passe, on place une entrée +correspondante dans credentials.conf. L’option broker définit toutes les +informations d’identification pour se connecter au serveur RabbitMQ.

+
    +
  • broker amqp{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

  • +
+
(par défaut: amqps://anonymous:anonymous@dd.weather.gc.ca/ )
+
+
+

Pour tous les programmes de sarracenia, les parties confidentielles +des justificatifs d’identité sont stockées uniquement dans +~/.config/sarra/credentials.conf. Cela comprend les mots de passe pour la destination +et le courtier ainsi que les paramètres requis par les composants. Une entrée par ligne. Exemples :

+
    +
  • amqp://user1:password1@host/

  • +
  • amqps://user2:password2@host:5671/dev

  • +
  • amqps://usern:passwd@host/ login_method=PLAIN

  • +
  • sftp://user5:password5@host

  • +
  • sftp://user6:password6@host:22 ssh_keyfile=/users/local/.ssh/id_dsa

  • +
  • ftp://user7:password7@host passive,binary

  • +
  • ftp://user8:password8@host:2121 active,ascii

  • +
  • ftps://user7:De%3Aize@host passive,binary,tls

  • +
  • ftps://user8:%2fdot8@host:2121 active,ascii,tls,prot_p

  • +
  • https://ladsweb.modaps.eosdis.nasa.gov/ bearer_token=89APCBF0-FEBE-11EA-A705-B0QR41911BF4

  • +
+

Dans d’autres fichiers de configuration ou sur la ligne de commande, l’url +n’inclut pas le mot de passe ou spécification de clé. L’url donné dans les +autres fichiers est utilisé comme clé de recherche pour credentials.conf.

+
+

Details d’Identifiants

+

Vous devrez peut-être spécifier des options supplémentaires pour des entrées d’informations d’identification spécifiques. +Ces détails peuvent être ajoutés après la fin de l’URL, avec plusieurs détails séparés par des virgules (voir les exemples ci-dessus).

+

Détails pris en charge

+
- ``ssh_keyfile=<path>`` - (SFTP) Chemin d’accès au fichier de clés SSH
+- ``passive`` - (FTP) Utiliser le mode passif
+- ``active`` - (FTP) Utiliser le mode actif
+- ``binary`` - (FTP) Utiliser le mode binaire
+- ``ascii`` - (FTP) Utiliser le mode ASCII
+- ``ssl`` - (FTP) Utiliser SSL/FTP standard
+- ``tls`` - (FTP) Utiliser FTPS avec TLS
+- ``prot_p`` - (FTPS) Utiliser une connexion de données sécurisée pour les connexions TLS (sinon, du texte clair est utilisé)
+- ``bearer_token=<token>`` (or ``bt=<token>``) - (HTTP) Jeton de porteur pour l’authentification
+- ``login_method=<PLAIN|AMQPLAIN|EXTERNAL|GSSAPI>`` - (AMQP) Par défaut, la méthode de connexion sera automatiquement déterminée. Cela peut être remplacé en spécifiant explicitement une méthode de connexion, ce qui peut être nécessaire si un courtier prend en charge plusieurs méthodes et qu’une méthode incorrecte est automatiquement sélectionnée.
+
+
+
+

Note

+

Les informations d’identification SFTP sont facultatives, car sarracenia cherchera dans le répertoire .ssh +et utilisera les informations d’identification SSH normales qui s’y trouvent.

+

Ces chaînes sont codées en URL, donc si un compte a un mot de passe avec un caractère spécial, +son équivalent codé par l’URL peut être fourni. Dans le dernier exemple ci-dessus, +%2f signifie que le mot de passe réel est: /dot8 +L’avant-dernier mot de passe est : De:olonize. ( %3a étant la valeur codée url pour un caractère deux-points. )

+
+
+
+
+
+

TRAITEMENT PÉRIODIQUE

+

La plupart des traitements ont lieu à la réception d’un message, mais il y a une maintenance périodique +qui se produit à chaque intervalle housekeeping (la valeur par défaut est de 5 minutes.) À chaque housekeeping tous les +les plugins on_housekeeping configurés sont exécutés. Par défaut, trois sont présents :

+
    +
  • log - imprime « housekeeping » dans le journal.

  • +
  • nodupe - vieillit les anciennes entrées dans le cache de réception, afin de minimiser sa taille.

  • +
  • memory - vérifie l’utilisation de la mémoire du processus et redémarre si elle est trop grande.

  • +
+

Le journal contiendra des messages des trois plugins à chaque intervalle de housekeeping, et +si un traitement périodique supplémentaire est nécessaire, l’utilisateur peut configurer d’autres +plugins à exécuter avec l’option on_housekeeping.

+
+

sanity_log_dead <interval> (défaut: 1.5*housekeeping)

+

L’option sanity_log_dead définit la durée à prendre en compte avant de redémarrer +un composant.

+
+
+

nodup_ttl <off|on|999> (défaut: off)

+

Le nettoyage des éléments expirés dans la store de suppression des doublons se produit à chaque housekeeping.

+
+
+
+

RÉCUPÉRATION D’ERREUR

+

Les outils sont conçus pour fonctionner bien sans surveillance, et donc lorsque des erreurs transitoires se produisent, ils +essayent de récupérer élégamment. Il y a des délais d’attente sur toutes les opérations, et en cas de défaillance +détectée, le problème est noté pour une nouvelle tentative. Des erreurs peuvent se produire à plusieurs reprises :

+
    +
  • Établir une connexion avec le courtier.

  • +
  • perte d’une connexion avec le courtier

  • +
  • établir une connexion au serveur de fichiers pour un fichier (à télécharger).

  • +
  • perte d’une connexion au serveur.

  • +
  • pendant le transfert de données.

  • +
+

Initialement, les programmes essaient de télécharger (ou d’envoyer) un fichier un nombre de fois fixe (tentatives, par défaut: 3). +Si les trois tentatives de traitement du fichier échouent, le fichier est placé dans “réessayer”. +Le programme poursuit ensuite le traitement des nouveaux éléments. Lorsqu’il n’y a pas de nouveaux éléments, +le programme recherche un fichier à traiter dans la fil d’attente de nouvelles tentatives. Il vérifie ensuite si le fichier +est vieux, au-delà du retry_expire (par défaut : 2 jours). Si le fichier n’a pas expiré, alors +il déclenche une nouvelle série de tentatives de traitement du fichier. Si les tentatives échouent, il revient en arrière +dans la fil d’attente de nouvelles tentatives.

+

Cet algorithme garantit que les programmes ne restent pas bloqués sur un seul mauvais produit qui empêche +le reste de la fil d’attente en cours de traitement, et permet une récupération raisonnable et progressive, +permettant aux nouvelles données de circuler et en envoyant les anciennes données de manière opportuniste +lorsqu’il y a des lacunes.

+

Bien qu’un traitement rapide de bonnes données soit très souhaitable, il est important de ralentir en cas d’erreur. +Souvent, les erreurs sont liées à la charge, et une nouvelle tentative rapide ne fera qu’empirer les choses. +Sarracenia utilise le back-off exponentiel à de nombreux endroits pour éviter de surcharger un serveur lorsqu’il y a +des erreurs. Le back-off peut s’accumuler au point où les nouvelles tentatives peuvent être séparées d’une minute +ou deux. Une fois que le serveur recommence à répondre normalement, les programmes reviendront à la +vitesse normale de traitement.

+
+
+

EXEMPLES

+

Voici un court exemple complet de fichier de configuration

+
broker amqps://dd.weather.gc.ca/
+
+subtopic model_gem_global.25km.grib2.#
+accept .*
+
+
+

Ce fichier ci-dessus se connectera au courtier dd.weather.gc.ca, se connectant en tant qu’anonyme +avec mot de passe anonyme (par défaut) pour obtenir des annonces a propos de fichiers dans le +répertoire http://dd.weather.gc.ca/model_gem_global/25km/grib2. +Tous les fichiers qui arrivent dans ce répertoire ou en dessous seront téléchargés +dans le répertoire actif (ou simplement imprimé en sortie standard si l’option -n +a été spécifié.)

+

Divers exemples de fichiers de configuration sont disponibles ici :

+
+
+
+

QUEUES and MULTIPLE STREAMS

+

Lorsqu’il est exécuté, subscribe choisit un nom de fil d’attente, ou il écrit +à un fichier nommé d’après le fichier de configuration donné comme argument pour subscribe +avec un suffixe .queue ( .”configfile”.queue). +Si l’abonnement est arrêté, les messages publiés continuent de s’accumuler sur le +broker dans la fil d’attente. Lorsque le programme est redémarré, il utilise le nom de la fil d’attente +stocké dans ce fichier pour se connecter à la même fil d’attente et ne perd aucun message.

+

Les téléchargements de fichiers peuvent être parallélisés en exécutant plusieurs abonnements à l’aide de +la même fil d’attente. Les processus partageront la fil d’attente et téléchargerons +une partie de ce qui a été sélectionné. Il suffit de lancer plusieurs instances +d’abonnement dans le même utilisateur/répertoire à l’aide du même fichier de configuration.

+

Vous pouvez également exécuter plusieurs abonnements avec différents fichiers de configuration pour +avoir plusieurs flux de téléchargement dans le même répertoire, +et ce flux de téléchargement peut également être multi-flux.

+
+

Note

+

Alors que les courtiers gardent les files d’attente disponibles pendant un certain temps, les files d’attente prennent des ressources sur +les courtiers, et sont nettoyés de temps en temps. Une fil d’attente à laquelle on n’accède pas pour +une longue période (dépendante de la mise en œuvre) sera détruite. Une fil d’attente qui n’est pas +accédé et ayant trop de fichiers (définis par l’implémentation) en fil d’attente seront détruits. +Les processus qui meurent doivent être redémarrés dans un délai raisonnable pour éviter la +perte de notifications.

+
+
+
+

report et report_exchange

+

Pour chaque téléchargement, par défaut, un message de rapport amqp est renvoyé au broker. +Cela se fait avec l’option :

+
    +
  • report <booléen> (défaut: True)

  • +
  • report_exchange <report_exchangename> (défaut: xreport|xs_*username* )

  • +
+

Lorsqu’un rapport est généré, il est envoyé au report_exchange configuré. Les composants administratifs +publient directement sur xreport, tandis que les composants d’utilisateur publient sur le leur +échanges (xs_*nom d’utilisateur*). Les démons de rapport copient ensuite les messages dans xreport après validation.

+

Ces rapports sont utilisés pour le réglage de la livraison et pour les sources de données afin de générer des informations statistiques. +Définissez cette option sur False, pour empêcher la génération de rapports.

+
+
+

INSTANCES

+

Parfois, une instance d’un composant et d’une configuration ne suffit pas pour traiter et envoyer toutes les notifications disponibles.

+

instances <integer> (défaut:1)

+

L’option d’instance permet de lancer plusieurs instances d’un composant et d’une configuration. +Lors de l’exécution de sender par exemple, un certain nombre de fichiers d’exécution sont créés. +Dans le répertoire ~/.cache/sarra/sender/configName

+
A .sender_configname.state         est créé, contenant le nombre d’instances.
+A .sender_configname_$instance.pid est créé, contenant le PID du processus $instance.
+
+
+

Dans le répertoire ~/.cache/sarra/log:

+
Un .sender_configname_$instance.log  est créé en tant que journal du processus $instance.
+
+
+
+

Note

+

Un bug connu dans l’interface de gestion ‘sr <sr.8.html>_ signifie que l’instance doit +toujours être dans le fichier .conf (pas un .inc) et doit toujours être un nombre +(pas une variable substituée ou une autre valeur plus complexe.

+
+
+

Note

+

FIXME: indiquez également l’emplacement de Windows… fichiers dot sur Windows?

+
+
+

Note

+

Alors que les courtiers gardent les files d’attente disponibles pendant un certain temps, les files d’attente prennent des ressources sur les +courtiers, et sont nettoyés de temps en temps. Une fil d’attente qui n’est pas +accédé et ayant trop de fichiers en fil d’attente (définis par l’implémentation) seront détruits. +Les processus qui meurent doivent être redémarrés dans un délai raisonnable pour éviter la +perte de notifications. Une fil d’attente à laquelle on n’accède pas pendant une longue période (dépendant de l’implémentation) +sera détruite.

+
+
+

vip - ACTIVE/PASSIVE OPTIONS

+

sr3 peut être utilisé sur un seul nœud de serveur ou sur plusieurs nœuds. +pourrait partager la responsabilité. D’autres, configurés séparément, haute disponibilité +présente un vip (ip virtuel) sur le serveur actif. Si +le serveur tombe en panne, le vip est déplacé sur un autre serveur. +Les deux serveurs fonctionneraient sr3. Les options suivants contrôle +ce genre de comportement:

+
+
    +
  • vip <string> (None)

  • +
+
+

Lorsque vous n’exécutez qu’un seul sr3 sur un serveur, ces options ne +sont pas définies et sr3 fonctionnera en mode ‘standalone’.

+

Dans le cas des courtiers en grappe, vous devez définir les options pour l’option +vip en mouvement.

+

vip 153.14.126.3

+

Lorsque sr3 ne trouve pas le vip, il dort pendant 5 secondes et réessaie. +S´il possède le vip, il consomme et traite un message, puis revérifie le vip. +lorque plus qu´une addresse est setté commen vip, la possesion de n´importe laquelle +dans la liste est suffisante.

+
+
+

[–blocksize <value>] (default: 0 (auto))

+

This blocksize option controls the partitioning strategy used to post files. +The value should be one of:

+
0 - calcul automatique d'une stratégie de partitionnement appropriée (par défaut)
+1 - toujours envoyer des fichiers entiers en une seule partie.
+<blocksize> - utilisation d'une taille de partition fixe (exemple : 1M)
+
+
+

Les fichiers peuvent être publiés en plusieurs parties. Chaque partie +a une somme de contrôle séparée. Les pièces et leurs sommes de contrôle sont +stockées dans le cache. Les cloisons peuvent traverser le réseau séparément, +et en parallèle. Lorsque les fichiers changent, les transferts sont +optimisé en n’envoyant que des portions qui ont changé.

+

L’option outlet permet la sortie finale d’être autre chose qu’un post. +Voir sr3_cpump(1) pour plus de détails.

+
+
+

[-pbd|–post_baseDir <path>] (facultatif)

+

L’option post_baseDir fournit le chemin du répertoire qui, lorsqu’il est +combiné (ou trouvé) dans le chemin d’accès donné, donne le chemin absolu local +vers le fichier de données à enregistrer. La post_baseDir du chemin sera +supprimée du avis. Pour sftp : url’s il peut être approprié de spécifier un +chemin relatif à un compte utilisateur. Un exemple de cette utilisation +serait : -pbd ~user -url sftp:user@host +pour file : url’s, baseDir n’est généralement pas approprié. Pour publier +un chemin absolu, omettez le paramètre -pbd, et spécifiez simplement le chemin +complet en argument.

+
+
+

post_baseUrl <url> (OBLIGATOIRE)

+

L’option post_baseUrl définit comment obtenir le fichier…. il définit +le protocole, hôte, port, et optionnellement, l’utilisateur. C’est une +bonne pratique de ne pas inclure les mots de passe dans l´URL.

+
+
+

post_exchange <name> (default: xpublic)

+

L’option post_exchange, qui permet d’échanger la nouvelle notification. +sera publié. Dans la plupart des cas, il s’agit d’un’xpublic’.

+

Chaque fois qu’une avis se produit pour un produit, un utilisateur peut +définir de déclencher un script. L’option on_post serait utilisée pour faire +une telle configuration.

+
+
+

post_exchangeSplit <number> (défaut: 0)

+

L’option post_exchangeSplit ajoute un suffixe à deux chiffres résultant d’une +division entière du dernier digit de la somme de contrôle, afin de répartir les +avis entre un certain nombre d’échanges, selon la valeur de leur somme de contrôle. +C’est utilisé dans les pompes à trafic élevé pour permettre des instances +multiples de winnow, ce qui ne peut pas être instancié de la manière normale. exemple:

+
post_exchangeSplit 5
+post_exchange xwinnow
+
+
+

se traduira par l’envoi de messages à cinq échanges nommées xwinnow00, xwinnow01, +xwinnow02, xwinnow03 et xwinnow04, où chaque échange ne recevra qu’un cinquième du flux total. +xinnow01 recevra tous les messages dont la reste quand sa somme de contrôle est divisé par 5 +est 1.

+
+
+
+

Remote Configurations

+

On peut spécifier des URI comme fichiers de configuration, plutôt que des fichiers locaux. Exemple :

+
+
    +
  • –config http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

Au démarrage, sr3 vérifie si le fichier local cap.conf existe dans le répertoire +répertoire de configuration local. Si c’est le cas, alors le fichier sera lu pour trouver +une ligne comme ça :

+
+
    +
  • –remote_config_url http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

Dans ce cas, il vérifiera l’URL distante et comparera le temps de modification. +du fichier distant contre le fichier local. Le fichier distant n’est pas plus récent ou ne peut pas être modifié. +est atteint, alors le composant continuera avec le fichier local.

+

Si le fichier distant est plus récent ou s’il n’y a pas de fichier local, il sera téléchargé, +et la ligne remote_config_url_config_url y sera pré-pendue, de façon à ce qu’elle continue +pour se mettre à jour à l’avenir.

+
+
+

Extensions

+

On peut remplacer ou ajouter des fonctionnalités avec des scripts python.

+

Sarracenia est livré avec une variété d’exemples de plugins, et en utilise certains pour implémenter des fonctionnalités de base, +tels que la journalisation (implémentée par défaut en utilisant des plugins msg_log, file_log post_log):

+
$ sr3 list fcb
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py          flowcb/filter/pclean_f90.py      flowcb/filter/pclean_f92.py
+flowcb/gather/file.py            flowcb/gather/message.py         flowcb/line_log.py               flowcb/line_mode.py
+flowcb/log.py                    flowcb/nodupe.py                 flowcb/pclean.py                 flowcb/post/message.py
+flowcb/retry.py                  flowcb/sample.py                 flowcb/script.py                 flowcb/v2wrapper.py
+flowcb/work/rxpipe.py
+$
+
+
+

Les utilisateurs peuvent placer leurs propres scripts dans le sous-répertoire de script dans leur répertoire de configuration +( sur Linux, le ~/.config/sarra/plugins).

+
+

flowCallback et flowCallbackPrepend <class>

+

La directive flowCallback prend une classe à charger et peut analyser les points d’entrée comme argument

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

Avec cette directive dans un fichier de configuration, la classe Log trouvée dans le package installé sera utilisée. +Ce module enregistre les messages after_accept (après que les messages sont passé par les masques d’accept/reject) +et le after_work (après le téléchargement ou l’envoi du fichier). Voici le code source +pour cette classe de rappel

+
import json
+import logging
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+
+class Log(FlowCB):
+  def __init__(self, options):
+
+      # FIXME: should a logging module have a logLevel setting?
+      #        just put in a cookie cutter for now...
+      if hasattr(options, 'logLevel'):
+          logger.setLevel(getattr(logging, options.logLevel.upper()))
+      else:
+          logger.setLevel(logging.INFO)
+
+  def after_accept(self, worklist):
+      for msg in worklist.incoming:
+          logger.info("accepted: %s " % msg_dumps(msg) )
+
+  def after_work(self, worklist):
+      for msg in worklist.ok:
+          logger.info("worked successfully: %s " % msg.dumps() )
+
+
+

Si vous avez plusieurs rappels configurés, ils seront appelés dans le même ordre que dans +fichier de configuration. Les composants de sr3 sont souvent différenciés par les rappels configurés. +Par exemple, un watch est un flux avec la classe sarracenia.flowcb.gather.file.File qui +est utilisé pour analyser les répertoires. Un besoin courant lorsqu’une source de données n’est pas facilement accessible +avec les scripts python est d’utiliser le rappel de script:

+
flowCallbackPrepend sarracenia.flowcb.script.Script
+
+script_gather get_weird_data.sh
+
+
+

En utilisant la variante _prepend de l’option flowCallback, le rappel de script aura le +point d’entrée de la classe, avant le rappel de fichier… Donc, un script sera exécuté, puis +le répertoire sera vérifié pour les nouveaux fichiers. Voici une partie de la classe de rappel Script

+
import logging
+from sarracenia.flowcb import FlowCB
+import subprocess
+
+logger = logging.getLogger(__name__)
+
+
+class Script(FlowCB):
+
+   .
+   .
+   .
+
+    def run_script( self, script ):
+        try:
+            subprocess.run( self.o.script_gather, check=True )
+        except Exception as err:
+            logging.error("subprocess.run failed err={}".format(err))
+            logging.debug("Exception details:", exc_info=True)
+
+
+    def gather(self ):
+        if hasattr( self.o, 'script_gather') and self.o.script_gather is not None :
+            self.run_script( self.o.script_gather )
+        return []
+
+
+
+
+
+

Identity

+

On peut utiliser la directive import pour ajouter de nouveaux algorithmes de somme de contrôle en sous-classant +sarracenia.identity.Identity.

+
+
+

Transfer

+

On peut ajouter la prise en charge de méthodes supplémentaires de téléchargement de données par sous-classification +sarracenia.transfer.Transfer.

+

Les scripts de protocole de transfert doivent être déclarés à l’aide de l’option import. +sauf pour les fonctions intégrées ciblées, un module registered_as qui définit +une liste des protocoles fournis par ces fonctions. Exemple:

+
+
def registered_as(self) :

return [‘ftp’,’ftps’]

+
+
+

Voir le Programming Guide pour plus d’informations sur le développement d’extensions.

+
+
+

ROLES - feeder/admin/declare

+

d’intérêt que pour les administrateurs

+

Les options d’administration sont définies à l’aide de:

+
edit ~/.config/sr3/admin.conf
+
+
+

L’option feeder spécifie le compte utilisé par défaut pour les transferts +système pour les composants tels que sr_shovel, sr_sarra et sr_sender (lors de publication).

+

feeder amqp{s}://<user>:<pw>@<post_brokerhost>[:port]/<vhost> (valeur par défaut : Aucun) +– admin <nom> (par défaut : Aucun)

+

L’utilisateur administrateur est utilisé pour effectuer des opérations de maintenance sur la pompe, telles que la définition +des autres utilisateurs. La plupart des utilisateurs sont définis à l’aide de l’option declare. Le chargeur peut également être déclaré de cette +manière.

+
    +
  • declare <role> <name> (pas de défaut)

  • +
+
+

subscriber

+
+

Un subscriber (abonné) est un utilisateur qui ne peut s’abonner qu’aux données +et renvoyer des messages de rapport. Les abonnés ne sont pas autorisés à injecter des données. +Chaque abonné a un central xs_<user>nommé sur la pompe, où si un utilisateur est +nommé Acme, l’échange correspondant sera xs_Acme. Cet échange +est l’endroit où un processus sr_subscribe enverra ses messages de rapport.

+

Par convention/défaut, l’utilisateur anonyme est créé sur toutes les pompes pour +permettre l’abonnement sans un compte spécifique.

+
+
+
+

source

+
+

Un utilisateur autorisé à s’abonner ou à générer des données. Une source ne représente pas nécessairement +une personne ou un type de données, mais plutôt une organisation responsable des données produites. +Donc, si une organisation recueille et met à disposition dix types de données avec un seul contact, +e-mail, ou numéro de téléphone, toute question sur les données et leur disponibilité par rapport aux +activités de collecte peuvent alors utiliser un seul compte “source”.

+

Chaque source reçoit un échange xs_<utilisateur> pour l’injection de publications de données. Cela est comme un abonné +pour envoyer des messages de rapport sur le traitement et la réception des données. La source peut également avoir +un échange xl_<utilisateur> où, selon les configurations de routage des rapports, les messages de rapport des +consommateurs seront envoyés.

+
+
+
+

feeder

+
+

Un utilisateur autorisé à écrire à n’importe quel échange. Une sorte d’utilisateur de flux administratif, destiné à pomper +des messages lorsque aucune source ou abonné ordinaire n’est approprié pour le faire. Doit être utilisé de +préférence au lieu de comptes d’administrateur pour exécuter des flux.

+
+

Les informations d’identification de l’utilisateur sont placées dans le credentials.conf +et sr3 –users declare mettra à jour le courtier pour accepter ce qui est spécifié dans ce fichier, tant que le +mot de passe de l’administrateur est déjà correct.

+
+
+
+

FICHIERS DE CONFIGURATION

+

Alors qu’on peut gérer les fichiers de configuration à l’aide de la fonction add, remove, +list, edit, disable, et enable actions, on peut aussi tout faire. +des mêmes activités manuellement en manipulant les fichiers dans les paramètres. +dans l’annuaire de l’entreprise. Les fichiers de configuration pour une configuration sr_subscribe. +appelé myflow serait ici :

+
+
    +
  • linux : ~/.config/sarra/subscribe/myflow.conf (selon : XDG Open Directory Specication )

  • +
  • Windows : %AppDir%/science.gc.ca/sarra/myflow.conf, cela pourrait être : +C:UserspeterAppDataLocalscience.gc.casarrasarramyflow.conf

  • +
  • MAC : FIXME.

  • +
+
+

Le sommet de l’arborescence a ~/.config/sarra/default.conf qui contient les paramètres suivants +sont lus par défaut pour tout composant au démarrage. Dans le même répertoire, +~/.config/sarra/credentials.conf contient des informations d’identification +(mots de passe) à utiliser par sarracenia (IDENTIFICATION (CREDENTIALS) pour plus de détails. ).

+

On peut également définir la variable d’environnement XDG_CONFIG_HOME pour remplacer +le placement par défaut, ou bien Les fichiers de configuration individuels peuvent +être placés dans n’importe quel répertoire et invoqués avec la commande chemin complet. +Lorsque des composants sont invoqués, le fichier fourni est interprété comme un fichier +(avec un suffixe.conf conf supposé.) S’il n’est pas trouvé comme chemin d’accès au +fichier, alors l’option recherchera dans le répertoire de configuration du +composant ( config_dir / component) pour un fichier.conf correspondant.

+

S’il n’est toujours pas trouvé, il le recherchera dans le répertoire de configuration du site. +(linux : /usr/share/default/sarra/component).

+

Enfin, si l’utilisateur a défini l’option remote_config sur True et s’il +dispose de sites web configurés où l’on peut trouver des configurations +(option remote_config_config_url), Le programme essaiera de télécharger +le fichier nommé à partir de chaque site jusqu’à ce qu’il en trouve un. +En cas de succès, le fichier est téléchargé dans config_dir/Downloads et +interprété par le programme à partir de là. Il y a un processus similaire +pour tous les plugins qui peuvent être interprété et exécuté à l’intérieur +des composantes de la Sarracenia. Les composants chercheront en premier lieu +dans le répertoire plugins dans l’arbre de configuration des +utilisateurs, puis dans le site, puis dans le paquet sarracenia lui-même, +et finalement il regardera à distance.

+
+
+

OPTIONS DE COMPATIBILITÉ SUNDEW

+

Pour la compatibilité avec Sundew, il existe des options de livraison supplémentaires qui peuvent être spécifiées.

+
+

destfn_script <script> (défaut:None)

+

Cette option définit un script à exécuter lorsque tout est prêt +pour la livraison du produit. Le script reçoit une instance de la classe sender. +Le script prends le parent comme argument, et par exemple, une +modification de parent.msg.new_file changera le nom du fichier écrit localement.

+
+
+

filename <mots-clé> (défaut:None)

+

De MetPX Sundew, le support de cette option donne toutes sortes de possibilités +pour définir le nom de fichier distant. Certains keywords sont basés sur le fait que +les noms de fichiers MetPX Sundew ont cinq (à six) champs de chaîne de caractères séparés par des deux-points.

+

La valeur par défaut sur Sundew est NONESENDER, mais dans l’intérêt de décourager l’utilisation +de la séparation par des deux-points dans les fichiers, le défaut dans Sarracenia est WHATFN.

+

Les mots-clés possibles sont :

+
+
None
    +
  • Aucune modification du nom de fichier (enlever toute interprétation de style Sundew) +N.B. différent de NONE décrit plus loin.

  • +
+
+
WHATFN
    +
  • la première partie du nom de fichier Sundew (chaîne de caractères avant le premier : )

  • +
+
+
HEADFN
    +
  • Partie EN-TETE du nom de fichier Sundew

  • +
+
+
SENDER
    +
  • le nom de fichier Sundew peut se terminer par une chaîne SENDER=<string> dans ce cas, +la <string> sera le nom de fichier distant

  • +
+
+
NONE
    +
  • livrer avec le nom du fichier Sundew complet (sans :SENDER=…)

  • +
+
+
NONESENDER
    +
  • livrer avec le nom de fichier Sundew complet (avec :SENDER=…)

  • +
+
+
TIME
    +
  • horodatage ajouté au nom de fichier. Exemple d’utilisation : WHATFN:TIME

  • +
+
+
DESTFN=str
    +
  • déclaration str direct du nom de fichier

  • +
+
+
SATNET=1,2,3,A
    +
  • Paramètres d’application satnet interne cmc

  • +
+
+
DESTFNSCRIPT=script.py
    +
  • appeler un script (identique à destfn_script) pour générer le nom du fichier à écrire

  • +
+
+
+

accept <regexp pattern> [<keyword>]

+

Keyword peut être ajouté à l’option accept. Le keyword est une des options filename. +Un message qui correspond au modèle accept regexp aura son remote_file +appliqué à cette option de mot-clé. Ce mot-clé a la priorité sur le précédent nom de fichier.

+

Le regexp pattern peut être utilisé pour définir des parties du répertoire si une partie du message est placée +entre parenthèses. sender peut utiliser ces parties pour générer le nom du répertoire. +Les chaînes de parenthèses entre les guillemets rst remplaceront le mot-clé ${0} dans le nom du répertoire… +le second {1} $ etc.

+

Exemple d’utilisation

+
filename NONE
+
+directory /this/first/target/directory
+
+accept .*file.*type1.*
+
+directory /this/target/directory
+
+accept .*file.*type2.*
+
+accept .*file.*type3.*  DESTFN=file_of_type3
+
+directory /this/${0}/pattern/${1}/directory
+
+accept .*(2016....).*(RAW.*GRIB).*
+
+
+

Un message sélectionné par le premier accept sera remis inaltérée dans le premier répertoire.

+

Un message sélectionné par le deuxième accept sera remis inaltérée dans deuxième répertoire.

+

Un message sélectionné par le troisième accept sera renommé « fichier_de_type3 » dans le deuxième répertoire.

+

Un message sélectionné par le quatrième accept sera remis inaltérée à un répertoire.

+

Ça sera appelé /ce/20160123/modèle/RAW_MERGER_GRIB/répertoire si la notice du message ressemble à cela:

+

20150813161959.854 http://this.pump.com/ relative/path/to/20160123_product_RAW_MERGER_GRIB_from_CMC

+
+
+

Field Replacements

+

Dans MetPX Sundew, le format de la nomination de fichier est beaucoup plus +stricte, et est spécialisée pour une utilisation aves les données du World Meteorological Organization (WMO). +Notez que la convention du format des fichiers est antérieure, et n’a aucun rapport avec la convention de +dénomination des fichiers de WMO actuellement approuvée, et est utilisé strictement comme format interne. Les fichiers sont +séparés en six champs avec deux points. Le premier champ, DESTFN, est le “Abbreviated Header Line (AHL)” de WMO +(style 386) ou les blancs sont remplacé avec des traits de soulignement

+
TTAAii CCCC YYGGGg BBB ...
+
+
+

(voir le manuel de WMO pour plus de détails) suivis de chiffres pour rendre le produit unique (cela est vrai en +théorie, mais pas en pratique vu qu’il existe un grand nombre de produits qui ont les mêmes identifiants). +La signification du cinquième champ est une priorité, et le dernier champ est un horodatage. +La signification des autres champs varie en fonction du contexte. Exemple de nom de fichier

+
SACN43_CWAO_012000_AAA_41613:ncp1:CWAO:SA:3.A.I.E:3:20050201200339
+
+
+

Si un fichier est envoyé à Sarracenia et qu’il est nommé selon les conventions de Sundew, +les champs de substitution suivants seront disponibles:

+
${T1}    remplacer par le bulletin T1
+${T2}    remplacer par le bulletin T2
+${A1}    remplacer par le bulletin A1
+${A2}    remplacer par le bulletin A2
+${ii}    remplacer par le bulletin ii
+${CCCC}  remplacer par le bulletin CCCC
+${YY}    remplacer par le bulletin YY   (obs. jour)
+${GG}    remplacer par le bulletin GG   (obs. heure)
+${Gg}    remplacer par le bulletin Gg   (obs. minute)
+${BBB}   remplacer par le bulletin bbb
+${RYYYY} remplacer par l'année de réception
+${RMM}   remplacer par le mois de réception
+${RDD}   remplacer par le jour de réception
+${RHH}   remplacer par l'heure de réception
+${RMN}   remplacer par la minute de réception
+${RSS}   remplacer par la seconde de réception
+
+
+

Les champs ‘R’ proviennent du sixième champ, et les autres viennent du premier champ. +Lorsque des données sont injectées dans Sarracenia à partir de Sundew, l’en-tête du message sundew_extension +fournira la source de ces substitions même si ces champs ont été supprimés des fichiers livrés.

+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/Annoncements_v03.html b/fr/Explication/Histoire/Annoncements_v03.html new file mode 100644 index 000000000..4d68fbe49 --- /dev/null +++ b/fr/Explication/Histoire/Annoncements_v03.html @@ -0,0 +1,197 @@ + + + + + + + Annonce de Sr3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Annonce de Sr3

+

Après deux ans de développement, le 11/04/2022, nous sommes heureux d’annoncer la disponibilité +de la première version bêta de Sarracenia version 3: Sr3. Pour célébrer la sortie, il y a un +nouveau site Web avec des informations détaillées:

+
+
+

Par rapport à la v2, Sr3 apporte:

+
    +
  • Prise en charge native pour mqtt et +amqp (rabbitmq et les courtiers MQTT.) +avec une implémentation modulaire qui permet des +message queueing protocols +être pris en charge.

  • +
  • L’Algorithme de Flux unifie +tous les composants en légères variations de ce +code commun unique. +Ce refactoring a permis d’éliminer la duplication de code et de réduire le nombre total de lignes de +code d’environ 30 % tout en ajoutant des fonctionnalités.

  • +
  • Une nouvelle interface de ligne de commande centrée sur un point d’entrée unique: sr3

  • +
  • Amélioration de Jupyter Notebook Tutorials

  • +
  • Un nouveau plugin API, +ce qui permet la personnalisation pythonique du traitement des applications par défaut.

  • +
  • Un nouveau python API, +ce qui donne un accès complet à l’implémentation, permettant une extension élégante grâce à la sous-classification.

  • +
  • Les applications peuvent appeler Sarracenia Python API à partir de leur ligne principale. +(Dans la v2, il fallait écrire des rappels pour appeler le code de l’application, la ligne principale de l’application ne pouvait pas être utilisée.)

  • +
  • Ajout d’un module de discussion GitHub, pour les questions et les clarifications de la communauté : +https://github.com/MetPX/sarracenia/discussions

  • +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/Evolution.html b/fr/Explication/Histoire/Evolution.html new file mode 100644 index 000000000..cd20a7fc7 --- /dev/null +++ b/fr/Explication/Histoire/Evolution.html @@ -0,0 +1,301 @@ + + + + + + + Histoire/Contexte de Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Histoire/Contexte de Sarracenia

+

MetPX-Sarracenia est un produit du projet d’échange de produits météorologiques, +provient d’Environnement Canada, mais est maintenant géré par Services partagés Canada sur son +nom. Le projet a débuté en 2004, dans le but de fournir une pile gratuite qui +met en œuvre l’échange de données en temps réel standard de l’Organisation météorologique mondiale, +et aussi les besoins adjacents. Sundew était le +commutateur WMO 386 (GTS) de première génération.

+

En 2007, lorsque MetPX était à l’origine open source, le personnel responsable faisait partie +d’environnement Canada. En l’honneur de la Loi sur les espèces en péril (LEP), pour souligner le sort +d’espèces en voie de disparition qui ne sont pas poilues (les espèces à fourrure attirent toute l’attention) et +parce que les moteurs de recherche trouveront plus facilement des références à des noms plus inhabituels, +le commutateur WMO MetPX original a été nommé d’après une plante carnivore sur l’espèce à +registre des risques : Le Thread-leaved Sundew.

+

Sundew, le commutateur de WMO, devait également être compatible avec les mécanismes de transfert interne existant +fortement basés sur FTP. Cela a fonctionné, mais le GTS lui-même est obsolète dans de nombreux cas. +Les travaux ont commencé en 2009 pour étendre Sundew afin de tirer parti des nouvelles technologies, +tels que les protocoles de mise en file d’attente des messages, à partir de 2008. Les versions de Sundew sont +généralement étiqueté < 1,0.

+

Les prototypes initiaux de Sarracenia tiraient parti de MetPX Sundew, l’ancêtre de Sarracenia. +Les plugins Sundew ont été développés pour créer des messages de notification pour les fichiers livrés par Sundew, +et Dd_subscribe a été initialement développé en tant que client de téléchargement pour dd.weather.gc.ca, un +site Web d’Environnement Canada où une grande variété de produits météorologiques sont fabriqués +à la disposition du public. C’est du nom de ce site que Sarracenia prend le préfixe dd_ pour ses outils. +La version initiale a été déployée en 2013 à titre expérimental en tant que dd_subscribe.

+
+

Renommage dd_subscribe

+

Le nouveau projet (MetPX-Sarracenia) a de nombreux composants, est utilisé pour plus de distribution, +et plus d’un site Web, et cause de la confusion pour les administrateurs de système pensant qu’il est associé +à la commande dd(1) (pour convertir et copier des fichiers). Ainsi, les composants ont été commutés pour utiliser +le préfixe sr_. L’année suivante, le soutien des sommes de contrôle ont été ajoutées et, à l’automne 2015, +les flux ont été mis à jour vers la version v02.

+

Nous nous sommes finalement heurtés aux limites de cette approche d’extension et, en 2015, nous avons +commencé Sarracenia en tant que remplacement de deuxième génération, +libéré de la stricte compatibilité GTS héritée. Sarracenia (version 2) était initialement un prototype, +et de nombreux changements de toutes sortes ont eu lieu au cours de sa vie. +C’est encore (en 2022) la seule version déployée opérationnellement. Ca a subi trois changements opérationnels +de format du message (exp, v00 et v02.) Il prend en charge des centaines de milliers de transferts de +fichiers par heure 24h / 24 et 7j / 7 au Canada.

+

Où Sundew prend en charge une grande variété de formats de fichiers, de protocoles et de conventions +spécifique à la météorologie en temps réel, Sarracenia s’éloigne un peu plus des +applications spécifiques et est un moteur de réplication d’arborescence impitoyablement générique, qui +devrait permettre son utilisation dans d’autres domaines. Le prototype du client initial, dd_subscribe, +en usage depuis 2013, était une sorte d’impasse logique. Une voie à suivre a été décrite +avec Sarracenia in 10 Minutes +en novembre 2015, ce qui a conduit au remplacement de dd_subscribe en 2016 par le +package Sarracenia, avec tous les composants nécessaires à la production ainsi que +la consommation des arborescences de fichiers.

+

L’organisation derrière MetPX est passée à Services partagés Canada en 2011, mais lorsque +il est venu le temps de nommer un nouveau module, nous avons gardé le thème des plantes carnivores, et +en a choisi une autre indigène dans certaines régions du Canada : Sarracenia, une variété +de pichets insectivores. Nous aimons les plantes qui mangent de la viande!

+

Sarracenia s’appelait initialement v2, comme dans la deuxième architecture de pompage de données +dans le projet MetPX, (v1 étant Sundew.) un bon calendrier de déploiements / réalisations +est ici. Bien qu’il se soit avéré très prometteur, +et a très bien fonctionné, au fil des années, un certain nombre de limitations avec +la mise en œuvre est devenue claire :

+
    +
  • Le faible support pour les développeurs python. Le code v2 n’est pas du tout du Python idiomatique.

  • +
  • La logique étrange de plugin, avec un mauvais rapport d’erreur.

  • +
  • L’incapacité de traiter des groupes de messages.

  • +
  • L’impossibilité d’ajouter d’autres protocoles de file d’attente (limités à rabbitmq/AMQP.)

  • +
  • Difficulté d’ajouter des protocoles de transfert.

  • +
+

En 2020, le développement a commencé sur Version 3, maintenant +surnommé Sr3. Sr3 est environ 30% moins de code que v2, offre une API bien améliorée, +et prend en charge des protocoles de message supplémentaires, plutôt que simplement rabbitmq.

+
+
+

Moins de Klocs, de meilleurs Klocs

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Historique des applications de pompage de données pour Environnement Canada

Era

Application

Code size

Fonctionnalités

1980s

Tandem, PDS (domestic GTS)

500kloc

X.25, WMO Socket, AM Socket, FTP (push only)

2000s

Sundew

30kloc

WMO Socket/TCP, FTP, SFTP (push only)

2010s

Sarracenia v2

25kloc

AMQP, HTTP, SFTP, FTP (pub/sub and push)

2020s

Sarracenia v3 (sr3)

15kloc

AMQP, MQTT, HTTP, SFTP, API (pub/sub and push)

+
+
+

Déploiements/cas d’utilisation

+

État du déploiement en 2015: Sarracenia in 10 Minutes Video (5:26 in)

+

État du déploiement en 2018: Deployments as of January 2018

+
+
+

Site web du projet

+

Avant mars 2018, le site Web principal du projet était metpx.sf.net. +Ce site Web MetPX a été construit à partir de la documentation dans les différents modules +dans le projet. Il se construit en utilisant tous les fichiers .rst trouvés dans +sarracenia/doc ainsi que certains des fichiers .rst trouvés dans +Sundew/doc. Au printemps 2018, le développement s’est déplacé à github.com. +github.com rend .rst lors de l’affichage des pages, donc le traitement séparé pour le rendu +des pages Web ne sont plus nécessaires.

+

Entre 2018 et 2022, la mise à jour a été effectuée en validant les modifications apportées aux fichiers .rst +Directement sur GitHub, donc la page d’accueil est : https://github.com/MetPX/sarracenia) +Aucun post-traitement n’était requis. Comme les liens sont tous relatifs et +d’autres services tels que GitLab prennent également en charge un tel rendu, le +site Web est portable à n’importe quelle instance GitLab, etc … Et le point d’entrée est à partir du +fichier README.rst dans le répertoire racine de chaque référentiel.

+

Cela avait des problèmes de lisibilité, avec des gens qui disaient “N’a-t-il pas un site Web?” +une mauvaise navigation, et pas d’indexation. En 2022/03, le sphinx a été adopté dans le cadre +de #380 (Document Restructure) et le processus de construction local est maintenant essentiellement +exécutez sur Sphinx. Le nouveau site web ( https://metpx.github.io/sarracenia ) est mis à jour +par un workflow GitHub à chaque commit.

+
+

Mise à jour du site Web sf.net

+

Seules les pages index-e.html et index-f.html sont utilisées sur le site Web de sf.net +Aujourd’hui. À moins que vous ne souhaitiez modifier ces pages, cette opération est inutile. +Pour toutes les autres pages, les liens vont directement dans les différents fichiers .rst sur +github.com.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/HPC_mirroring_cas_dutilisation.html b/fr/Explication/Histoire/HPC_mirroring_cas_dutilisation.html new file mode 100644 index 000000000..9186e33fa --- /dev/null +++ b/fr/Explication/Histoire/HPC_mirroring_cas_dutilisation.html @@ -0,0 +1,705 @@ + + + + + + + Étude de cas : Mise en miroir HPC — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Étude de cas : Mise en miroir HPC

+
+

Mise en miroir continue de 27 millions d’arborescence de fichiers très rapidement

+
+

Résumé

+

Ce projet a pris plus de temps que prévu, soit plus de trois ans, car l’espace problématique a été exploré avec le +l’aide d’un client très patient alors que l’outil pour concevoir et mettre en œuvre la solution efficace était finalement +réglé. Le client a demandé une solution pour rendre les fichiers disponibles sur le cluster de sauvegarde dans +un délai de cinq minutes de leur création sur la principale, et la première version de mirroring, déployée en 2017, +atteint un délai d’environ 20 minutes. La version 2 a été déployée en janvier 2020.

+

Le client est en fait plus un partenaire, qui avait de très gros cas de test disponibles et +a fini par assumer la responsabilité pour nous tous de comprendre si la solution fonctionnait ou non. +Bien qu’il existe de nombreuses spécificités de cette implémentation, l’outil résultant ne repose sur aucune +fonctionnalité spécifique au-delà d’un système de fichiers Linux normal pour atteindre une accélération de +72:1 par rapport à rsync en temps réel en mise en miroir continu de 16 téraoctets dans 1,9 million de fichiers +par jour entre deux arbres de 27 millions de fichiers chacun. Pendant que cela atteint en moyenne 185 Mo/seconde +sur une période de 24 heures. Il convient de noter que les transferts sont très pointus. Sur le même équipement +en même temps, un autre 4 téraoctets par jour sont écrits sur des clusters sur un autre réseau, de sorte que +le taux de lecture agrégé sur le cluster opérationnel est de 20 téraoctets par jour (231 Mo/seconde) +pour cette application, tandis que les systèmes de fichiers sont également utilisés pour toutes les +applications d’utilisateur normales.

+

Alors que ce projet a dû souffrir à travers le développement, avec les leçons apprises et les outils +maintenant disponible, il devrait être simple d’appliquer cette solution à d’autres cas. Le résultat final est +que l’on ajoute une Bibliothèque Shim à l’environnement des utilisateurs (transparent pour les tâches des utilisateurs), et +puis chaque fois qu’un fichier est écrit, un message AMQP avec les métadonnées du fichier est publié. Un pool de +démons de transfert sont prêts à transférer les fichiers publiés dans une file d’attente partagée. Le nombre d’abonnés +est programmable et évolutif, et les techniques et la topologie pour effectuer le transfert sont toutes facilement +contrôlé pour optimiser les transferts pour les critères jugés les plus importants.

+
+
+

Énoncé du problème

+

En novembre 2016, le Service météorologique du Canada (MSC) d’Environnement et Changement climatique Canada (ECCC) +dans le cadre du projet High Performance Computing Replacement (HPCR) a demandé un très grand répertoire +d’arbres à refléter en temps réel. Services partagés Canada (SPC) était le principal responsable du déploiement +du HPCR, ECCC/MSC étant la seule communauté d’utilisateurs. On savait dès le départ que ces arbres seraient trop grands pour +traiter l’utilisation d’outils ordinaires. On s’attendait à ce qu’il faille environ 15 mois pour explorer +et arriver à un déploiement opérationnel efficace. Il convient de noter que SPC a travaillé tout au long de +cette période en partenariat étroit avec ECCC, et que ce déploiement a nécessité la participation très active +d’utilisateurs sophistiqués à suivre avec les rebondissements et les différentes avenues explorées et mises en œuvre.

+

L’environnement informatique est le centre météorologique national du Canada, dont l’application principale est +la production numérique de la prévision météorologique, où les modèles fonctionnent 7j/7 et 24h/24 +en exécutant différentes simulations (modèles de l’atmosphère, et parfois les voies navigables et l’océan, +et la terre sous l’atmosphère) soit en ingérant les observations actuelles aka assimilation, les mapper +à une grille analyse, puis faire avancer la grille dans le temps prédiction/pronostic. Les prévisions +suivent un calendrier précis tout au long du cycle de 24 heures, alimentant l’un dans l’autre, de sorte +que les retards se répercutent et que des efforts considérables sont déployés pour éviter les interruptions et +maintiennent l’horaire.

+
+
+

Présentation de la solution HPCR

+../../../_images/HPC-XC_High_Availability.png + +

Dans le diagramme ci-dessus, si les opérations se trouvent dans le hall de données 1 (à gauche du centre) +et qu’elles échouent, l’objectif est de reprendre les opérations rapidement à partir du Data Hall 2 +(à droite). Pour que cela soit réaliste, les données de production doivent être disponibles +dans l’autre hall, sur l’autre boutique du site, rapidement. Le problème de la mise en miroir est la +synchronisation d’un très grand sous-ensemble de données entre le boutique de sites 1 et le magasin de +sites 2. À des fins de surveillance, dans le même temps, un sous-ensemble doit être mis en miroir +dans la salle de données 0.

+
+
+

Mise en miroir continue

+

Il y a deux clusters qui exécutent ces simulations, l’un d’eux travaillant normalement principalement +sur les opérations, et l’autre en tant que rechange (exécutant uniquement des charges de recherche +et développement). Lorsque la primaire échoue, l’intention est d’exécuter des opérations sur l’autre +supercalculateur, en utilisant un disque de rechange sur lequel toutes les données en direct ont été +mises en miroir. Comme il y a (presque) toujours des exécutions en cours, les répertoires n’ont jamais +arrêté d’être modifié, et il n’y a pas de période de maintenance où l’on peut rattraper si l’on prend +du retard.

+

Il y a essentiellement trois parties du problème :

+
+
    +
  • Détection : obtenir la liste des fichiers qui ont été modifiés (récemment).

  • +
  • Transfert: copiez-les sur l’autre cluster (minimisant la surcharge.)

  • +
  • Performance : délai ambitieux pour livrer un fichier miroir : cinq minutes.

  • +
+
+

Les arbres réels à refléter étaient les suivants dans la phase contractuelle initiale (appelée rétrospectivement U0):

+
psilva@eccc1-ppp1:/home/sarr111/.config/sarra/poll$ grep directory *hall1*.conf
+policy_hall1_admin.conf:directory /fs/site1/ops/eccc/cmod/prod/admin
+policy_hall1_archive_dbase.conf:directory /fs/site1/ops/eccc/cmod/prod/archive.dbase
+policy_hall1_cmop.conf:directory /fs/site1/ops/eccc/cmod/cmop/data/maestro/smco500
+policy_hall1_daily_scores.conf:directory /fs/site1/ops/eccc/cmod/prod/daily_scores
+policy_hall1_hubs.conf:directory /fs/site1/ops/eccc/cmod/prod/hubs
+policy_hall1_products.conf:directory /fs/site1/ops/eccc/cmod/prod/products
+policy_hall1_stats.conf:directory /fs/site1/ops/eccc/cmod/prod/stats
+policy_hall1_version_control.conf:directory /fs/site1/ops/eccc/cmod/prod/version_control
+policy_hall1_work_ops.conf:directory /fs/site1/ops/eccc/cmod/prod/work_ops
+policy_hall1_work_par.conf:directory /fs/site1/ops/eccc/cmod/prod/work_par
+psilva@eccc1-ppp1:/home/sarr111/.config/sarra/poll$
+
+
+

Au départ, on savait que le nombre de dossiers était important, mais on ne connaissait pas +les montants en jeu. Ces données n’ont seulement été disponibles que beaucoup plus tard.

+

La façon la plus efficace de copier ces arbres, comme on l’a dit au début, serait pour tous les travaux +d’écrire des fichiers dans les arborescences pour annoncer explicitement les fichiers à copier. Cela +impliquerait aux utilisateurs de modifier leurs tâches pour inclure l’appel de sr_cpost (une commande qui +met en file d’attente les transferts de fichiers pour que les tiers les exécutent). Toutefois, le client +a défini la contrainte supplémentaire selon laquelle la modification des tâches d’utilisateur +n’était pas réalisable, la méthode utilisée pour obtenir la liste des fichiers à copier devait être +implicite (effectuée sans la participation active de l’utilisateur).

+
+
+

La lecture de l’arbre prend trop de temps

+

On pourrait simplement analyser à un niveau supérieur afin d’analyser un seul répertoire parent, mais la demi-douzaine +des sous-arbres des arbres ont été choisis afin d’en avoir des plus petits qui fonctionnaient plus rapidement, +indépendamment de la méthode utilisée pour obtenir des listes de nouveaux fichiers. Que voulons-nous dire quand +nous disons que ces arbres sont trop grands? Le plus grand de ces arbres est hubs +( /fs/site1/ops/eccc/cmod/prod/hubs ). Rsync a été exécuté sur les hubs, en tant que juste visiter l’arbre une fois, +sans aucune copie de fichier en cours. Visiter l’arbre, en utilisant rsync avec la somme de contrôle +désactivée en tant qu’optimisation, a abouti au journal ci-dessous:

+
psilva@eccc1-ppp1:~/test$ more tt_walk_hubs.log
+nohup: ignoring input
+rsync starting @ Sat Oct  7 14:56:52 GMT 2017
+number of files examined is on the order of: rsync --dry-run --links -avi --size-only /fs/site1/ops/eccc/cmod/prod/hubs /fs/site2/ops/eccc/cmod/prod/hubs |& wc -l
+27182247
+rsync end @ Sat Oct  7 20:06:31 GMT 2017
+psilva@eccc1-ppp1:~/test$
+
+
+

Un seul passage a pris plus de cinq heures pour examiner 27 millions de dossiers ou examiner +environ 1500 fichiers par seconde. Le taux maximal d’exécution de rsyncs sur cet arbre est +donc de l’ordre d’une fois toutes les six heures (pour permettre un certain temps de copie) pour +cet arbre. Notez que toute méthode habituelle de copie d’une arborescence de répertoires nécessite +de parcourir et qu’il n’y a aucune raison de croire qu’un autre outil tel que find, dump, tar, tree, +etc… serait nettement plus rapide que rsync. Nous avons besoin d’une méthode plus rapide pour savoir +quels fichiers ont été modifiés afin qu’ils puissent être copiés.

+
+
+

Méthodes de détection : Inotify, Policy, SHIM

+

Il existe une fonctionnalité du noyau Linux appelée INOTIFY, qui peut déclencher un événement +lorsqu’un fichier est modifié. En définissant un déclencheur INOTIFY sur chaque répertoire de +l’arborescence, nous pouvons être avertis lorsqu’un fichier est modifié dans l’arbre. C’était +l’approche initiale adoptée. Il s’avère (en janvier 2017), qu’INOTIFY est bien une fonctionnalité +Linux, en ce sens que les événements INOTIFY ne se propagent que sur un seul serveur. Avec un +système de fichier cluster comme GPFS, il faut exécuter un moniteur INOTIFY sur chaque noyau +où les fichiers sont écrits. Donc, plutôt que d’exécuter un seul démon, nous étions confrontés +à l’exécution de plusieurs centaines de démons (un par nœud physique), chacun surveillant le +même ensemble de dizaines de millions de fichiers. Puisque les démons fonctionnaient sur de +nombreux nœuds, l’utilisation de la mémoire a atteint le téraoctet.

+

Une autre approche : au lieu d’exécuter la détection de modification au niveau Linux, utilisez +le système de fichier lui-même, qui est piloté par une base de données, pour indiquer quels +fichiers ont été modifiés. Les principaux objectifs de la solution HPC et le système de stockage qui +utilise le système de fichiers parallèle général d’IBM, ou GPFS. À l’aide de la méthode GPFS-policy, +une requête est exécutée sur la base de données du système de fichiers à un rythme aussi élevé que +possible (environ cinq à dix minutes par requête) combiné avec sr_poll pour annoncer les fichiers +modifiés (et donc éligibles à la copie). C’est complètement non portable, mais on s’attendait à +ce qu’il soit beaucoup plus rapide que la traversée de l’arborescence des fichiers.

+

Au cours de l’hiver 2016-2017, ces deux méthodes ont été mises en œuvre. Le sr3_watch basé sur +INOTIFY était la méthode la plus rapide (instantanée), mais les démons avaient des problèmes de +stabilité et de consommation de mémoire, et ils ont également pris trop de temps à démarrer +(nécessite une traversée initiale de l’arbre, qui prend le même temps que rsync). Bien que plus +lent (prenant plus de temps pour remarquer qu’un fichier a été modifié), la politique GPFS avait +une performance acceptable et était beaucoup plus fiable que la méthode de sr3_watch parallèle, +et au printemps, avec un déploiement prévu pour le début de juillet 2017, l’approche stratégique +du GPFS a été choisie.

+

Au fur et à mesure que la migration progressait, les systèmes de fichiers se développaient parce +qu’ils avaient plus de fichiers dans les arborescences et la méthode de stratégie GPFS a +progressivement ralentie. Déjà en juillet 2017, ce n’était pas une solution acceptable. À ce stade, +l’idée d’intercepter les appels d’i/o de fichiers de jobs avec une bibliothèque SHIM a été introduite. +ECCC a dit à SPC à l’époque, le fait d’avoir une alimentation correcte et d’avoir tout prêt pour la +transition était la priorité, de sorte que les efforts se sont concentrés dans cette direction jusqu’à +ce que la migration soit réalisée en septembre. Bien qu’il s’agisse d’une priorité moindre au cours de +l’été, une mise en œuvre C de la partie d’envoi de la bibliothèque Sarra a été implémentée avec un +prototype de bibliothèque Shim pour l’appeler.

+

C’est à noter que les exécutions de la politique GPFS ont été déployées opérationnellement depuis 2017. +Cela s’est avéré être la version 1 de la solution de mise en miroir, et a permis d’obtenir une mise +en miroir vers les clusters secondaires avec environ 20 minutes de retard pour acheminer les données vers +le deuxième système. Trois ans plus tard, il y a maintenant une mise à niveau des clusters de +supercalculateurs (appelée U1) en cours avec deux nouveaux clusters supplémentaires en ligne. +Le client utilise maintenant les méthodes normales Sarracenia pour mettre en miroir l’ancien cluster +de sauvegarde vers les nouveaux, avec seulement quelques secondes de retard au-delà de ce qu’il faut pour +accéder au cluster de sauvegarde.

+

Il convient également de noter que l’utilisation de requêtes de stratégie GPFS a imposé une charge +importante, et continue, aux clusters GPFS, et constitue une préoccupation constante pour les +administrateurs GPFS. Ils aimeraient beaucoup s’en débarrasser. Les performances se sont stabilisées +au cours de la dernière année, mais elles semblent ralentir à mesure que la taille de l’arborescence +des fichiers augmente. De nombreuses optimisations ont été mises en œuvre pour obtenir des performance +adéquates.

+
+

Bibliothèque Shim

+

La méthode choisie pour la notification est une bibliothèque Shim +Lorsqu’une application en cours d’exécution effectue des appels aux points d’entrée d’API +fournis par les bibliothèques ou le noyau, il existe un processus de recherche (résolu au +moment du chargement de l’application) qui trouve la première entrée dans le chemin d’accès +qui a la signature appropriée. Par exemple, lors de l’émission d’un appel de fermeture de +fichier(2), le système d’exploitation organisera l’appel de la routine correcte dans la +bibliothèque appropriée.

+../../../_images/shim_explanation_normal_close.svg

Un appel à la routine de fermeture indique qu’un programme a fini d’écrire le fichier en question, +et indique donc généralement la première fois qu’il est utile d’annoncer un fichier pour le transfert. +Nous avons créé une bibliothèque Shim, qui a des points d’entrée qui usurpent l’identité de ceux +appelés par l’application, afin que les notifications de disponibilité des fichiers soient publiées +par l’application elle-même, sans aucune modification de l’application.

+../../../_images/shim_explanation_shim_close.svg

L’utilisation de la bibliothèque de Shim est détaillée dans sr_post(1)

+
+
+
+

Copie de fichiers

+

Il est important de noter que pendant que tout ce travail progressait sur la partie “obtenir +la liste des fichiers à copier” du problème, nous travaillions également sur la partie “copier +les fichiers de l’autre côté” du problème. Au cours de l’été, les résultats des tests de +performance et d’autres considérations ont entraîné de fréquents changements de tactique. +Les boutique du sites sont des clusters à part entière. Ils ont des nœuds de protocole pour +servir le trafic en dehors du cluster GPFS. Il existe des nœuds siteio avec des connexions +infiniband et des disques réels. Les nœuds de protocole (appelés nfs ou proto) sont des +participants du cluster GPFS dédié aux opérations d’i/o, utilisé pour décharger les i/o du +clusters de calcul principaux (PPP et Supercalculateur), qui ont des connexions comparables +au boutique du sites en tant que nœuds de protocole.

+

Il existe plusieurs réseaux (40GigE, Infiniband, ainsi que des réseaux de gestion) et celui +à utiliser doit également être choisi. Ensuite, il y a les méthodes de communication (ssh +sur tcp / ip? BBCP sur TCP/IP ? GPFS sur tcpip? Ipoib? natif-ib?).

+../../../_images/site-store.jpg +

De nombreuses sources et destinations différentes (ppp, nfs et nœuds de protocole), ainsi que +de nombreuses méthodes différentes (rcp, scp, bbcp, sscp, cp, dd) ont toutes été testées à des +degrés différents à différents moments. À ce stade, plusieurs forces de sarracenia étaient évidentes:

+
    +
  • La séparation de la publication et de l’abonnement signifie que l’on peut s’abonner sur le +nœud source et pousser vers la destination, ou sur la destination et extraire de la source. +Il est facile à adapter à l’une ou l’autre approche (on s’est retrouvé avec les nœuds de +protocole de destination, en tirant de la source).

  • +
  • La séparation de copier depuis des jobs computationnel signifie que les temps d’exécution des +modèles ne sont pas affectés, car les travaux d’i/o sont complètement séparés.

  • +
  • La capacité d’adapter le nombre de travailleurs à la performance requise (finalement décidé +de 40 travailleurs effectuant des copies en parallèle).

  • +
  • La disponibilité des plugins download_cp, download_rcp, download_dd, permet d’appliquer +facilement de nombreux programmes de copie différents (et donc des protocoles) au problème de transfert.

  • +
+

De nombreux critères différents ont été pris en compte (tels que: charge sur les nœuds nfs/protocole, +autres nœuds, vitesse de transfert, charge sur les nœuds PPP). La configuration finale sélectionnée +d’utiliser cp (via le download_cp plugin) initié à partir des nœuds de protocole de la +boutique du site récepteur. Ainsi, les lectures se produiraient via GPFS sur IPoIB, et les +écritures seraient effectuées sur GPFS natif sur IB. Ce n’était pas la méthode de transfert la +plus rapide testée (bbcp était plus rapide), mais elle a été sélectionnée parce qu’elle +répartissait la charge sur les nœuds siteio, ce qui entraînait un NFS et un protocole plus stable. +Les nœuds et surcharge de configuration TCP/IP/démontage supprimée. La partie “copier les fichiers +de l’autre côté” du problème était stable à la fin de l’été 2017, et l’impact sur la stabilité +du système est minimisé.

+
+
+

Bibliothèque Shim nécessaire

+

Malheureusement, la mise en miroir entre les sites fonctionnait avec un décalage d’environ 10 minutes +sur le système de fichiers source (environ 30 fois plus rapide qu’une approche rsync naïve), et ne +fonctionnait qu’en principe, avec de nombreux fichiers manquants dans la pratique, elle n’était pas +utilisable aux fins prévues. La mise en service opérationnelle de la solution HPCR dans son ensemble +(avec mise en miroir différée) a eu lieu en septembre 2017, et les travaux de mise en miroir ont +essentiellement été arrêtés jusqu’en octobre (en raison des activités liées aux travaux de mise en +service).

+

Nous avons continué à travailler sur deux approches, la libsrshim et la politique GPFS. Les requêtes +exécutées par la politique GPFS devaient être réglées, éventuellement un chevauchement de 75 secondes +(où une requête suivante demandait des modifications de fichier jusqu’à un point 75 secondes avant la +fin de la dernière) car il y avait des problèmes avec les fichiers manquants dans les copies. Même avec +ce niveau de chevauchement, il manquait encore des dossiers. À ce stade, fin novembre, début décembre, +les libsrshim fonctionnaient assez bien pour être si encourageants que les gens ont perdu tout intérêt +pour la politique du GPFS. Contrairement à un délai moyen d’environ 10 minutes pour démarrer une copie +de fichier avec des requêtes de stratégie GPFS, l’approche libsrshim a la copie en file d’attente dès +que le fichier est fermé sur le système de fichiers source.

+

Il convient de noter que lorsque le travail a commencé, l’implémentation python de Sarracenia était +un outil de distribution de données, sans support pour la mise en miroir. Au fur et à mesure que +l’année avançait, des fonctionnalités (prise en charge des liens symboliques, transport des attributs +de fichier, prise en charge de la suppression de fichiers) ont été ajoutées au package initial. L’idée +d’un traitement périodique (appelé pulsations) a été ajoutée, d’abord pour détecter les défaillances +des clients (en voyant les journaux inactifs), mais plus tard pour lancer le nettoyage de la mémoire +pour la cache des doublons, la surveillance de l’utilisation de la mémoire et la récupération d’erreurs +complexes. Le cas d’utilisation a précipité de nombreuses améliorations dans l’application, y compris +une deuxième implémentation en C pour les environnements où un environnement Python3 était difficile +à établir, ou où l’efficacité était primordiale (le cas libsrshim).

+
+
+

Est-ce que ça marche?

+

En décembre 2017, le logiciel pour l’approche libsrshim semblait prêt, il a été déployé en quelques +petites exécutions parallèles (non opérationnelles). Les essais en parallèle ont commencé en janvier 2018. +Il y a eu de nombreux cas limites, et les tests se sont poursuivis pendant deux ans, jusqu’à ce qu’ils +soient finalement prêts à être déployés en décembre 2019.

+
    +
  • FIXME: inclure des liens vers des plugins

  • +
  • FIXME: Une autre approche envisagée consiste à comparer les instantanés du système de fichiers.

  • +
+

Comme la bibliothèque Shim a été utilisée dans des contextes de plus en plus larges pour la rapprocher +du déploiement, un nombre important de cas limites ont été rencontrés :

+ +

Au cours des deux années qui ont suivi, ces cas limites ont été traités et le déploiement a +finalement eu lieu avec la transition vers U1 en janvier 2020. On s’attend à ce que le délai +dans les fichiers apparaissant sur le deuxième système de fichiers soit de l’ordre de cinq +minutes après leur écriture dans l’arborescence source, soit 72 fois plus rapide que rsync +(voir la section suivante pour les informations sur les performances), mais nous n’avons pas +encore de métriques concrètes.

+

La question s’est naturellement posée, si l’arborescence des répertoires ne peut pas être +parcourue, comment savons-nous que les arborescences source et destination sont les mêmes ? +Un programme permettant de sélectionner des fichiers aléatoires sur l’arborescence source +est utilisé pour alimenter un sr_poll, qui ajuste ensuite le chemin pour le comparer au +même fichier sur la destination. Sur un grand nombre d’échantillons, nous obtenons une +quantification de la précision de la copie. Le plugin pour cette comparaison est encore +en développement.

+
+
+

Est-ce rapide?

+

Les exécutions de la politique GPFS sont toujours la méthode utilisée sur le plan opérationnel au moment +de la rédaction de cet article (2018/01). Les chiffres de performances indiqués dans le résumé sont extraits +des journaux d’une journée d’exécution de stratégie GPFS.

+
+
    +
  • Hall1 à Hall2: bytes/days: 18615163646615 = 16T, nb fichier/jour: 1901463

  • +
  • Hall2 à CMC: octets/jours: 4421909953006 = 4T, nb fichier/jour: 475085

  • +
+
+

Tout indique que la bibliothèque de shim copie plus de données plus rapidement que les exécutions +basées sur des stratégies, mais jusqu’à présent (2018/01), seuls des sous-ensembles de l’arborescence +principale ont été testés. Sur une arborescence de 142000 fichiers, l’exécution de la stratégie GPFS +avait un temps de transfert moyen de 1355 secondes (environ 23 minutes), alors que l’approche de la +bibliothèque de shim avait un temps de transfert moyen de 239 secondes (moins de cinq minutes) ou une +accélération pour libshim vs stratégie GPFS d’environ 4:1. Sur un deuxième arbre où la bibliothèque +de Shim transférait 144 000 fichiers en une journée, le temps de transfert moyen était de 264 secondes, +alors que le même arbre avec l’approche de politique GPFS prenait 1175 (essentiellement 20 minutes).

+

Les statistiques sont accumulées pour des heures particulières, et à des heures de faible trafic, le +temps de transfert moyen avec la bibliothèque de Shim était de 0,5 seconde contre 166 secondes avec la +politique. On pourrait prétendre à une accélération de 300:1, mais cela est inhérent au fait que la +méthode GPFS-policy doit être limitée à une certaine intervalle d’interrogation (cinq minutes) pour +limiter l’impact sur le système de fichiers, et cela fournit une limite inférieure sur la latence +de transfert.

+

Sur des arbres comparables, le nombre de fichiers copiés avec la bibliothèque shim est toujours plus +élevé qu’avec la stratégie GPFS. Bien que l’exactitude soit encore en cours d’évaluation, la méthode +shim fonctionne apparemment mieux que la politique. Si nous revenons à la performance rsync d’origine +de 6 heures pour l’arbre, alors le ratio que nous prévoyons de livrer est de 6 heures contre 5 minutes … +ou une accélération de 72:1.

+

Ce qui précède est basé sur le rapport client suivant :

+
Jan 4th
+Preload:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t daily -d 2018-01-04
+Mean transfer time: 238.622s
+Max transfer time: 1176.83s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wh/banco/cutoff/2018010406_078_prog_gls_rel.tb0
+Min transfer time: 0.0244577s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/backup/ATX_radprm
+Total files: 142426
+Files over 300s: 44506
+Files over 600s: 14666
+Policy:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/mirror-ss1-from-hall2.conf  -t daily -d 2018-01-04
+Mean transfer time: 1355.42s
+Max transfer time: 2943.53s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/surface/201801041500_tt.obs
+Min transfer time: 1.93106s for file: /space/hall2/sitestore/eccc/cmod/prod/archive.dbase/dayfiles/par/2018010416_opruns_capa25km_rdpa_final
+Total files: 98296
+Files over 300s: 97504
+Files over 600s: 96136
+
+Jan 3rd
+Preload:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t daily -d 2018-01-03
+Mean transfer time: 264.377s
+Max transfer time: 1498.73s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/capa/bassin/6h/prelim/05/2018010312_05ME005_1.dbf
+Min transfer time: 0.0178287s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/XSS_0p1_statqpe
+Total files: 144419
+Files over 300s: 60977
+Files over 600s: 14185
+Policy:
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/mirror-ss1-from-hall2.conf  -t daily -d 2018-01-03
+Mean transfer time: 1175.33s
+Max transfer time: 2954.57s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/surface/201801032200_tt.obs
+Min transfer time: -0.359947s for file: /space/hall2/sitestore/eccc/cmod/prod/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/1h/XTI/201801031300~~PA,PA_PRECIPET,EE,1H:URP:XTI:RADAR:META:COR1
+Total files: 106892
+Files over 300s: 106176
+Files over 600s: 104755
+
+À garder à l’esprit:
+
+Nous avons 12 instances pour le préchargement alors que nous en exécutons 40 pour la stratégie.
+
+* J’ai filtré l’ensemble des fichiers qui faussaient fortement les résultats.
+* L’audit de préchargement en tranches horaires montre qu’il est fortement lié à l’instance.
+* Si nous devions l’augmenter, il devrait donner de bien meilleurs résultats dans des situations de compte élevé.
+
+Voici à nouveau le 4 janvier, mais par tranche horaire:
+
+dracette@eccc1-ppp1:~$ ./mirror.audit_filtered -c ~opruns/.config/sarra/subscribe/ldpreload.conf  -t hourly -d 2018-01-04
+00 GMT
+Mean transfer time: 0.505439s
+Max transfer time: 5.54261s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/6h/XME/201801040000~~PA,PA_PRECIPET,EE,6H:URP:XME:RADAR:META:NRML
+Min transfer time: 0.0328007s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/IWX_0p5_statqpe
+Total files: 847
+Files over 300s: 0
+Files over 600s: 0
+01 GMT
+Mean transfer time: 166.883s
+Max transfer time: 1168.64s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wh/banco/cutoff/2018010318_078_prog_gls_rel.tb0
+Min transfer time: 0.025425s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/XPG/201801031800_XPG_statomr
+Total files: 24102
+Files over 300s: 3064
+Files over 600s: 1
+02 GMT
+Mean transfer time: 0.531483s
+Max transfer time: 4.73308s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/archive.dbase/dayfiles/par/2018010401_opruns_capa25km_rdpa_preli
+Min transfer time: 0.0390887s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/XMB/201801031900_XMB_radprm
+Total files: 774
+Files over 300s: 0
+Files over 600s: 0
+03 GMT
+Mean transfer time: 0.669443s
+Max transfer time: 131.666s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/pa/1h/WKR/201801032000~~PA,PA_PRECIPET,EE,1H:URP:WKR:RADAR:META:COR2
+Min transfer time: 0.0244577s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/radprm/backup/ATX_radprm
+Total files: 590
+Files over 300s: 0
+Files over 600s: 0
+04 GMT
+Mean transfer time: 59.0324s
+Max transfer time: 236.029s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/wcps_20170501/wf/depot/2018010400/nemo/LISTINGS/ocean.output.00016.672
+Min transfer time: 0.033812s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/resps_20171107/forecast/products_dbase/images/2018010400_resps_ens-point-ETAs_239h-boxplot-NS_Pictou-001_240.png
+Total files: 2297
+Files over 300s: 0
+Files over 600s: 0
+05 GMT
+Mean transfer time: 6.60841s
+Max transfer time: 28.6136s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/rewps_20171018/forecast/products_dbase/images_prog/2018010400_rewps_ens-point-Hs_Tp_072h-45012-000_072.png
+Min transfer time: 0.0278831s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/XSM/201801032200_XSM_0p2_statqpe
+Total files: 3540
+Files over 300s: 0
+Files over 600s: 0
+06 GMT
+Mean transfer time: 1.90411s
+Max transfer time: 18.5288s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/radar/statqpe/backup/ARX_0p5_statqpe
+Min transfer time: 0.0346384s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/WWW/201801040600_WWW_statomr
+Total files: 757
+Files over 300s: 0
+Files over 600s: 0
+07 GMT
+Mean transfer time: 262.338s
+Max transfer time: 558.845s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/capa/bassin/6h/final/11/2018010400_11AA028_1.shp
+Min transfer time: 0.028173s for file: /space/hall2/sitestore/eccc/cmod/cmoi/opruns/ldpreload_test/hubs/suites/par/capa25km_20170619/gridpt/qperad/biais/6h/DLH/201801040000_DLH_statomr
+Total files: 23849
+Files over 300s: 11596
+Files over 600s: 0
+
+
+
+
+

Frais généraux

+

Quel est l’effet de la mise en service de la bibliothèque de Shim sur les tâches des utilisateurs? +Lorsqu’il est utilisé dans de grands modèles avec de bons modèles d’i/o nécessaires pour des performances +élevées, la surcharge ajoutée par la bibliothèque de Shim peut être négligeable. Cependant, une surcharge +supplémentaire est introduite chaque fois qu’un processus est généré, ferme un fichier et se termine. +Les scripts shell, qui fonctionnent en générant et en récoltant des processus en continu, voient un impact +maximal de la bibliothèque de shim. Ceci est exploré dans le numéro https://github.com/MetPX/sarrac/issues/15 :

+

Le numéro 15 décrit le script shell le plus défavorable qui réécrit un fichier, une ligne à la fois, +générant et récoltant un processus à chaque fois. Dans ce cas, nous voyons jusqu’à 18 fois plus de +pénalité dans les performances du script shell. Cependant, la réécriture du script shell en python +peut entraîner une amélioration de 20 fois la performance, avec presque aucune surcharge de la bibliothèque +shim (360 fois plus rapide que le script shell équivalent avec la bibliothèque shim active.)

+

Ainsi, les scripts shell qui étaient lents auparavant peuvent être beaucoup plus lents avec la bibliothèque +de shim, mais l’accélération disponible en reformulant des méthodes plus efficaces peut également avoir +des avantages beaucoup plus importants.

+
+
+

Contributions

+

Dominic Racette - ECCC CMC Operations Implementation

+
+

Responsable client sur le projet de mise en miroir. Beaucoup d’audit et d’exécution de tests. +Intégration/déploiement de plugins de copie. Beaucoup de tests et d’extraction de rapports de journal. +Il s’agissait d’un projet qui reposait sur une large participation des clients pour fournir une suite +de tests extrêmement variée, et Dominic était responsable de la part de ce travail.

+
+

Anthony Chartier - ECCC CMC Development

+
+

Responsable client de l’Acquisition de Données Environnementales, le système d’acquisition de données utilisé +par les séries canadiennes de prévisions météorologiques numériques.

+
+

Doug Bender - ECCC CMC Operations Implementation

+
+

Un autre analyste client participant au projet. Sensibilisation, engagement, etc…

+
+

Daluma Sen - SSC DCSB Supercomputing HPC Optimization

+
+

Création de bibliothèques C dans un environnement HPC, contribution au sélecteur de fichiers aléatoires, +conseil général.

+
+

Alain St-Denis - Manager, SSC DCSB Supercomputing HPC Optimization

+
+

Inspiration, consultation, sage. Initialement proposé bibliothèque de Shim. Aide au débogage.

+
+

Daniel Pelissier - SSC DCSB Supercomputing HPC Integration / then replacing Alain.

+
+

Inspiration/consultation sur le travail de la politique GPFS et l’utilisation des systèmes de stockage.

+
+

Tarak Patel - SSC DCSB Supercomputing HPC Integration.

+
+

Installation de Sarracenia sur des nœuds de protocole et d’autres emplacements spécifiques. +Développement de scripts de politique GPFS, appelé par les plugins de Jun Hu.

+
+

Jun Hu - SSC DCSB Supercomputing Data Interchange

+
+

Responsable du déploiement pour SPC, développement des plug-ins d’intégration de la politique GPFS Sarracenia, +les a mis en œuvre au sein de sr_poll, a travaillé avec le CMOI sur les déploiements. +A assumé la majeure partie de la charge de déploiement de SPC. Déploiement de la mise en œuvre inotify/sr_watch.

+
+

Noureddine Habili - SSC DCSB Supercomputing Data Interchange

+
+

Empaquetage Debian pour l’implémentation C. Certains travaux de déploiement également.

+
+

Peter Silva - Manager, SSC DCSB Supercomputing Data Interchange

+
+

Chef de projet, a écrit l’implémentation C y compris la bibliothèque shim, piraté sur le Python +aussi de temps en temps. Versions initiales de la plupart des plugins (en Sarra.)

+
+

Michel Grenier - SSC DCSB Supercomputing Data Interchange

+
+

Responsable du développement Python Sarracenia. Quelques corrections C aussi.

+
+

Deric Sullivan - Manager, SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/travail sur les déploiements avec la solution inotify.

+
+

Walter Richards - SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/travail sur les déploiements avec la solution inotify.

+
+

Jamal Ayach - SSC DCSB Supercomputing HPC Solutions

+
+

Consultation/travail sur les déploiements avec la solution inotify, ainsi que +l’installation native de paquets sur pré et post-processeurs.

+
+

Michael Saraga - SSC DCSB .Data Interchange

+
+

travailler sur la mise en œuvre de C en 2019, préparer des emballages et des emballages natifs +pour les distributions Suse et Redhat.

+
+

Binh Ngo - SSC DCSB Supercomputing HPC Solutions

+
+

Installation de paquets natifs sur les backends Cray.

+
+

FIXME: Qui d’autre devrait être ici: ?

+

La direction d’ECCC et de SPC a également bénéficié d’un soutien et d’une surveillance tout au long du projet.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/deploiement_2018.html b/fr/Explication/Histoire/deploiement_2018.html new file mode 100644 index 000000000..1fade6e36 --- /dev/null +++ b/fr/Explication/Histoire/deploiement_2018.html @@ -0,0 +1,454 @@ + + + + + + + Sarracenia Janvier 2018 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sarracenia Janvier 2018

+

Sarracenia est une petite application développée de façon itérative +en s’adressant à une seule cas d´usage à la fois de sorte que le +développement et le déploiement sont inextricablement liés jusqu’ici. +Ce processus itératif a précipité des changements au cœur de +l´application qui en ont fait que jusqu’à présent elle était une cible +mouvante. Dans Janvier 2018, l’application a atteint le point où tous +les cas d’utilisation prévue sont traités par l’application. Au cours +de l’année à venir, l’accent sera mis sur les points suivants sur la +facilitation de l’embarquement, le développement de certains services +dérivés, et le déploiement de la nouvelle application complète de +manière plus générale.

+ +
+

Comparison to 2015 Video

+

La vidéo de novembre 2015 ( Sarracenia in 10 Minutes ) +a tracé les grandes lignes d’une vision. La première phase des travaux de +développement s’est déroulée en 2015 et au début de l’année 2016, suivie de +déploiements importants plus tard en 2016. Cette mise à jour, écrit au début +de 2018, explore les progrès réalisés principalement en 2017.

+

Les cas d’utilisation mentionnés dans la vidéo qui ont été implantés:

+
    +
  • Le pompage météorologique central a fait des progrès substantiels en matière de migration. +à la nouvelle pile. C’était le principal cas d’utilisation initiale qui a motivé les travaux initiaux. +La transformation n’est pas complète, mais elle est bien en main.

  • +
  • Acquisition redondante de RADAR par l’intermédiaire de deux centres nationaux. (printemps 2016)

  • +
  • Ninjo national (poste de travail principal pour les prévisionnistes) Diffusion (été 2016)

  • +
  • Traitement RADAR unifié (application pour transformer les scans de volume en produits) +les flux de données. (jusqu’en 2017.)

  • +
+

Cas d’utilisation dans la vidéo, mais pas encore réalisés :

+
    +
  • Utilisation par l’utilisateur final. Quelques essais ont été achevés au début de l’année 2017, ce qui a donné lieu à certains des essais suivants +et refactoring, et maintenant refactoring, et re-test.

  • +
  • Les ensembles de données des séquenceurs ont attendu que les cas d’utilisation par l’utilisateur final soit +améliorée.

  • +
  • reports aux sources qui ont consommé leurs produits. C´est présent dans +l´application mais il faut le configuré, et des tests et un déploiement +minutieux sont nécessaires.

  • +
+

Mise en œuvre de cas d’utilisation imprévue :

+
    +
  • Échange de données du SMT : CMC <-> NWS. Les EDAN ont demandé une modification de la connectivité. +en décembre 2015.

  • +
  • HPC mirroring (à compléter au printemps 2018)

  • +
  • Legacy Application 7-way réplication (pour les SPC) mise en œuvre l’année dernière.

  • +
  • Acquisition de GOES-R (en direct à partir de janvier 2018.)

  • +
+

Les détails suivront.

+
+
+

Flots Centrales

+

La diagramme ci-dessous correspond aux flux de données quotidiennes +déployées à l’appui du programme Environnement Canada, surtout pour les +prévisions météorologiques opérationnelles, en place depuis le début des +travaux jusqu´à janvier 2018.

+../../../_images/E-services_data-volume_pas.png +

Sarracenia est utilisée de façon opérationnelle pour acquérir environ quatre téraoctets. +des observations provenant de systèmes automatisés d’observation météorologique, des RADARS météorologiques qui +livrer les données directement à nos hubs, fichier public international géré par des pairs. +qui fournissent des images satellitaires et des produits numériques provenant d’autres pays de l’Union européenne. +les centres météorologiques nationaux.

+

Dans le centre de données principal du calcul haute performance (HPC), il y a deux centres de données +deux magasins de chantier et deux grappes de pré- et post-traitement. +En cas de défaillance d’un composant d’une chaîne, l’autre peut prendre le relais. L’entrée +les données sont envoyées à une chaîne primaire, puis le traitement sur cette chaîne est mis en miroir, +en utilisant la sarracénie pour copier les données dans l’autre chaîne. That´s au sujet de 16 de la +25 téraoctets du trafic du centre de données dans ce diagramme.

+

Une distillation des données acquises, ainsi que l’analyse et les prévisions effectuées en HPC, +est les sept téraoctets en haut à droite, c’est à dire qu’il est envoyé aux sept régions. +Centres de prévision des intempéries (SPC´s).

+

Les produits du SPC´s et du HPC central sont ensuite partagés avec le public. +et des partenaires de l’industrie, du milieu universitaire et d’autres gouvernements.

+
+
+

Applications Météorologiques

+

FIXME : image ?

+

Il y a un certain nombre (peut-être une douzaine ?) d’applications plus anciennes (les plus importantes). +BULLPREP et Scribe) utilisés depuis des décennies dans les centres de prévision des intempéries. +pour créer des produits de prévision et d’alerte. Ces demandes sont basées sur un dossier. +qu’ils lisent et écrivent. Auparavant, chaque application avait sa propre sauvegarde. +avec l’un des six autres bureaux et des ententes bilatérales ont été conclues. +pour copier des données spécifiques parmi les arbres.

+

En janvier 2017, réplication complète à 7 voies de l’arborescence des fichiers d’état du fichier +a été mise en œuvre de manière à ce que tous les bureaux aient des copies des dossiers en format +en temps réel. Ceci est accompli en utilisant la sarracénie à travers la plaque tournante orientale. N’importe quel +peut maintenant prendre en charge le travail sur n’importe quel produit pour n’importe quel autre, sans qu’il n’y ait d’exigences particulières. +de travail d’application nécessaire.

+
+
+

Acquisition de GOES-R

+

Acquisition de produits GOES-R simulés et réels auprès du PDA de la NOAA, ainsi que des produits GOES-R. +via des liaisons descendantes locales à un endroit (pour finalement en devenir deux) était entièrement +par Sarracénie. Le déploiement opérationnel de GOES-R s’est déroulé dans la région de l +première semaine de janvier 2018.

+
+
+

acquisition HPC

+

FIXME : image ?

+

L’environnement de supercalculateur a été entièrement remplacé en 2017. Dans ce contexte, +la suite d’acquisition de données environnementales du client (acronyme ADE) était +de travailler avec des performances beaucoup plus élevées qu’auparavant, et d’accepter +La sarracénie se nourrit directement, plutôt que d’accepter les aliments de la génération précédente. +Pompe (Sundew.) Le volume et la vitesse d’acquisition des données ont été considérablement réduits. +s’est améliorée en conséquence.

+
+
+

RADAR

+

Si nous commençons par l’acquisition de données RADAR, les différents systèmes RADAR +utiliser FTP et/ou SFTP pour envoyer des fichiers aux centres de communication de l’est et de l’ouest. +Ces hubs exécutent le composant de surveillance d’annuaire (sr_watch) et déterminent +pour les analyses de volume à mesure qu’elles arrivent. Le traitement RADAR unifié +(URP) sr_subscribes systems sr_subscribes to a hub, listening for new volume scans, and +télécharge les nouvelles données dès qu’elles sont affichées. Les systèmes URP en dérivent alors de nouveaux +et les annoncer au hub local à l’aide du composant sr_post. +Avec le temps, nous espérons avoir un deuxième URP à la plaque tournante de l’Ouest.

+

Dans les bureaux régionaux, les serveurs de visualisation NinJo téléchargent des scans de +volume et des scans de volume. Les données sont traitées à partir de l’URP en +utilisant des abonnements identiques, en tirant les données de l’URP quelle que soit +la plaque tournante nationale qui rend les données disponibles en premier. L’échec d’un +est transparent pour les données RADAR, en ce sens que les scans de volume seront +téléchargé à partir de l’autre concentrateur, et l’autre processeur URP produira le fichier +produits nécessaires.

+../../../_images/RADAR_DI_LogicFlow_Current.gif + +

Chaque site a plusieurs serveurs Ninjo. Nous utilisons des serveurs de fichiers +http (des dossiers accessibles sur le Web) pour servir les données. Cela permet +une intégration facile des caches web-proxy, ce qui signifie que seul le premier +serveur Ninjo à demander des données sera téléchargé à partir du hub national. +Les autres serveurs Ninjo obtiendront leurs données à partir du cache proxy local. +L’utilisation de Sarracenia pour les notifications lorsque de nouveaux +produits sont disponibles est complètement indépendante de l’utilisation de +Sarracenia. méthode utilisée pour servir et télécharger des données. Les +serveurs de données peuvent être implémentés avec une grande variété d’outils. +et très peu d’intégration est nécessaire.

+
+
+

HPC Mirroring

+

Tout au long de l’année 2017, les travaux se poursuivaient pour mettre en place +un miroir à grande vitesse entre les magasins du site du superordinateur pour +permettre le basculement. Ces travaux sont maintenant dans une phase finale de +déploiement et devraient être opérationnels d’ici le printemps 2018. +Pour plus de détails, voir : Cas d´utilisation de miroitage CHP

+
+
+

Modifications d’application en 2017

+

Le développement de Sarracenia avait été exploratoire pendant un certain +nombre d’années. Les cas d’utilisation initialement attaqués étaient ceux qui +avaient un degré élevé de participation d’experts. Elle s’est déroulée en +suivant le stratégie de produit minimum viable (MVP) pour chaque cas +d’utilisation, en acquérant des fonctionnalités pour traiter le cas +d’utilisation suivant avant le déploiement. En 2016, le déploiement +national de NinJo et de la météo.

+

Exploration des cas d’utilisation élargie :

+
    +
  • Mirroring : Avant ce cas d’utilisation, la sarracénie était utilisée pour +la diffusion de données brutes sans égard pour permissions, propriété, liens +symboliques, etc…. Pour le cas d’utilisation en miroir, les métadonnées exactes. +la réplication était une exigence étonnamment complexe.

  • +
  • Mise en œuvre C : En explorant la mise en miroir à grande échelle, il est +devenu évident que pour un nombre suffisamment important d’images (27 millions +de fichiers), la seule méthode pratique disponible était l’utilisation d’une +bibliothèque C shim. Le fait que tous les codes d’utilisateur invoquent un +script python3 est complètement absurde dans un environnement HPC. +Il était nécessaire d’implémenter une version C du code d’affichage Sarracenia +pour l’utilisation par la bibliothèque shim. Une fois l’implémentation C +commencée, ce n’était qu’un petit travail supplémentaire pour implémenter +une version C de sr3_watch (appelé sr_cpost) qui était beaucoup plus efficace +en mémoire et en CPU que l’original python.

  • +
  • Implantation de client Node.js : Un client du datamart public a décidé d’implémenter +suffisamment de Sarraenia pour télécharger les avertissements en temps réel.

  • +
  • L’application a été refactorisée pour maximiser l’uniformité par la +réutilisation du code, réduisant ainsi d’environ 20 % les coûts d’ +exploitation de l’application. la taille du code en un point. Le code +est revenu à la taille initiale lorsque de nouvelles fonctionnalités ont +été ajoutées, mais il reste assez compact à moins de 20 kloc.

  • +
  • Utilisation par l’utilisateur final : Tous les déploiements effectués jusqu’à +présent sont mis en œuvre par des analystes ayant une compréhension +approfondie de Sarracenia, ainsi qu’un soutien et des connaissances +approfondies. Cette année, nous avons vécu plusieurs itérations et +tentatives d’avoir des utilisateurs qui déploient leurs flux, recueillant +les retours d’expérience et facilitant la tâche des utilisateurs de la +prochaine itération. Beaucoup de ces changements étaient des +changements cassants, en ce que les options et les moyens ou +étaient encore des prototypes et nécessitaient une révision.

  • +
+

Modifications pour prendre en charge l’utilisation par l’utilisateur final :

+
    +
  • Les échanges étaient une ressource définie par l’administrateur. Le modèle de +permission a été modifié de telle sorte que les utilisateurs peuvent +maintenant déclarer les échanges.

  • +
  • Auparavant, il fallait regarder sur les sites web pour trouver des exemples. +Maintenant, la commande list affiche de nombreux exemples inclus avec le +paquet.

  • +
  • Il était difficile de trouver où mettre les fichiers de configuration. Les +commandes list/add/remove/edit simplifient cela.

  • +
  • Dans chaque point d’entrée de plugin, il fallait modifier différentes +variables d’instance, a été refactorisé pour la cohérence sur tous +(on_msg, on_file, on_part, on_part, on_post, do_download, do_send, do_send, etc….)

  • +
  • Les spécifications de cloisonnement étaient obscures et ont été remplacées +par l’option Option blocksize, avec seulement trois +possibilités : 0, 1, beaucoup.

  • +
  • Le routage à travers plusieurs pompes était obscur. L’algorithme original +était remplacé par un plus simple avec des valeurs par défaut plus +intelligentes. Les utilisateurs peuvent maintenant généralement l’ignorer.

  • +
  • Une interface de plugin beaucoup plus élégante est disponible pour avoir des +routines multiples qui fonctionnent ensemble, spécifiés dans un seul plugin.

  • +
  • Auparavant, annoncés uniquement sur les serveurs web par rapport à l’URL +racine. Maintenant, la prise en charge des URL de base non racine a été +ajoutée.

  • +
+

La seule caractéristique opérationnelle majeure introduite en 2017 a été +Sauvegarder/restaurer/ressai** : Si une destination a un problème, il y a +risque substantiel de surcharger les courtiers AMQP en laissant les files d’attente de produits à +de transfert se transforment en millions d’entrées. Fonctionnalité pour une utilisation efficace (en +parallèle) décharger les files d’attente des courtiers sur le disque local a été implémenté afin d’adresser +ceci. Dans un premier temps, la récupération devait être déclenchée manuellement (restauration) mais par +à la fin de l’année, un mécanisme automatisé de récupération (réessai) fonctionnait. +de déploiement, ce qui réduira les besoins en matière de surveillance et de surveillance. +l’intervention dans les opérations.

+
+
+

À venir en 2018

+

A partir de la version 2.18.01a5, tous les cas d’utilisation ciblés ont été +explorés et des solutions raisonnables sont disponibles, de sorte qu’il ne +devrait pas y avoir d’autres modifications à la la langue ou les options de +configuration existantes. Aucun changement à l’existant les paramètres de +configuration sont planifiés. Des ajouts mineurs peuvent encore se produire, +mais pas au prix de la rupture des configurations existantes. Le noyau +est maintenant terminée.

+

Attendez-vous au début de l’année 2018 pour la dernière version de paquet +alpha et pour les travaux ultérieurs d’être sur une version bêta avec une +cible de beaucoup plus de version stable à longue durée de vie en 2018.

+
    +
  • Le déploiement des cas d’utilisation HPC en miroir sera terminé.

  • +
  • Le cas d’utilisation du dépôt permanent de fichiers (PFD) sera déployé. À l’heure actuelle, ce +est utilisé pour couvrir un horizon temporel court. On peut l’étendre arbitrairement dans le cadre de la +en persistant l’arbre basé sur le temps jusqu’au stockage proche de la ligne. En développement +depuis 2016, en progression progressive.

  • +
  • Améliorer la cohérence du déploiement : Les changements apportés en 2017 ont semé la confusion dans l’esprit de l +les analystes experts, car des changements importants dans les détails se sont produits d’une version à l’autre. +Différents déploiements utilisent actuellement des versions opérationnelles différentes, et la plupart d’entre eux sont des versions opérationnelles différentes. +les questions soulevées dans le cadre des opérations sont traitées par le code existant, mais ne le sont pas. +mais déployé dans ce cas d’utilisation. En 2018, nous réexaminerons les déploiements précoces pour +les mettre à jour.

  • +
  • Amélioration continue des tests de pré-déploiement.

  • +
  • L’outil d’indexation Sarrasemina, qui facilite la recherche de flux à déployer. +pour aider à l’embarquement.

  • +
  • Amélioration de la documentation d’embarquement. Les matériaux de référence sont encore en cours, mais +Les matériaux d’introduction à démarrage rapide et les matériaux orientés gateway ont besoin d’être travaillés. +Des traductions en français sont également nécessaires.

  • +
  • Rapports : Bien que le reportage ait été mis en place dès le début, il s’est avéré être très efficace. +Les déploiements effectués jusqu’à présent l’ont donc omis. Maintenant que le déploiement +les charges se calment, cette année devrait nous permettre d’ajouter un rapport en temps réel. +routage vers les configurations déployées. Il n’y a pas de fonctionnalité à développer, +car tout est déjà dans l’application, mais le plus souvent non utilisé. L’utilisation peut +découvrir d’autres problèmes.

  • +
  • algorithmes de checksum plugins. Actuellement, les algorithmes de somme de +contrôle sont intégrés. Il est nécessaire de prendre en charge les plugins +pour rajouter algorithmes de somme de contrôle définis par l’utilisateur +(attendus en 2.18.02a1).

  • +
  • Poursuite du remplacement progressif des configurations des applications +alpha. (RPDS, Sundew).

  • +
  • Poursuite de l’adaptation des applications à Sarracenia (DMS, GOES-R).

  • +
  • Déploiement d’instances supplémentaires : flux.weather.gc.ca, +hpfx.collab.science.gc.ca, etc…..

  • +
  • Poursuite des travaux sur l’approbation et le financement de la plaque +tournante de l’Ouest (aka. Projet Alta).

  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/index.html b/fr/Explication/Histoire/index.html new file mode 100644 index 000000000..277cca15f --- /dev/null +++ b/fr/Explication/Histoire/index.html @@ -0,0 +1,224 @@ + + + + + + + Histoire — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Histoire

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/mesh_gts.html b/fr/Explication/Histoire/mesh_gts.html new file mode 100644 index 000000000..938172c2f --- /dev/null +++ b/fr/Explication/Histoire/mesh_gts.html @@ -0,0 +1,979 @@ + + + + + + + Échange de données de type maillé pour le WIS-GTS en 2019 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Échange de données de type maillé pour le WIS-GTS en 2019

+

À l’origine, une partie de la démonstration au comité de WMO sur les protocoles de mise +en file d’attente des messages. Bien que cette proposition n’ait pas été retenue à ce +moment-là, elle a servi de base pour beaucoup de discussions depuis.

+ +
+

Résumé

+

Au cours de la dernière décennie, le Service d’information de l’Organisation +météorologique mondiale (WMO), partie 1, a mis en place des catalogues de +métadonnées pour certains ensembles de données météorologiques mondiales. Dans WIS-DAR, +Discovery, Access, and Retrieval, l’accent a été mis sur Discovery, car il +s’agissait d’une difficulté majeure. En termes d’échange de données proprement dit, +le réseau préexistant: le Système mondial de télécommunications (SMT) est resté la méthode +acceptée par WMO pour faire circuler les données météorologiques en temps réel météorologique +[1] utilisée pour obtenir l’accès et soutenir la récupération. Il le fait par +le biais d’accords bilatéraux établis entre les membres individuels.

+

Le réseau préexistant alimente des systèmes préexistants qui fonctionnent +sur des prémisses différentes de WIS, et il n’y a pas de mappage particulièrement +bon de D à A et R aujourd’hui. L’utilisation des métadonnées SIO pour +favoriser l’échange de données en temps réel n’est pas efficace, et les données +GTS ne sont pas facilement interopérables avec les technologies principalement +Web d’autres industries.

+

Cette note suggère qu’un réseau maillé de serveurs Web est maintenant possible +grâce aux grands progrès technologiques qui réduisent le besoin de règles et de +structures pour l’échange international de données météorologiques. Auparavant, +les limitations technologiques imposaient des contraintes strictes à l’échange +international de données. De nos jours, un échange international efficace de +données peut être organisé avec moins d’intervenants, moins de hiérarchie, +moins de coordination. Une stratégie d’interconnexion plus souple et moins +lâche permet aux membres d’échanger beaucoup plus facilement de nouveaux +ensembles de données de manière systématique, sans avoir à élaborer des +accords d’échange bilatéraux ou multilatéraux. En effet, souvent, le GTS +est contourné parce qu’il est si mal adapté à de tels échanges.

+

Les membres qui acceptent d’échanger des données peuvent le faire directement, +ce qui complète le rêve du WIS. Les travaux existants sur le SIO, la normalisation +de l’échange de métadonnées, sont complétés par l’ajout de l’échange des données +sous-jacentes à l’aide de méthodes ouvertes normalisées, avec une cartographie +simple à partir des métadonnées. Étant donné que la mise en œuvre proposée utilise +des normes existantes, WMO n’a pas besoin d’en définir d’autres, et l’interopérabilité +et l’accès pour d’autres acteurs de la société au sens large devraient être simples. +Le protocole sr_post, et un certain nombre d’implémentations existantes de celui-ci, +conviennent parfaitement.

+

Bien que l’on pense que le protocole sr3_post a un grand potentiel pour améliorer l’échange +de données de WMO, il faudra quelques années pour l’adopter, et avant son adoption, il faut +s’entendre sur le contenu de l’arborescence des fichiers. Aujourd’hui, la prochaine étape +consisterait à trouver des pays partenaires avec lesquels s’engager dans des échanges de +données expérimentales pour valider l’approche et déterminer en collaboration les détails +de la mise en œuvre.

+ +
+
+

GTS a été conçu il y a longtemps

+../../../_images/gtsstructureL.png +

Le Système mondial de télécommunications (GTS) du Service météorologique mondial (WIS) de +l’Organisation météorologique mondiale (WMO) est la méthode acceptée par WMO pour diffuser +des données météorologiques en temps réel [1] Il le fait par le biais d’accords bilatéraux +préétablis sur qui fait circuler quoi à qui. Lorsque les données arrivent, elles sont immédiatement +envoyées aux destinations appropriées. En termes matériels, le GTS était un ensemble de liaisons +point à point. Il est, en général, très fiable et a de bonnes performances pour les types de données +pour lesquelles il a été envisagé.

+

Le GTS a appliqué avec succès et élégance les technologies des années 1940 pour obtenir un échange +mondial de données météorologiques, le plus évident, pour permettre au transport aérien commercial +de se développer de manière exponentielle au cours des décennies suivantes. Il a été fabriqué dans +un monde de liaisons téléphoniques point à point coûteuses, de bande passante très faible, de très +faible puissance de calcul et de peu de normes existantes pour un transfert de données fiable. +Aujourd’hui, les technologies sous-jacentes incarnées par Internet et le réseau principal régional +de communication de données (RMDCN) +(qui ont déjà absorbé les liaisons physiques du GTS) sont complètement différentes: la bande passante +et le stockage sont relativement bon marché, la puissance de calcul est moins chère, les liaisons +point à point sont maintenant plus chères que les nuages multipoints. Aujourd’hui, les membres de +WMO veulent échanger des ordres de grandeur plus de types de données et des volumes plus élevés +à une vitesse plus élevée.

+

Dans le passé, la communauté météorologique a dû créer des normes parce qu’aucun besoin similaire +n’existait dans le reste de l’industrie. Au cours des dernières décennies, à mesure que l’adoption +de l’internet s’est élargie et approfondie, des solutions horizontales ont émergé pour la plupart +des questions techniques abordées par les normes de WMO. L’orientation stratégique de WMO a toujours +été et continuera d’être d’utiliser des technologies du reste du monde et de ne pas définir inutilement +leurs propres normes.

+
+
+

GTS est limité et inflexible

+../../../_images/GTS_Routing.jpg +

Dans le VTM traditionnel, lorsque de nouvelles données sont mises à disposition par un centre +météorologique national (NMC), celui-ci doit émettre des avis et probablement discuter avec son +centre régional de télécommunications (RTH) de l’acceptation et de l’acheminement des nouvelles +données. Les RTH doivent discuter entre eux de l’opportunité d’échanger les données, et les +autres NMC doivent en faire la demande à leur RTH. Pour que la CNG A puisse mettre les données +à la NMC B, le personnel du STM des NMC et de tous les RTH intervenants doit accepter la demande +et y donner suite.

+

De plus, les tables de routage de chaque NMC et de chaque RTH sont théoriquement, mais en pratique, +difficilement inspectables. Il se peut que le produit recherché par NMC B soit déjà disponible +dans leur RTH local, mais ni A, ni B n’ont de moyen efficace de le déterminer, autre que de +demander aux administrateurs du RTH de B. Le routage manuel est inefficace, opaque et nécessite +beaucoup de ressources humaines.

+

Enfin, le GTS a une taille de message maximale, qui, bien qu’elle ait été augmentée de 14 000 octets +à 500 000 octets au cours des dernières années, est encore très limitative. Cela exclut de nombreux +ensembles de données modernes importants (par exemple, RADAR, satellite, PNT). WMO n’a pas besoin de +son propre transport de données, comme le montre l’utilisation par de nombreux membres de protocoles +sans telles limitations, adoptés à partir de l’internet au sens large, tels que FTP, SFTP et HTTP. +Encore plus souvent, ces transferts sont effectués par un arrangement bilatéral, car les transferts +d’ensembles de données plus importants ne peuvent pas être exprimés dans les protocoles GTS actuels.

+

Le SIE initial, tel qu’il a été formulé il y a plus de dix ans, était en partie une tentative de remédier +à cette opacité en introduisant certains concepts de gestion de l’information (GI) et en prenant en charge +le DAR : découverte, accès et récupération. Tout ce que les membres de WMO veulent savoir, c’est quelles +données sont disponibles auprès de quels centres. Nous publions donc des métadonnées dans les centres +mondiaux de services d’information, où les informations météorologiques en temps réel du monde sont +disponibles et certains moyens de récupération sont spécifiés.

+

Lorsqu’il s’agit dans l’abstrait, sans contraintes de temps, d’ajouter/ou de petits ensembles de données, +le temps de récupération n’a pas d’importance, mais la pénalité d’accès imposée par l’utilisation de bases +de données pour la récupération individuelle augmente avec le nombre d’éléments stockés et le nombre de +requêtes ou de récupérations à soutenir. Au départ, le WIS était surtout préoccupé par l’obtention d’une +plus grande visibilité des données, la compréhension des données disponibles. La partie 1 du WIS a +principalement mis en œuvre une couche de métadonnées, tandis que le GTS a persisté à transférer les +données réelles. Une fois que WIS Part 1 était en service et que DAR était disponible, ce qui à première +vue semble beaucoup plus facile et plus convivial, pourquoi tout le monde n’a-t-il pas simplement +utilisé DAR pour remplacer le GTS?

+../../../_images/dar.png +

L’architecture WIS a tendance à concentrer la charge sur le GISCS, qu’il le veuille ou non. Même en +supposant qu’ils le souhaitent, répondre à de gros volumes de requêtes dans une telle architecture +est un problème. Le modèle mental pour cela est une base de données et chaque récupération est +conceptualisée comme une requête. De la théorie de la complexité computationnelle[2]_, chaque +requête est souvent une opération O(N) (ou dans le meilleur des cas d’indexation parfaite, O(log N)) +et la récupération d’éléments spécifiques par leur clé est aussi, au mieux, O(log N).

+

Pour effectuer la récupération (le R dans DAR) de tous les éléments d’un index, un à la fois, le +meilleur algorithme a la complexité N * log N. Le meilleur cas ne se présente que si les indices +sont correctement conçus pour la méthode d’accès prévue. En pratique, sans une conception et une +mise en œuvre minutieuses, la performance peut être dévolue à N**2.

+ +
+
+

Les bases de données sont optimales pour les enregistrements de taille fixe. Personne ne les utilise de cette façon

+

Il existe généralement deux grandes écoles de pensée sur le stockage des données météorologiques: +les fichiers et les bases de données. Pendant de nombreuses années, les données étaient relativement +petites, il y avait relativement peu de types de données, et elles tenaient dans de petites bases de +données et, avec suffisamment d’analyse, on pouvait les normaliser à des champs de taille fixe.

+

Les bases de données relationnelles ont été inventées une décennie ou deux après le GTS, et elles +optimisent le stockage et la récupération de données de taille fixe. ELles atteignent des performances +presque optimales grâce à une sélection minutieuse du modèle de données et à l’utilisation intensive de +champs de taille fixe. En pratique, l’utilisation de champs de taille fixe s’avère être une contrainte +difficile à satisfaire et de nombreux ensembles de données intéressants, en particulier sur les systèmes +dont la fonction principale est la transmission de données, sont plus logiquement stockés sous forme de +séquences d’octets de taille arbitraire, généralement appelées: Binary Large Objects, ou BLOBS. Lorsque +l’on stocke des BLOBS, une base de données devient algorithmiquement un peu différente d’un système de +fichiers.

+

Essentiellement, les BLOBS sont une liste indexée de flux d’octets. Une fois qu’une base de données +utilise des BLOBS, elle supporte la même surcharge pour la récupération des données qu’un système de +fichiers correctement utilisé. Quelle que soit la méthode de stockage, le coût de récupération sera +O(log N) pour une clé connue. Dans un système de fichiers, la clé est le nom. Dans une base de données, +la clé est une valeur object-id ou index. Si l’on souhaite utiliser plusieurs indices, on peut toujours +utiliser une base de données, ce qui est en grande partie ce qu’est la partie 1 du SIF. Il s’agit simplement +d’une pratique standard pour stocker les données réelles dans des BLOBS, et les systèmes de fichiers sont +une méthode compétitive pour les stocker.

+

Au fil du temps, à mesure que la taille des éléments de données augmente, il devient de plus en plus logique +de les stocker dans des systèmes de fichiers et de limiter l’utilisation des systèmes de base de données au +stockage des métadonnées et des index qui pointent vers les éléments de données stockés dans les fichiers.

+
+
+

Internet Push est un mauvais ajustement pour les gros flux

+

Les technologies dites Push sont en fait Pull. Un client demande à un serveur s’il dispose +de nouvelles données. Le serveur répond avec la liste des nouvelles données disponibles, puis +le client extrait les données. Cela signifie qu’une entité stockant des données doit récupérer +les éléments du magasin de données (avec un coût O(log N) pour chaque extraction). Comme le +domaine est un traitement en temps réel, le temps nécessaire à l’obtention des données par +un client est également pertinent et lié à la fréquence maximale à laquelle un client est +autorisé à demander des mises à jour. En général, les protocoles ATOM/RSS prévoient un +intervalle minimum entre les événements d’interrogation de cinq minutes.

+

Chaque événement d’interrogation nécessite que le serveur examine sa base de données pour toutes +les entrées correspondantes, cette recherche est probablement une opération d’ordre N. Ainsi, les +réponses aux demandes de sondage sont coûteuses, et les extractions à partir du système de données +sont également coûteuses, ce qui motive probablement le découragement habituel des sondages rapides.

+

Dans le meilleur des cas, des index basés sur le temps seront présents, et on pourra rechercher une +table avec cette dimension et engager des opérations log(N) pour trouver la première observation à +récupérer, puis avancer le long de cet index. Dans de nombreux cas pratiques, les bases de données +ne sont pas indexées par heure, et donc la recherche initiale concerne toutes les stations, puis il +faut examiner le temps pour les entrées récupérées, ce qui entraînera des opérations N**2, et dans +certains cas, cela peut être encore pire.

+

Le coût réel de service d’un client dépend essentiellement de la construction optimale des indices +du serveur. Ces caractéristiques sont cachées dans une base de données et ne sont pas facilement +inspectées par quiconque sauf l’administrateur de la base de données.

+
+
+

Store And Forward est souvent meilleur dans la pratique

+

“Store and Forward (Stocker et transférer)” est un terme que nous utiliserons ici pour désigner +les technologies qui traitent des données à la réception, par opposition au simple stockage des +données et à l’attente des sondages des clients. Les systèmes en temps réel tels que le GTS +contournent le problème des frais de récupération en stockant et en transférant en même temps. +Lorsqu’une donnée est reçue, une table des parties intéressées est consultée, puis la transmission +est effectuée sur la base des données déjà “récupérées”.

+

Le coût de transfert d’un article à un client donné est plus proche de O( log N ).

+

Cela fonctionne comme une optimisation car on transfère le message exactement au moment où il est +reçu, de sorte que l’ensemble du processus de recherche est ignoré pour tous ces consommateurs connus. +À titre de comparaison, les normes web de sondage normalisent le coût de la recherche à chaque +intervalle d’interrogation.

+

Le coût de la recherche est très variable et n’est pas contrôlé par le serveur. Des requêtes mal +structurées (par exemple par station, puis heure) peuvent entraîner une requête N*log(N) ou même +une complexité N-carré.

+

Cela est particulièrement aigu pour les informations d’alerte météorologique, où une fréquence +d’interrogation élevée est un besoin commercial, mais le volume de données est relativement faible +(les alertes sont rares). Dans de tels cas, les données d’interrogation peuvent être 10 fois, voire +100 fois la quantité de transfert de données nécessaire pour envoyer les avertissements eux-mêmes.

+

En pratique, la charge sur les serveurs avec d’importants flux en temps réel vers de nombreux +clients sera inférieure de plusieurs ordres de grandeur avec une technologie de poussée réelle, +telle que le GTS traditionnel, à celle supportant la même charge avec les technologies Internet Push. +Un coût distinct mais connexe de l’interrogation est la bande passante pour les données d’interrogation. +En transférant les notifications à la réception, plutôt que d’avoir à gérer les interrogations, on +réduit la charge globale, éliminant ainsi la grande majorité du trafic de lecture.

+

Un exemple concret d’économies de bande passante, à partir de 2015, serait celui d’une entreprise +Allemande qui a commencé à récupérer les sorties NWP du datamart canadien à l’aide du web-scraping +(sondage périodique du répertoire). Lorsqu’ils sont passés à l’utilisation de la méthode push AMQP, +le nombre total d’octets téléchargés est passé de 90 Go/jour à 60 Go par jour pour les mêmes données +obtenues. 30 GBytes/jour n’étaient que des informations (d’interrogation) pour savoir si de nouveaux +résultats de modèle étaient disponibles.

+

Les exigences pour un système de stockage et de transfert:

+
    +
  • connectivité TCP/IP,

  • +
  • transmission de données en temps réel,

  • +
  • par destination de file d’attente pour permettre l’asynchronie (clients qui fonctionnent à des vitesses différentes ou qui ont des problèmes transitoires),

  • +
  • Garanties d’intégrité au niveau de l’application.

  • +
+

De plus, la possibilité de régler les abonnements, en fonction de l’intérêt du client, +optimisera davantage le trafic.

+

En termes de technologies internet, les principaux protocoles d’échange de données en temps réel +sont XMPP et websocket. XMPP fournit une messagerie en temps réel, mais il n’inclut aucun concept +d’abonnements, hiérarchiques ou autres, ou de file d’attente. Les sockets Web sont une technologie +de type transport. L’adoption de l’un ou l’autre de ces éléments signifierait la création d’une pile +spécifique au domaine pour gérer les abonnements et les files d’attente. Le protocole AMQP (Advanced +Message Queueing Protocol) n’est pas une technologie Web, mais c’est une norme internet assez mature, +qui provient du secteur financier et comprend toutes les caractéristiques ci-dessus. Il peut être +adopté tel quel et une application AMQP relativement simple peut être construite pour servir les +notifications sur les données nouvellement arrivées.

+

Alors qu’AMQP fournit une couche de messagerie et de file d’attente robuste, une petite +application supplémentaire qui comprend le contenu spécifique des messages AMQP, et c’est +la valeur du protocole Sarracenia et de l’application proposée comme implémentation de +référence du protocole. Sarracenia envoie et reçoit des notifications via AMQP. Cette +application ne nécessite ni ne possède aucune fonctionnalité spécifique à WMO et peut être +utilisée pour la réplication de données en temps réel en général.

+../../../_images/A2B_message.png +

Une notification Sarracenia contient une URL (Uniform Resource Location) informant les clients +qu’une donnée particulière est arrivée, les invitant ainsi à la télécharger. L’URL peut annoncer +n’importe quel protocole que le client et le serveur comprennent : HTTP, HTTPS, SFTP par exemple. +Si de nouveaux protocoles deviennent importants à l’avenir, leur mise en œuvre peut se faire sans +modification de la couche de notification.

+

Comme ces notifications sont envoyées en temps réel, les clients peuvent lancer des téléchargements +alors que la référence en question est encore dans la mémoire du serveur et ainsi bénéficier de +performances de récupération optimales. Comme le temps d’accès des clients aux données est plus +étroitement regroupé dans le temps, les i/o globales effectuées par le serveur sont minimisées.

+

Une notification contient également une empreinte digitale, ou somme de contrôle, qui identifie +de manière unique un produit. Cela permet aux nœuds d’identifier s’ils ont déjà reçu une donnée +particulière ou non. Cela signifie que les risques de mauvais acheminement des données sont plus +faibles qu’auparavant, car s’il y a des cycles dans le réseau, ils sont résolus automatiquement. +Les cycles dans le graphique de connectivité sont en fait un avantage car ils indiquent plusieurs +routes et redondance dans le réseau, qui seront automatiquement utilisées en cas de défaillance +du nœud.

+
+
+

Avec les avis AMQP sur un serveur de fichiers standard

+

Plusieurs protocoles et piles logicielles robustes et matures sont disponibles pour de nombreux +protocoles de transport de données : FTP, HTTP, HTTP(S), SFTP. Un serveur de fichiers, en tant +que moyen de transport de données est un problème résolu avec de nombreuses solutions disponibles +dans l’ensemble de l’industrie. Contrairement au transport de données, pub/sub est une zone +atomisée avec une myriade de solutions de niche, et aucune solution clairement dominante.

+

Le protocole Advanced Message Queueing Protocol est un standard ouvert, mis au point par les +institutions financières, adopté plus tard par de nombreux éditeurs de logiciels, grands et +petits. AMQP remplace les systèmes propriétaires de transmission de messages tels que IBM/MQ, +Tibco, Oracle SOA et/ou Tuxedo. RabbitMQ est une implémentation AMQP de premier plan, avec des +déploiements dans de nombreux domaines différents :

+ +

Rabbitmq fournit actuellement une implémentation de passage de messages mature et fiable, mais +il existe de nombreuses autres implémentations open source et propriétaires si cela devait changer. +Les brokers AMQP sont des serveurs qui fournissent des services de publication de messages et +d’abonnement, avec une prise en charge robuste des files d’attente et des échanges hiérarchiques +basés sur des sujets.

+

Chaque serveur exécute un courtier pour annoncer sa propre contribution, et ils s’abonnent aux messages +de notification des autres. Les publicités sont transitives, en ce sens que chaque nœud peut annoncer +tout ce qu’il a téléchargé à partir de n’importe quel autre nœud afin que les autres nœuds qui lui sont +connectés puissent les consommer. Cela met en œuvre un réseau maillé entre tous les CN/DPCC/GISC.

+

Une couche de notification AMQP ajoutée au réseau de transfert de fichiers existant :

+
    +
  • améliorer la sécurité car les utilisateurs ne téléchargent jamais, n’ont jamais à écrire sur un serveur distant. +(tous les transferts peuvent être effectués par des abonnements initiés par le client, aucune écriture sur des serveurs homologues n’est nécessaire).

  • +
  • permettre des échanges ad hoc entre les membres à travers le RMDCN sans avoir à faire appel à des tiers.

  • +
  • peut fonctionner uniquement avec des échanges anonymes, pour éliminer complètement le besoin d’authentification. +une authentification explicite supplémentaire est disponible si vous le souhaitez.

  • +
  • fournir un mécanisme similaire pour supplanter le GTS traditionnel +(performances similaires au GTS existant, pas d’énormes pénalités d’efficacité).

  • +
  • contrairement aux GTS actuels : pas de limite de taille de produit, peut fonctionner avec +n’importe quel format. l’insertion de données consiste à choisir une hiérarchie de fichiers (nom)

  • +
  • transparent (peut voir quelles données se trouvent sur n’importe quel nœud, sans nécessiter +d’échanges humains). (Les personnes autorisées peuvent parcourir une arborescence FTP/SFTP/HTTP).

  • +
  • activer/supporter les topologies d’interconnexion arbitraires entre NC/DCPC/GISC (les cycles +dans le graphique sont une caractéristique, pas un problème, avec les empreintes digitales).

  • +
  • Raccourcir le temps de propagation des données de NMC vers d’autres centres de données à +travers le monde (moins de sauts entre les nœuds que dans GTS, charge plus répartie entre les nœuds).

  • +
  • relativement simple à configurer pour des topologies arbitraires (configuration des +abonnements, peu besoin de configurer la publication).

  • +
  • contourner les défaillances de nœuds au sein du réseau en temps réel sans intervention +humaine (le routage est implicite et dynamique, plutôt qu’explicite et statique).

  • +
+
+
+

Et une arborescence de répertoires convenue

+

Similaire au choix des indices dans les bases de données, l’efficacité de l’échange dans +les serveurs de fichiers dépend essentiellement de l’équilibre de la hiérarchie en termes +de nombre de fichiers par répertoire. Une hiérarchie qui garantit que moins de 10 000 +fichiers par répertoire fonctionne bien.

+

Exemple de serveur: http://dd.weather.gc.ca

+

L’arborescence sur dd.weather.gc.ca est le déploiement d’origine de ce type de service. +À titre d’exemple du type de service (bien que les détails soient différents pour WMO), +l’ordre des répertoires est le suivant:

+
+
+

Il existe une url de base initialement fixe: +http://dd.weather.gc.ca/bulletins/alphanumeric/, +Ensuite, les sous-répertoires commencent: date (YYYYMMDD), WMO-TT, CCCC, GG, then +les bulletins, dont le contenu est:

+
Parent Directory                                               -
+[   ] SACN31_CWAO_111200__CYBG_42669            11-Feb-2018 12:01   98
+[   ] SACN31_CWAO_111200__CYQQ_42782            11-Feb-2018 12:02  106
+[   ] SACN31_CWAO_111200__CYTR_43071            11-Feb-2018 12:03   98
+[   ] SACN31_CWAO_111200__CYYR_42939            11-Feb-2018 12:01   81
+[   ] SACN31_CWAO_111200__CYZX_43200            11-Feb-2018 12:02   89
+[   ] SACN43_CWAO_111200__CWHN_43304            11-Feb-2018 12:12   85
+  .
+  .
+  .
+
+
+
+

Note

+

Ces fichiers ne suivent pas les conventions d’appellation de WMO, mais illustrent des questions +intéressantes. Dans les bulletins de WMO, il ne faut publier qu’un seul bulletin avec l’AHL: +SACN31 CWAO 111200 Pour être distribuées à WMO, ces observations individuelles sont collectées +et même envoyées en tant que SACN31 CWAO 111200, mais cela signifie retarder la transmission +des rapports CYBG, BYQQ, CYTR en attendant la fin de l’intervalle de collecte ( 12:05? ) avant +d’émettre le bulletin collecté. Ce datamart, à usage national, offre des observations individuelles +au fur et à mesure qu’elles arrivent en temps réel, en ajoutant l’identifiant de la station ainsi +qu’un entier aléatoire au nom du fichier, pour assurer l’unicité.

+

Ceci est une illustration d’un premier prototype qui reste en service. L’arbre réel à utiliser +par WMO serait probablement différent.

+
+

Mis à part le contenu de l’arbre, le reste de la fonctionnalité proposée serait tel que décrit. +On peut facilement s’abonner au datamart pour répliquer l’arbre entier au fur et à mesure que les +données lui sont livrées. Bien que l’application ne l’exige pas, la normalisation de l’arbre à +échanger par les membres de WMO simplifiera considérablement l’échange de données. Très probablement, +un arbre approprié à normaliser pour les utilisations de WMO serait quelque chose comme:

+
20180210/          -- YYYYMMDD
+     CWAO/         -- CCCC, origin, or 'Source' in Sarracenia.
+          00/      -- GG (hour)
+             SA/   -- TT
+                  follow the naming convention from WMO-386...
+
+
+

Si nous avons un ordre par jour (YYYYMMDD), puis ORIGIN (CCCC?), puis types de données, et +peut-être heure, alors les arbres qui en résultent seraient presque équilibrés de manière +optimale et assureraient une récupération rapide. La configuration optimale est également +clairement visible puisque cet arbre peut être consulté par n’importe quel membre de WMO +simplement en naviguant sur le site Web, contrairement aux bases de données, où les schémas +d’indexation sont complètement cachés.

+

Les nœuds copient les arbres les uns des autres textuellement, de sorte que l’arbre est +l’emplacement relatif sur n’importe quel nœud. Les pointeurs de métadonnées WIS vers les +données réelles peuvent ensuite être modifiés par programmation pour faire référence au +nœud le plus proche pour les données, ou un algorithme de recherche simple peut être +implémenté pour demander à d’autres nœuds, sans avoir besoin de recourir à une requête +de recherche coûteuse.

+

Dans AMQP, les abonnements peuvent être organisés en rubriques hiérarchiques, avec +le caractère de point (‘.’) comme séparateur. Pour cette application, l’arborescence +de répertoires, avec ‘/’ ou ‘’ comme séparateur remplacé par le séparateur AMQP est +traduite en une arborescence de rubriques AMQP. AMQP a un caractère générique +rudimentaire, en ce sens qu’il utilise l’astérisque (‘*’) pour désigner n’importe quel +sujet, et le symbole de hachage (‘#’) est utilisé pour correspondre au reste de +l’arborescence des sujets. Voici des exemples de la façon dont on pourrait s’abonner +sélectivement sur un nœud:

+
v02.post.#            -- all products from all Origins (CCCC)'s on a node.
+v02.post.*.CWAO.#     -- all products from CWAO (Canada) on a node
+v02.post.*.CWAO.WV.#  -- all volcanic ash warnings (in CAP?) from Canada RSMC/VAAC.
+
+
+
+

Note

+

Le préfixe de sujet (début de l’arborescence des sujets) est constant pour cette discussion. Explication:

+

v02 - identifie la version du protocole. Si le schéma change à l’avenir, cela permet à un serveur de servir +plusieurs versions à la fois. Cela a déjà été utilisé pour migrer progressivement de exp, à v00, à v02.

+

post - identifie le format du message. Autres formats : rapport et pouls. décrit ailleurs.

+
+

Une fois ce premier niveau de filtrage effectué côté serveur, Sarracenia implémente un niveau +supplémentaire de filtrage côté client en utilisant +Expressions régulières pour exclure ou +inclure des sous-ensembles spécifiques.

+

Pour échanger des types de données connus, il suffit de définir les répertoires qui seront +injectés dans le réseau. Les nations peuvent adopter leurs propres politiques sur la quantité +de données à acquérir auprès d’autres pays et sur la quantité à offrir pour la retransmission. +Pour proposer un nouveau format de données ou une nouvelle convention, un pays télécharge vers +un nouveau répertoire sur son nœud. Les autres pays qui souhaitent participer à l’évaluation +du format proposé peuvent s’abonner au flux à partir de ce nœud. D’autres pays qui commencent +à produire le nouveau format ajoutent également le répertoire à leur hiérarchie. Aucune +coordination avec les parties intervenantes n’est nécessaire.

+

Si deux pays décident d’échanger des produits météorologiques numériques (NWP), ou des +données RADAR, en plus des types de base échangés aujourd’hui, ils se mettent simplement +d’accord sur les répertoires où ces données doivent être placées et s’abonnent aux flux +de nœuds de l’autre.

+
+
+

Configurations homologues simples/évolutives pour les nations

+../../../_images/WMO_mesh.png +

Supposons un maillage de nœuds nationaux avec une connectivité arbitraire entre eux. +Les nœuds téléchargent à partir du premier voisin pour annoncer des données, les +transferts suivent la vitesse de téléchargement à partir de chaque nœud. Si un nœud +ralentit, les voisins recevront des messages de notification d’autres nœuds qui +présentent de nouvelles données plus tôt. Le réseau doit donc équilibrer naturellement +la bande passante.

+

Les centres nationaux peuvent disposer d’autant ou aussi peu d’informations localement +qu’ils le jugent bon. L’ensemble minimum ne concerne que les données propres au pays. +La redondance est assurée par de nombreux pays qui s’intéressent aux ensembles de données +d’autres pays. Si un CN a un problème, les données peuvent probablement être obtenues à +partir d’un autre nœud. Les NC peuvent également se comporter égoïstement s’ils le souhaitent, +en téléchargeant des données vers des services internes sans les rendre disponibles pour +être retransmises à leurs pairs. Les nœuds super nationaux peuvent être provisionnés dans +le cloud, à des fins de gestion ou d’optimisation des ressources. Ces nœuds faciliteront +la communication en ajoutant de la redondance aux routes entre les nations. Avec +l’interconnexion de type maillé, en cas de défaillance d’un nœud provisionné dans +le cloud, il est probable que les connexions entre pays compensent automatiquement +les défaillances individuelles.

+

Il y a également peu ou pas d’exigence pour le GISC supranational dans ce modèle. +Les nœuds peuvent être établis avec une capacité plus ou moins grande et ils peuvent +décider eux-mêmes quels ensembles de données valent la peine d’être copiés localement. +Comme les abonnements sont sous contrôle local, le besoin de coordination lors de +l’obtention de nouveaux ensembles de données est fortement réduit. Il n’est pas non +plus nécessaire qu’un nœud corresponde uniquement à un centre national. Il existe de +nombreuses situations où des membres disposant de plus de ressources aident d’autres +membres, et cette pratique pourrait se poursuivre en demandant aux nœuds d’insérer +des données dans le SMT au nom d’autres pays. La redondance pour le téléchargement +pourrait également être réalisée en téléchargeant vers plusieurs sites initiaux.

+

S’il existe des nœuds qui, pour une raison quelconque, ne souhaitent pas communiquer +directement, ils ne s’abonnent pas directement aux messages de notification des autres. +Chacun peut acquérir des données en toute sécurité grâce à des intermédiaires avec +lesquels chacun est à l’aise. Tant qu’il y a un seul chemin qui mène entre les deux +nœuds, les données arriveront finalement à chaque nœud. Aucune action explicite des +intermédiaires n’est nécessaire pour assurer cet échange, car le réseau normal contournera +simplement le bord manquant dans le graphique.

+

En cas de mauvaise conduite, d’autres nœuds peuvent cesser de s’abonner à certaines +données sur un nœud ou cesser d’importer des données à partir d’un nœud qui injecte +des données corrompues ou indésirables. Il peut arriver que certains pays disposent +d’une très bonne bande passante et de très bonnes performances de serveur. La motivation +serait d’obtenir les données le plus rapidement pour eux-mêmes, mais en mettant en œuvre +cet excellent service, il attire plus de demande de données du reste du monde. Si un +nœud estime qu’il supporte trop la charge globale de l’échange de trafic, il existe +de nombreux moyens simples d’encourager l’utilisation d’autres nœuds: non-publication, +publication différée, mise en forme du trafic, etc. Toutes ces techniques sont des +applications simples de la technologie industrielle, sans qu’il soit nécessaire de +recourir à des normes spécifiques de WMO.

+
+
+

Utilisation d’une pile (stack)de référence ouverte

+../../../_images/A2B_oldtech.png +

Un exemple de configuration de nœud maillé national (Linux/UNIX très probablement) +comprendrait les éléments suivants:

+
    +
  • application d’abonnement pour publier des données nationales sur le courtier local pour les autres ( Sarracenia )

  • +
  • L’application d’abonnement se connecte aux courtiers d’autres nœuds ( Sarracenia ) et la publie +sur le courtier local pour qu’elle soit téléchargée par les clients.

  • +
  • Notifications de service de courtier AMQP ( Rabbitmq )

  • +
  • Serveur http pour servir les téléchargements (plain old apache-httpd, with indexes).

  • +
  • serveur ssh pour la gestion et les téléchargements locaux par les entités nationales (OpenSSH)

  • +
+

La pile se compose de logiciels entièrement libres, et d’autres implémentations peuvent +être remplacées. Le seul élément rare de la pile est Sarracenia, qui n’a jusqu’à présent +été utilisé qu’avec le courtier RabbitMQ. Bien que Sarracenia +( https://metpx.github.io/sarracenia/fr ) ait été inspiré par le +problème d’échange de données GISC, il n’est en aucun cas spécialisé dans les prévisions +météorologiques, et le plan est de l’offrir à d’autres pour dans d’autres domaines pour +soutenir les transferts de données à grande vitesse.

+

L’implémentation de référence de Sarracenia est inférieure à 20 000 lignes dans Python 3. +Les clients ont contribué à des implémentations partielles open source en javascript, C# +et Go, et en ont implémenté une autre en C pour prendre en charge le cas d’utilisation +du calcul haute performance. Le format du message est +publié et manifestement indépendant du langage du programme.

+

Cette pile peut être déployée sur de très petites configurations, comme un Raspberry +Pi ou un serveur virtuel hébergé très peu coûteux. Les performances évolueront en +fonction des ressources disponibles. La principale pompe de données météorologiques +internes au Canada est mise en œuvre sur 10 serveurs physiques (probablement trop, +car ils sont tous légèrement chargés).

+
+
+

Maturité

+

Pour le Canada, il ne s’agit pas d’un projet expérimental à côté d’autres initiatives. +Sarracenia est au centre d’une décennie de travail et le cœur du pompage de données +actuellement opérationnel. Il est utilisé en mode opérationnel pour transférer des +dizaines de téraoctets par jour dans une grande variété de cas d’utilisation différents.

+

Prochaines étapes:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Date

Milestone

2008 pour MetPX/Sundew +sender et receiver rajoutés.

Premières expériences

2010 National Unified RADAR +Traitement des extrants

Expérience d’amélioration +de la fiabilité par le principe +de l’algorithme premier arrivé +premier servi +pour les extrants de NURP. +Plusieurs appels par mois ->0

2010 WMO CBS-Ext 10 Windhoek

WMO discussions initiales +Modèle maillé conçu pour les +GISC (Le travail était encore +expérimental)

2013 dd.weather.gc.ca +à présenter… +dd_subscribe (client initial)

premier déploiement publique

+

certains utilisent logiciels +fournir pas le client, d’autres +ont écrit leur propre. Bcp +d’implémentations maintenant.

+

Un client Allemand pour Grib +Trafic de téléchargement de +sortie économise 30 G/jour de +bande passante

+

2013 MetPX/Sarracenia +commence

Décision de baser Next Gen. +WMO pompe de données sur AMQP

2015 à aujourd’hui +(Variété de clients)

Les clients de DataMart ont +utilisé des clients fournis +et/ou construit les leurs.

2015 Sarracenia en 10 Minutes +(pour donner aux analystes +un aperçu )

Vision pour Sarracenia

2015 NWS WMO socket remplacé

NWS n’offre que l’arborescence +SFTP. La consommation d’arbres +via le sondage Sarracenia sur +le courtier distribue avec +40 processus de transfert sur +huit nœuds de transfert.

2015 PanAmerican Games

Fed Ninjo sur internet +via l’abonnement Sarracenia.

2016 déploiement Ninjo

Le bureau central alimente tous +les serveurs ninjo via WAN. +L’utilisation de la mise en +cache/des proxys réduit le +trafic WAN après le déploiement

2016 Weather Apps.

Consistent,basculement national +pour BULLPREP, Scribe, etc… +(Applications clés des +prévisionnistes). +Mettre en œuvre un +Drive partagé pour fournir +une vue commune de l’état des +applications dans 9 bureaux

2016 Redundant RADAR Acq.

Radars en bande C reliés à deux +endroits, premier arrivé, +premier servi pour les entrées +d’URP.

2016-2017 HPC Mirroring.

+

Gen 1: GPFS Policy

+

mise en miroir entre les +clusters HPC +12x plus rapide que rsync +(Décalage de 5 à 40 minutes)

2018 US FAA radar feed. +( essai en cours )

La FAA utilise le forfait +sarracenia pour s’abonner aux +balayages de volume RADAR +canadiens (bandes C et S)

2017-2019 HPC Mirroring.

+

Gen 2: shim library

+

mise en miroir entre les +clusters HPC +72x plus rapide que rsync +(Moins de 5 minutes de +décalage)

+

Pour plus d”informations: Deployments as of January 2018

+
+
+

Apatridie/Crawlable

+

Comme les serveurs de fichiers en question présentent des fichiers statiques, +les transactions avec la pile Pile préférée sont complètement sans état. +Les moteurs de recherche explorent facilement ces arbres et, compte tenu +de la masse critique, on pourrait s’arranger avec les moteurs de recherche +pour leur fournir un flux continu de notifications afin que l’index d’un +utilisateur donné puisse être mis à jour en temps réel. Ces caractéristiques +ne nécessitent aucun travail ni coût car elles sont inhérentes aux technologies +proposées.

+
+
+

Programmabilité/interopérabilité

+

Une nouvelle application pour traiter sr3_post messages peut être réimplémentée s’il y a un désir +de le faire, car en plus de la documentation complète, le code source d’une poignée d’implémentations +(Python, C, Go, node.js), est facilement accessible au public. L’implémentation python dispose +d’une interface de plug-in étendue disponible pour personnaliser le traitement de différentes +manières, par exemple pour ajouter des protocoles de transfert de fichiers et effectuer un pré +ou un post-traitement avant l’envoi ou après la réception des produits. L’interopérabilité avec +Apache NiFi a été démontrée par certains clients, mais ils ont refusé de publier le travail.

+
+
+

Priorités

+

FIXME: Faire une image, avec des files d’attente séparées pour des types de données distincts?

+

Dans le GTS de WMO, les données sont séparées en données alphanumériques et binaires et, au sein +d’un flux unique, un mécanisme de priorité était disponible, dont la mise en œuvre n’était pas +vraiment spécifiée. L’objectif est essentiellement que les données les plus critiques soient +transférées avant les autres informations mises en file d’attente. Lorsque trop de données sont +envoyées sur un canal hautement prioritaire, certaines implémentations peuvent finir par affamer +les données de priorité inférieure, ce qui n’est pas toujours souhaitable.

+

La priorité a pour effet d’établir une file d’attente distincte pour les produits à chaque niveau +de priorité. Dans cette proposition, plutôt que d’avoir des priorités explicites dans une seule +file d’attente, on utilise simplement des files d’attente distinctes pour différents ensembles +de données. Comme les données à haute priorité doivent être plus petites ou peu fréquentes que +les autres données afin d’être transférées et traitées rapidement, les files d’attente sur ces +files d’attente à haute priorité seront naturellement plus courtes que celles contenant d’autres +données. Étant donné que le mécanisme est général, les détails de la mise en œuvre ne nécessitent +pas de normalisation rigide, mais peuvent être mis en œuvre par chaque NMC pour répondre à ses +besoins.

+

En pratique, les déploiements canadiens permettent de transférer des avertissements en moins +d’une seconde en utilisant uniquement des files d’attente distinctes pour les types de données +hautement prioritaires, comme les avertissements et RADAR.

+
+

Contenu en ligne dans les messages

+

Il est tentant d’intégrer (ou d’inclure) des données dans les messages AMQP pour les petits +types de données. L’espoir est que nous évitions une initiation de connexion et un aller-retour +supplémentaire. L’exemple typique serait les avertissements météorologiques. Pouvons-nous améliorer +la rapidité d’exécution en incluant les avertissements météorologiques dans le flux de données AMQP +plutôt que de les télécharger séparément?

+

Chaque fois que les courtiers de messagerie sont comparés, les repères d’essai incluent toujours +des notes sur la taille des messages, et les performances des systèmes en termes de messages +par seconde sont invariablement plus élevées avec des messages plus courts. Il est assez évident +que chaque système impose une taille maximale de message, que les messages sont normalement +conservés en mémoire et que la taille maximale des messages que chaque homologue devrait prendre +en charge devrait être spécifiée afin d’assurer l’interopérabilité. Il n’est pas clair que, +bien que les messages individuels puissent bénéficier de l’inlining, il n’y a pas un coût +dans la performance globale de la pompe de données qui l’emporte.

+

Compte tenu de ce qui précède, il existe trois approches possibles pour limiter la taille des +messages : troncation, segmentation et seuils.

+

Troncature : WMO limite actuellement les messages à moins de 500 000 octets. Cela empêche +le transfert de nombreux types de données modernes (balayages de volume radar, imagerie +satellite, vidéo, etc.) Les gens suggéreront que seuls les avertissements seraient +envoyés en ligne. Le format actuel des messages d’avertissement est Common Alerting +Protocol, un format XML très flexible qui permet d’incorporer des médias tels que +l’intégration. Il n’y a pas de taille maximale de message pour les messages CAP, et +on ne pouvait donc pas garantir que tous les messages CAP correspondraient à une +limite de troncature que nous imposerions.

+

Segmentation: Pour éviter la troncature on pourrait plutôt implémenter l’envoi de +produits segmentés en plusieurs messages. Il existe une longue et troublée histoire +de segmentation des messages dans le GTS, dans la mesure où la segmentation a été +purgée de GTS lorsque la limite de taille des messages a été portée à 500 000 octets. +Des protocoles comme FTP, HTTP, TCP font déjà ce travail. L’ajout d’une autre couche +de logiciel qui reproduit ce qui est fait à des niveaux inférieurs est peu susceptible +d’être utile. Il y a probablement très peu d’appétit pour définir la segmentation des +messages à superposer sur la transmission de messages AMQP.

+

Remarque : Le protocole Sarracenia implémente la segmentation des fichiers (partitionnement) +sur les protocoles de transfert de données, en vue de l’utiliser pour des segments de taille +beaucoup plus grande, de l’ordre de 50 mégaoctets par segment. Le but est de chevaucher le +transfert et le traitement des fichiers (permettant au début des fichiers de plusieurs gigaoctets +de commencer avant qu’ils ne soient complètement livrés.)

+

Seuil : Il est probable que le seuillage soit la seule stratégie raisonnable d’intégration de données. +Si la taille de la référence est supérieure à X octets, utilisez un autre mécanisme de transport. +Cela garantit que seules les données inférieures à X octets seront insérées. Il fournit une +taille de message pour tous les courtiers à optimiser. D’autre part, cela signifie qu’il faut +toujours implémenter deux méthodes de transfert, car on ne peut pas garantir que toutes les +données rentreront dans le flux AMQP, on doit prévoir le chemin de données alternatif à utiliser +lorsque le seuil est dépassé.

+

Choisir X n’est pas évident. Les types de données sont en croissance, les formats futurs ou +actuels tels que: AvXML, CAP, ODIM, GIF étant d’un ordre de grandeur ou plus grand que les +codes alphanumériques traditionnels (TAC.) Choisir un X suffisant pour de tels types de données +est susceptible d’être beaucoup plus difficile pour les courtiers, et aucune valeur que +nous pouvons choisir ne prendra tous les avertissements.

+

Comme à l’avenir, l’intention est d’utiliser cette méthode avec l’imagerie satellitaire, +les données RADAR et les grands ensembles de données GRIB, on soupçonne qu’une grande +quantité de données hautement prioritaires dépassera toute valeur raisonnable de X. +Si nous n’utilisons pas de files d’attente distinctes pour les données de haute priorité, +une pression à la baisse sur X provient du fait que nous évitons que les messages +volumineux retardent excessivement l’envoi d’un message de priorité plus élevée.

+

Pour garantir les performances de transfert d’avertissement, il faudrait également le +garantir pour les avertissements volumineux, ce qui est assez bien accompli en utilisant +uniquement des files d’attente séparées.

+

Il n’est pas clair que la valeur de X que nous choisissons aujourd’hui aura un sens dans dix ans. +Un X plus élevé utilisera plus de mémoire dans les courtiers et réduira les performances absolues +de transmission des messages. Les courtiers sont les éléments les plus critiques de ces pompes de +données, et minimiser la complexité est un avantage.

+

Une autre considération est le temps économisé. L’application Sarracenia maintient les connexions, +il ne coûte donc pas d’établissement de connexion pour transférer un fichier. On exploite généralement +un certain nombre de téléchargeurs parallèles partageant une file d’attente pour obtenir un parallélisme. +Avec l’acquisition canadienne de données auprès du NWS, il y a 40 processus qui extraient des données +simultanément, et il y a très peu de files d’attente. Il peut être plus important d’initier les transferts +plus rapidement plutôt que d’accélérer les flux individuels.

+

Une dernière considération est la séparation des chemins de contrôle et de données. Le point de +terminaison AMQP peut ne pas être le point de terminaison de transfert de données. Dans les +déploiements à haute performance au Canada, il existe des courtiers qui sont des serveurs distincts +des Data Movers. Le seul but du courtier est de répartir la charge entre les nœuds de Data Mover, +où l’idéal est que cette distribution soit aussi uniforme que possible. Dans cette conception, il +est peu logique de télécharger des messages aux courtiers et peut en fait retarder le transfert en +ajoutant un saut (un transfert supplémentaire vers un nœud de transfert de données avant le transfert). +Les principaux déploiements de pompes à données canadiennes transfèrent plusieurs centaines de +messages par seconde, et nous ne sommes pas optimistes quant à l’ajout de charges utiles à ce +mélange.

+

En résumé : sans inlining, les déploiements actuels atteignent déjà un transfert inférieur à la +seconde en utilisant uniquement des files d’attente distinctes. Si nous voulons éviter de réintroduire +la segmentation et le réassemblage, l’inlining n’est probablement pratique qu’avec une taille de +charge utile maximale fixe. Déterminer un seuil raisonnable n’est pas évident, et une fois le seuil +établi, il faut s’assurer que le trafic hautement prioritaire au-dessus du seuil est également +transféré rapidement, évitant ainsi la motivation de l’inlining. Les déploiements hautes performances +comportent souvent des brokers complètement séparés du chemin de transfert de données, où le broker +dispose d’une fonction de distribution de charge et où des nœuds de transfert de données plus simples +effectuent le transport. Un seuil ajoute de la complexité dans l’application, ajoute de la charge +sur le courtier, qui est l’élément le plus complexe à mettre à l’échelle, et peut donc ralentir +le système global. Il n’est pas clair que les avantages en vaudront la peine par rapport aux frais +généraux dans les charges réelles.

+
+
+
+

Mise en garde: Solution à ce problème, pas tous les problèmes

+

Les courtiers AMQP fonctionnent bien, avec les implémentations sarracenia du service météorologique +canadien, ils sont utilisés pour des dizaines de millions de transferts de fichiers pour un total +de 30 téraoctets par jour. L’adoption est encore limitée car elle est plus compliquée à comprendre +et à utiliser que par exemple, rsync. Il existe des concepts supplémentaires (courtiers, bourses, +files d’attente) qui constituent une barrière technique à l’entrée.

+

En outre, bien que les courtiers fonctionnent bien pour les volumes modérés utilisés (centaines +de messages par seconde et par serveur), il n’est pas du tout clair si cela convient à une +technologie Internet plus large (c’est-à-dire pour le problème 10K). Pour l’instant, ce type +de flux est destiné à des dizaines ou des centaines de pairs sophistiqués ayant un besoin +démontré de services de fichiers en temps réel. La démonstration de la mise à l’échelle du +déploiement à l’échelle d’Internet est un travail futur.

+

Il existe de nombreuses autres solutions robustes pour le problème du transfert de fichiers. +AMQP est mieux utilisé uniquement pour transférer des notifications (métadonnées de transfert +en temps réel), qui peuvent être très grandes en nombre mais peu en volume, et non les données +elles-mêmes.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/messages_v01.html b/fr/Explication/Histoire/messages_v01.html new file mode 100644 index 000000000..162e1f0c2 --- /dev/null +++ b/fr/Explication/Histoire/messages_v01.html @@ -0,0 +1,367 @@ + + + + + + + Message v01 Format — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Message v01 Format

+

Status: Approved-Draft1-20150805

+

Description du protocole / format du message.

+

Ce dossier documente les conclusions/propositions finales, les raisonnements/débats vont ailleurs.

+

Les messages publiés incluent un “sujet” et un “corps”.

+

La sujet du message se décompose comme suit

+
<version>.<type>.[Varie selon la version].<dir>.<dir>.<dir>...
+
+<version>:
+        exp -- Version initiale, obsolète (non traitée dans ce document)
+        v00 -- utilisé pour NURP & PAN-AM en 2013-2014. (non couvert dans le présent document)
+        v01 -- version 2015.
+
+<type>:
+        adm  - Modifier les paramètres
+                ´admin´, ´config´, etc...
+
+        log  - rendre compte de l’état des opérations.
+
+        notify - ´post´ mais dans les versions exp et v00. (Non couvert ici.)
+
+        post - annoncer ou notifier qu’un nouveau bloc de produits est disponible.
+                Chaînes possibles : post,ann(ounce), not(ify)
+
+<source>:
+
+
+

Le reste de ce document suppose la version 1 (sujet v01) :

+

sujet: <version>.<type>.<src>(.<dir>.)*.<nom de fichier> +contenu: 1ère ligne: +<horodatage> <taille de bloc en octets> <taille du fichier en blocs> <bloc#> <reste> <flags> <md5sum> <flowid> <srcpath> <relpath>

+

se décompose en:

+
<horodatage>: date
+      YYYYMMDDHHMMSS.<decimal>
+
+<taille de bloc en octets>: bsz
+      Nombre d’octets dans un bloc.
+      Les sommes de contrôle sont calculées par bloc, donc un post
+
+<taille du fichier en blocs>: fzb
+      Nombre total entier de blocs dans le fichier
+      FIXME: (y compris le dernier bloc ou pas?)
+      si ce paramètre est défini sur 1.
+
+<block#>: bno
+      0 origine, le numéro de bloc couvert par cette publication.
+
+<remainder>: brem
+      normalement 0, sur le dernier bloc, il reste des blocs dans le fichier
+      à transférer.
+
+      -- si (fzb=1 and brem=0)
+             alors bsz=fsz en octets.
+             -- fichiers entiers remplacés.
+             -- C’est la même chose que le mode --whole-file de rsync.
+
+<flags>:Une liste de lettres d’option séparées par des virgules, certaines avec des arguments après '='.
+
+      Paramètre de somme de contrôle contenu dans le champ 'flags', mais n’est pas tout.
+  D’autres lettres / chiffres pourraient être là pour désigner d’autres choses.
+      '=' sépare les indicateurs des arguments.
+
+
+      donne lieu à l’entrée 'flags':
+
+      0 - Aucune somme de contrôle (copie inconditionnelle.)
+      d - Additionner l’intégralité des données
+      n - checksum le nom du fichier
+      c=<script> - somme de contrôle avec un script, nommée <script>
+
+      <script> doit être "enregistré" dans le réseau de commutateurs.
+                      enregistré signifie que tous les abonnés en aval
+                      peuvent obtenir le script pour valider la somme de contrôle.
+                      Il faut un mécanisme de récupération.
+
+      Autres valeurs d’indicateur possibles :
+
+      u - unlinked... pour les fichiers qui ont été supprimés ? 'r'?
+
+      Stratégie de segment de fichier :
+              i - inplace (ne pas créer de fichiers temporaires, juste chercher
+                      dans le fichier.)
+                  peut entraîner la création d’un fichier .ddsig?
+              p - part files.  Utilisez .part fichiers, suffixe fixe.
+                  Je ne sais pas lequel sera par défaut.
+         - La stratégie de segment de fichier peut être remplacée par le client. Juste une suggestion.
+         - analogues aux options rsync : --inplace, --partial,
+
+<flowid>
+      Une balise arbitraire utilisée pour le suivi des données via le réseau.
+
+Les deux voies sont subtilement liées.  Ni l’un ni l’autre ne peuvent être interprétés seuls.
+Il faut considérer les deux composantes du chemin.
+------
+
+  Que se passe-t-il s’il y a des espaces dans le nom du fichier ?
+      Il est codé en URL, donc un espace doit se transformer en : %20
+
+
+
+
+

<srcpath> – URL de base utilisée pour récupérer les données.

+
+

options: URL Complet:

+

sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif

+

dans le cas où l’URL ne se termine pas par un séparateur de chemin (‘/’), +le chemin src est considéré comme la source complète du fichier à récupérer.

+

URL Statique:

+

sftp://afsiext@cmcdataserver/

+

Si l’URL se termine par un séparateur de chemin (‘/’), alors l’URL src est +considéré comme un préfixe pour la partie variable de l’URL de récupération.

+
+

<relpath> – Chemin d’accès relatif du répertoire actif dans lequel placer le fichier.

+
+
+

Deux cas basés sur la fin étant un séparateur de chemin ou non.

+

cas 1: NURP/GIF/

+

basé sur le répertoire de travail actuel du client de téléchargement, +créer un sous-répertoire appelé URP, et à l’intérieur de celui-ci, un sous-répertoire +appelé GIF sera créé. Le nom du fichier sera tiré du +srcpath.

+
+
+
Si le srcpath se termine par pathsep, alors le relpath ici sera

concaténé au srcpath, formant l’URL de récupération complète.

+

cas 2: NRP/GIF/mine.gif

+

Si le srcpath se termine par pathsep, le relpath sera concaténé +à srcpath pour former l’URL de récupération complète.

+
+
si le chemin src ne se termine pas par pathsep, l’URL src est prise

comme terminé, et le fichier est renommé lors du téléchargement conformément à la +spécification (dans ce cas, la mienne.gif)

+
+
+
+
+
+
FIXME: Vérifiez les points suivants :

fsz = taille du fichier en octeta = ( bsz * (fsb-1) ) + brem ?

+
+
+

exemple 1:

+
v01.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif
+201506011357.345 457 1 0 0 d <md5sum> exp13 sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif NRDPS/GIF/
+
+      v01 - version du protocol
+      post - indique le type de message
+
+      La version et le type déterminent ensemble le format des sujets suivantes et le corps du message.
+
+      ec_cmc - le compte utilisé pour émettre le post (unique dans un réseau).
+
+        -- blocksize est 457  (== taille du fichier)
+        -- Le nombre de blocs est de 1
+        -- le reste est égal à 0.
+        -- le numéro de bloc est 0.
+        -- d - La somme de contrôle a été calculée sur le corps.
+        -- flow est un argument après le chemin relatif.
+        -- URL source complète spécifiée (ne se termine pas par '/')
+        -- chemin relatif spécifié pour
+
+      pull de:
+              sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif
+
+      Chemin de téléchargement relatif complet :
+              NRDPS/GIF/NRDPS_HiRes_000.gif
+
+              -- Prend le nom de fichier de srcpath.
+              -- peut être modifié par un processus de validation.
+
+
+

exemple 2:

+
v01.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif
+201506011357.345 457 1 0 0 d <md5sum> exp13 http://afsiext@cmcdataserver/data/  NRDPS/GIF/NRDPS_HiRes_000.gif
+
+Dana ce cas,
+      pull de:
+              http://afsiext@cmcdataserver/data/NRPDS/GIF/NRDPS_HiRes_000.gif
+              -- srcpath se termine par '/', donc concaténé, prend le fichier de l’URL relative.
+              -- véritable 'miroir'
+
+      Chemin de téléchargement relatif complet :
+              NRDPS/GIF/NRDPS_HiRes_000.gif
+
+  -- peut être modifié par un processus de validation.
+
+
+
+

Journaux des messages

+

Le message du journal contient :

+

n’est émis qu’une fois le traitement terminé, pour indiquer un état final.

+

Le sujet correspond au message de notification sauf…

+

v01.log. <source>. <consumer>……

+

version est la version du protocole, doit incrémenter en synchronisation avec notifier.

+

Le début est comme par post… Il suffit d’ajouter des champs après:

+

<date> blksz blckcnt rest blocknum flags <flow> baseurl relativeurl <status> <host> <client> <duration>

+
+
+

Messages du CFG

+

juste un espace réservé.

+

vraiment pas encore fini. La pensée est en configuration.txt

+

v01.cfg

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/messages_v02.html b/fr/Explication/Histoire/messages_v02.html new file mode 100644 index 000000000..f33c5d4aa --- /dev/null +++ b/fr/Explication/Histoire/messages_v02.html @@ -0,0 +1,369 @@ + + + + + + + Description du protocole / format du message v02 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +

Status: Approved-Draft2-20150825

+
+

Description du protocole / format du message v02

+

Ce fichier a été utilisé lors de la phase de conception, mais après la mise en œuvre, +elle est remplacée par la page de manuel sr_post(7).

+

Ce dossier documente les conclusions/propositions finales, les raisonnements/débats +va ailleurs.

+

Les messages affichés comprennent quatre parties:

+
    +
  • Sujet: .<version><type>. <src>(.<dir>.) *. <nom de fichier>

  • +
  • en-têtes : série de paires clé-valeur, selon les spécifications AMQP.

  • +
  • 1ère ligne (champs séparés par des espaces) : <horodatage> <srcURL> <relURL><newline>

  • +
  • Reste du corps.

  • +
+

La sujet du message se décompose comme suit

+
<version>.<type>.[Varie selon la version].<dir>.<dir>.<dir>...
+
+<version>:
+        exp -- Version initiale, obsolète (non traitée dans ce document)
+        v00 -- utilisé pour NURP & PAN-AM en 2013-2014. (non couvert dans le présent document)
+        v01 -- version 2015.
+        v02 -- 2015 est passé aux embases AMQP pour les composants non obligatoires
+
+<type>:
+        adm  - Modifier les paramètres
+                ´admin´, ´config´, etc...
+
+        log  - rendre compte de l’état des opérations.
+
+        notify - ´post´ mais dans les versions exp et v00. (Non couvert ici.)
+
+        post - annoncer ou notifier qu’un nouveau bloc de produits est disponible.
+                Chaînes possibles : post,ann(ounce), not(ify)
+
+<source>:
+
+
+

Le reste de ce document suppose la version 2 (sujet v02) :

+

se décompose en:

+
+
+
<horodatage>: date

YYYYMMDDHHMMSS.<decimal>

+
+
+
+

<srcURL> – URL de base utilisée pour récupérer les données.

+
+

options: URL Complet:

+

sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif

+

dans le cas où l’URL ne se termine pas par un séparateur de chemin (‘/’), +le chemin src est considéré comme la source complète du fichier à récupérer.

+

URL Statique:

+

sftp://afsiext@cmcdataserver/

+

Si l’URL se termine par un séparateur de chemin (‘/’), alors l’URL src est +considéré comme un préfixe pour la partie variable de l’URL de récupération.

+
+

<relURL> – Chemin d’accès relatif du répertoire actif dans lequel placer le fichier.

+
+

Deux cas basés sur la fin étant un séparateur de chemin ou non.

+
+

cas 1: NURP/GIF/

+

basé sur le répertoire de travail actuel du client de téléchargement, +créer un sous-répertoire appelé URP, et à l’intérieur de celui-ci, un sous-répertoire +appelé GIF sera créé. Le nom du fichier sera tiré du +srcpath.

+
+
+
Si le srcpath se termine par pathsep, alors le relpath ici sera

concaténé au srcpath, formant l’URL de récupération complète.

+

cas 2: NRP/GIF/mine.gif

+

Si le srcpath se termine par pathsep, le relpath sera concaténé +à srcpath pour former l’URL de récupération complète.

+
+
si le chemin src ne se termine pas par pathsep, l’URL src est prise

comme terminé, et le fichier est renommé lors du téléchargement conformément à la +spécification (dans ce cas, la mienne.gif)

+
+
+
+

AMQP fournit des EN-TÊTES qui sont des paires clé/valeur.

+

Décrivez quelle partie de l’URL est annoncée :

+
+
parts=1,sz

– Extraction dans une seule pièce, de la taille donnée en octets

+
+
parts=<i|p>,<bsz>,<fzb>,<bno>,<remainder>
+

– Récupération en plusieurs parties.

+
+
– File Segment strategy::
+
i - inplace (ne pas créer de fichiers temporaires, juste lseek dans le fichier.)

peut entraîner la création d’un fichier .srsig?

+
+
p - part files. Utilisez .part fichiers, suffixe fixe.

Je ne sais pas lequel sera par défaut.

+
+
+
+
+
+

– La stratégie de segment de fichier peut être remplacée par le client. Juste une suggestion. +– analogues aux options rsync : –inplace, –partial,

+
+
<taille de bloc en octets>: bsz

Nombre d’octets dans un bloc. +Les sommes de contrôle sont calculées par bloc, donc un post

+
+
<taille du fichier en blocs>: fzb

Nombre total entier de blocs dans le fichier +FIXME: (y compris le dernier bloc ou pas?) +si ce paramètre est défini sur 1.

+
+
<block#>: bno

0 origine, le numéro de bloc couvert par cette publication.

+
+
<remainder>: brem
+

normalement 0, sur le dernier bloc, il reste des blocs dans le fichier +à transférer.

+
+
+
– si (fzb=1 and brem=0)

alors bsz=fsz en octets. +– fichiers entiers remplacés. +– C’est la même chose que le mode –whole-file de rsync.

+
+
+
+
+

sum=<algorithm>,<value>

+
+

<algorithm>

+

d - Additionner l’intégralité des données +n - somme de contrôle sur le nom du fichier +<script> - omme de contrôle avec un script, nommée <script>

+
+
<script> doit être “enregistré” dans le réseau de commutateurs.

enregistré signifie que tous les abonnés en aval +peuvent obtenir le script pour valider la somme de contrôle. +Il faut un mécanisme de récupération.

+
+
+

<value> est la valeur de la somme de contrôle

+
+
+
flow=<flowid>
+

Une balise arbitraire utilisée pour le suivi des données via le réseau.

+
+

Les deux voies sont subtilement liées. Ni l’un ni l’autre ne peuvent être interprétés seuls. +Il faut considérer les deux composantes du chemin.

+
+
FIXMEvérifiez les points suivants :

fsz = Taille d’un fichier en octets = ( bsz * (fsb-1) ) + brem ?

+
+
+

exemple 1:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif +EN-TÊTES: +parts=1,457 +rename=NRDPS/GIF/ +sum=d,<md5sum> +flow=exp13

+
+

v01 - version du protocol +post - indique le type de message

+

La version et le type déterminent ensemble le format des sujets suivantes et le corps du message.

+

ec_cmc - le compte utilisé pour émettre le post (unique dans un réseau).

+
+

– taille du fichier est de 457 (== taille du fichier) +– d - La somme de contrôle a été calculée sur le corps. +– flow est appelé ´exp13´ par le poster… +– URL source complète spécifiée (ne se termine pas par ‘/’) +– chemin relatif spécifié pour

+
+
+
pull de:

sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif

+
+
Chemin de téléchargement relatif complet :

NRDPS/GIF/NRDPS_HiRes_000.gif

+

– Prend le nom de fichier de srcpath. +– peut être modifié par un processus de validation.

+
+
+
+

exemple 2:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 http://afsiext@cmcdataserver/data/ +HEADERS: +rename=NRDPS/GIF/NRDPS_HiRes_000.gif +parts=1,457 +sum=d,<md5sum> +flow=exp13

+
+
Dans ce cas,
+
+
pull de:

http://afsiext@cmcdataserver/data/NRPDS/GIF/NRDPS_HiRes_000.gif

+

– srcpath se termine par ‘/’, donc concaténé, prend le fichier de l’URL relative. +– véritable ‘miroir’

+
+
Chemin de téléchargement relatif complet :

NRDPS/GIF/NRDPS_HiRes_000.gif

+
+
+
+

– peut être modifié par un processus de validation.

+
+
+

exemple 3:

+

v02.post.ec_cmc.NRDPS.GIF.NRDPS_HiRes_000.gif +201506011357.345 http://afsiext@cmcdataserver/data/ +HEADERS: +rename=NRDPS/GIF/NRDPS_HiRes_000.gif +parts=i,457,0,0,1,0 +sum=d,<md5sum> +flow=exp13

+

Cas d’attente.

+

wait=on/off

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/Histoire/messages_v03.html b/fr/Explication/Histoire/messages_v03.html new file mode 100644 index 000000000..ba4f97084 --- /dev/null +++ b/fr/Explication/Histoire/messages_v03.html @@ -0,0 +1,218 @@ + + + + + + + Modifications apportées pour créer la v03 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Modifications apportées pour créer la v03

+

Le format réel est défini ici +Une explication de la motivation des changements est ci-dessous:

+
+

Différences par rapport à v02

+

La version 03 est un changement d’encodage, mais la sémantique des champs +sont inchangés par rapport à la version 02. Les modifications sont limitées à la façon dont les champs +sont placés dans les messages. Dans v02, les en-têtes AMQP étaient utilisés pour stocker la valeur des +paires nom-valeur

+
+
    +
  • Les en-têtes v03 ont une longueur pratiquement illimitée. Dans la v02, individuel +Les paires nom-valeur sont limitées à 255 caractères. Cela a prouvé +limitant dans la pratique. Dans v03, la limite n’est pas définie par le standard JSON, +mais par des implémentations d’analyseur spécifiques. Les limites +dans les analyseurs commun sont suffisamment élevés pour ne pas causer de problèmes pratiques.

  • +
  • L’utilisation de la charge utile des messages pour stocker les en-têtes permet d’envisager +d’autres protocoles de messagerie, tels que MQTT 3.1.1, à l’avenir.

  • +
  • Dans la version v03, la charge utile JSON pure simplifie les implémentations, réduit la +documentation requise et la quantité d’analyse à implémenter. L’utilisation d’un format couramment +implémenté permet d’utiliser des analyseurs optimisés existants.

  • +
  • Dans la v03, l’encodage JSON de l’ensemble de la charge utile réduit les fonctionnalités requises +pour qu’un protocole transfère les messages Sarracenia. Par exemple, on pourrait envisager d’utiliser +Sarracenia avec des courtiers MQTT v3.11 qui sont plus standardisés et donc plus facilement +interopérables que AMQP.

  • +
  • Les champs fixes v02 sont maintenant des clés “pubTime”, “baseURL” et “relPath” +dans l’objet JSON qui est le corps du message.

  • +
  • L’en-tête v02 sum avec valeur codée hexadécimale, est remplacé par l’en-tête v03 identity avec codage base64.

  • +
  • L’en-tête v03 content permet l’intégration du contenu du fichier.

  • +
  • Changement de frais généraux… Environ +75 octets par message (variable.)

    +
      +
    • Objet JSON marquant les accolades ‘{’ ‘}’, virgules et guillemets pour +trois champs fixes. net: +10

    • +
    • La section AMQP Propriétés de l’application n’est plus incluse dans la charge utile, ce qui permet d’économiser +un en-tête de 3 octets (remplacé par 2 octets de charge utile entre accolades ouvertes et fermées.) +net: -1 octet

    • +
    • chaque champ a un en-tête d’un octet pour indiquer l’entrée de table dans un paquet AMQP +contre 4 guillemets, un deux-points, un espace et probablement une virgule : 7 au total. +Le changement net est donc de +6 caractères, par en-tête. La plupart des messages v02 ont 6 en-têtes, +net : +36 octets

    • +
    • les champs fixes sont maintenant nommés : pubTime, baseUrl, relPath, en ajoutant 10 caractères +chaque. +30 octets.

    • +
    +
  • +
  • Dans v03, le format des fichiers de sauvegarde est le même que la charge utile du message. +Dans v02, il s’agissait d’un tuple json qui incluait un champ de sujet, le corps et les en-têtes.

  • +
  • Dans v03, le format de rapport est un message de publication avec un en-tête, plutôt que +d’être analysé différemment. Cette spécification unique s’applique donc aux deux.

  • +
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/SarraPluginDev.html b/fr/Explication/SarraPluginDev.html new file mode 100644 index 000000000..201caa29e --- /dev/null +++ b/fr/Explication/SarraPluginDev.html @@ -0,0 +1,1082 @@ + + + + + + + Guide de programmation sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Guide de programmation sarracenia

+
+

Travailler avec des plugins

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+

Audience

+

Les lecteurs de ce manuel doivent être à l’aise avec les scripts légers dans Python version 3. +Alors qu’une grande partie de la compatibilité v2 est incluse dans Sarracenia version 3, +le remplacement des interfaces de programmation est une grande partie de ce qui se trouve dans la version 3. +S’ils travaillent avec la version 2, les programmeurs doivent se référer au Guide du programmeur de la version 2, +car le contenu ici est très différent.

+
+
+

Introduction

+

Sarracenia v3 comprend un certain nombre de points où le traitement peut être personnalisé par +de petits extraits de code fourni par l’utilisateur, appelés flowCallbacks. Les flowCallbacks eux-mêmes +sont censés être concis, et une connaissance élémentaire de Python devrait suffire à +en construire de nouveaux de manière copier/coller, avec de nombreux exemples disponibles à la lecture.

+

Il existe d’autres façons d’étendre Sarracenia v3 en sous-classant :

+
    +
  • Sarracenia.transfer.Transfer pour ajouter plus de protocoles de transfert de données

  • +
  • Sarracenia.identity.Identity pour ajouter plus de méthodes de somme de contrôle.

  • +
  • Sarracenia.moth.Moth pour ajouter la prise en charge de plus de protocoles de messagerie.

  • +
  • Sarracenia.flow.Flow pour créer de nouveaux flux.

  • +
  • Sarracenia.flowcb.FlowCB pour personnaliser les flux.

  • +
  • Sarracenia.flowcb.poll.Poll pour personnaliser les flux de sondage.

  • +
  • Sarracenia.flowcb.scheduled.Scheduled pour personnaliser les flux cédulés.

  • +
+

Cela sera discuté après que les rappels auront été traités.

+

Une pompe de données Sarracenia est un serveur Web avec des notifications pour les abonnés à +savoir, rapidement, quand de nouvelles données sont arrivées. Pour savoir quelles données sont déjà +disponible sur une pompe, visualisez l’arbre avec un navigateur web. Pour de besoins simple et immédiats, +on peut télécharger des données en utilisant le navigateur lui-même ou via un outil standard +comme wget. L’intention habituelle est que sr_subscribe télécharge automatiquement +les données dans un répertoire sur une machine d’abonnée où d’autres logiciels +peuvent les traiter.

+

Souvent, le but du téléchargement automatisé est d’avoir d’autres codes qui ingerent +les fichiers et effectuent un traitement ultérieur. Plutôt que d’avoir un +processus qui regarde un fichier dans un répertoire, on peut insérer un +traitement personnalisé à différents points du flux.

+

Des exemples sont disponibles à l’aide de la commande list

+
fractal% sr3 list fcb
+Provided plugins: ( /home/peter/Sarracenia/development/sarra )
+flowcb/gather/file.py            flowcb/gather/message.py         flowcb/line_log.py               flowcb/line_mode.py
+flowcb/filter/deleteflowfiles.py flowcb/filter/fdelay.py          flowcb/filter/log.py             flowcb/nodupe.py
+flowcb/post/log.py               flowcb/post/message.py           flowcb/retry.py                  flowcb/v2wrapper.py
+fractal%
+fractal% fcbdir=/home/peter/Sarracenia/development/sarra
+
+
+
+

Listes de travail (Worklist)

+

La structure de données de liste de travail est un ensemble de listes de messages. Il y en a quatre :

+
+
    +
  • worklist.incoming - messages à traiter. (construit par gather)

  • +
  • worklist.rejected – message qui ne doit pas être traité ultérieurement. (généralement par filtrage.)

  • +
  • worklist.ok - messages qui ont été traités avec succès. (généralement par work.)

  • +
  • worklist.failed : messages pour lesquels le traitement a été tenté, mais qui a échoué.

  • +
+
+

La liste de travail est transmise aux plugins after_accept et after_work comme détaillé dans la section suivante.

+

Tous les composants (post, subscribe, sarra, sender, shovel, watch, winnow) +partagent du code substantiel et ne diffèrent que par les paramètres de défaut. L’algorithme de flux est :

+
    +
  • Rassemblez une liste de messages, à partir d’un fichier ou d’une source de messages en amont (une pompe de données). +placer de nouveaux messages dans _worklist.incoming_

  • +
  • Filtrez-les avec des clauses accept/reject, les messages rejetés sont déplacés vers _worklist.rejected_ . +Les callbacks after_accept manipulent davantage les listes de travail après le filtrage initial d’accept/reject.

  • +
  • Travailler sur les messages entrants restants, en effectuant le téléchargement, l’envoi ou tout autre travail qui crée de nouveaux fichiers. +lorsque le travail d’un message réussit, le message est déplacé vers _worklist.ok_ . +Si le travail pour un message échoue, le message est déplacé vers _worklist.failed_ .

  • +
  • (facultatif) Publiez le travail accompli (messages sur _worklist.ok_) pour le prochain flux à consommer.

  • +
+
+
+
+

Rappels de Flux (Flow Callbacks)

+

Avec les nombreuses façons d’étendre les fonctionnalités, la plus courante est l’ajout de rappels +pour faire circuler les composants. Tous les composants de Sarracenia sont implémentés à l’aide de +la classe sarra.flow. Il existe une classe parent sarra.flowcb pour les implémenter. +Les plugins du paquet sont affichés dans le premier groupe de ceux disponibles. Beaucoup d’entre eux ont des arguments qui +sont documentés en les énumérant. Dans un fichier de configuration, on peut avoir la ligne:

+
flowCallback sarracenia.flowcb.log.Log
+
+
+

Cette ligne amène Sarracenia à regarder dans le chemin de recherche Python une classe comme :

+
blacklab% cat sarra/flowcb/msg/log.py
+
+from sarracenia.flowcb import FlowCB
+import logging
+
+logger = logging.getLogger(__name__)
+
+class Log(FlowCB):
+  def after_accept(self, worklist):
+      for msg in worklist.incoming:
+          logger.info("received: %s " % msg)
+
+  def after_work(self, worklist):
+      for msg in worklist.ok:
+          logger.info("worked successfully: %s " % msg)
+
+
+

Le module imprimera chaque message accepté, et chaque message après avoir travaillé dessus +quand il est terminé (ou le téléchargement a eu lieu, par exemple). Pour modifier la classe de callback, +copiez-la à partir du répertoire répertorié dans la commande list fcb vers un endroit dans le +PYTHONPATH de l’environnement, puis modifiez-la aux fins prévues.

+

On peut également voir quels plugins sont actifs dans une configuration en regardant les messages au démarrage:

+
blacklab% sr3 foreground subscribe/clean_f90
+2018-01-08 01:21:34,763 [INFO] sr_subscribe clean_f90 start
+
+.
+.
+.
+
+2020-10-12 15:20:06,250 [INFO] sarra.flow run callbacks loaded: ['sarra.flowcb.retry.Retry', 'sarra.flowcb.msg.log.Log', 'file_noop.File_Noop', 'sarra.flowcb.v2wrapper.V2Wrapper', 'sarra.flowcb.gather.message.Message'] 2
+.
+.
+.
+blacklab%
+
+
+

L’utilisation de l’option flowCallbackPrepend aura la classe chargée au début de la liste, plutôt que +à la fin.

+
+
+

Paramètres

+

Souvent, lors de l’écriture d’extensions via la sous-classification, des options supplémentaires doivent être définies. +La classe sarracenia.config effectue analyse d’options a partir de la ligne de commande et de fichier de configuration. +Il y a une routine qui peut être appelée à partir du nouveau code +pour définir des paramètres supplémentaires, généralement à partir de la routine __init__, dans les classes intégrées +ou flowcb accepte comme paramètre _options_ dans leurs routines __init__()

+
somewhere in the __init__(self, options):
+
+options.add_option('accel_wget_command', 'str', '/usr/bin/wget')
+
+
+def add_option(self, option, kind='list', default_value=None):
+
+"""
+     options can be declared in any plugin. There are various *kind* of options, where the declared type modifies the parsing.
+
+     'count'      integer count type.
+     'duration'   a floating point number indicating a quantity of seconds (0.001 is 1 milisecond)
+                  modified by a unit suffix ( m-minute, h-hour, w-week )
+     'flag'       boolean (True/False) option.
+     'list'       a list of string values, each succeeding occurrence catenates to the total.
+                  all v2 plugin options are declared of type list.
+     'size'       integer size. Suffixes k, m, and g for kilo, mega, and giga (base 2) multipliers.
+     'str'        an arbitrary string value, as will all of the above types, each succeeding occurrence overrides the previous one.
+
+"""
+
+
+

L’exemple ci-dessus définit une option “accel_wget_command” +comme étant de type chaîne, avec la valeur par défaut __/usr/bin/wget__ .

+

Autres méthodes utiles dans la classe sarracenia.config.Config :

+
    +
  • variableExpansion( value, Message=None) … pour que des modèles tels que ${YYYYMMDD-5m} soient évalué +dans les fichiers de configuration. On peut vouloir évaluer ces expansions à différents moments du traitement, +selon le but des options définies par l’utilisateur.

  • +
+

liste complète ici : https://metpx.github.io/sarracenia/Reference/code.html#sarracenia.config.Config

+
+

Paramètres hiérarchiques

+

On peut également créer des paramètres spécifiques pour les classes de rappel individuelles à l’aide du _set_ +et en identifiant la classe exacte à laquelle le paramètre s’applique. Par exemple +parfois, tourner le logLevel en débogage peut entraîner des fichiers journaux très volumineux, et on pourrait +activer uniquement la sortie de débogage pour certaines classes de rappel. Cela peut être fait via:

+
set sarracenia.flowcb.gather.file.File.logLevel debug
+
+
+

La commande _set_ peut également être utilisée pour définir des options à transmettre à n’importe quel plugin.

+
+
+

Affichage de tous les paramètres

+

Utilisez la commande _sr3_ _show_ pour afficher tous les paramètres actifs résultant d’un fichier de configuration

+
fractal% sr3 show sarra/download_f20.conf
+
+Config of sarra/download_f20:
+_Config__admin=amqp://bunnymaster@localhost, _Config__broker=amqp://tfeed@localhost, _Config__post_broker=amqp://tfeed@localhost, accel_threshold=100.0,
+accept_unmatch=True, accept_unmatched=False, announce_list=['https://tracker1.com', 'https://tracker2.com', 'https://tracker3.com'], attempts=3,
+auto_delete=False, baseDir=None, batch=1, bind=True, bindings=[('v03', 'xsarra', '#')], bufsize=1048576, bytes_per_second=None, bytes_ps=0,
+cfg_run_dir='/home/peter/.cache/sr3/sarra/download_f20', chmod=0, chmod_dir=509, chmod_log=384, config='download_f20', currentDir=None, debug=False,
+declare=True, declared_exchanges=['xpublic', 'xcvan01'], declared_users="...rce', 'anonymous': 'subscriber', 'ender': 'source', 'eggmeister': 'subscriber'}",
+delete=False, destfn_script=None, directory='/home/peter/sarra_devdocroot', documentRoot=None, download=False, durable=True, exchange=['xflow_public'],
+expire=25200.0, feeder=amqp://tfeed@localhost, filename=None, fixed_headers={}, flatten='/', hostdir='fractal', hostname='fractal', housekeeping=60.0,
+imports=[], inflight=None, inline=False, inlineEncoding='guess', inlineByteMax=4096, instances=1,
+logFormat='%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', logLevel='info', log_reject=True, lr_backupCount=5, lr_interval=1,
+lr_when='midnight', masks="...nia/insects/flakey_broker', None, re.compile('.*'), True, True, 0, False, '/')]", message_count_max=0, message_rate_max=0,
+message_rate_min=0, message_strategy={'reset': True, 'stubborn': True, 'failure_duration': '5m'}, message_ttl=0, mirror=True, notify_only=False,
+overwrite=True, plugins=['sample.Sample', 'sarracenia.flowcb.log.Log'], post_baseDir='/home/peter/sarra_devdocroot', post_baseUrl='http://localhost:8001',
+post_documentRoot=None, post_exchange=['xflow_public'], post_exchanges=[], prefetch=1, preserve_mode=True, preserve_time=False, program_name='sarra',
+pstrip=False, queue_filename='/home/peter/.cache/sr3/sarra/download_f20/sarra.download_f20.tfeed.qname',
+queue_name='q_tfeed_sarra.download_f20.65966332.70396990', randid='52f9', realpathPost=False, report=False, report_daemons=False, reset=False,
+resolved_exchanges=['xflow_public'], resolved_qname='q_tfeed_sarra.download_f20.65966332.70396990', settings={}, sleep=0.1, statehost=False, strip=0,
+subtopic=None, suppress_duplicates=0, suppress_duplicates_basis='path', timeout=300, tlsRigour='normal', topicPrefix='v03',
+undeclared=['announce_list'], users=False, v2plugin_options=[], v2plugins={}, vhost='/', vip=None
+
+fractal%
+
+
+
+
+
+

Contrôle de la journalisation

+

La méthode de compréhension de l’activité de flux sr3 consiste à examiner ses journaux. +La journalisation peut être assez lourde dans sr3, il existe donc de nombreuses façons de l’affiner.

+
+

logLevel

+

le logLevel normal est utilisé dans les classes Log de python intégrées. Il a les +niveaux : debug, info, warning, error, et critical, où level indique +le message de priorité la plus basse à imprimer. La valeur par défaut est info.

+

Parce qu’un simple commutateur binaire du logLevel peut entraîner d’énormes journaux, pour +exemple lors de l’interrogation (poll), où chaque fois que chaque ligne est interrogée peut générer une ligne de journal. +La surveillance des protocoles MQP peut être également détaillée, donc par défaut ni l’un ni l’autre +d’entre eux sont en fait mis en mode débogage par le paramètre logLevel global. +certaines classes n’honorent pas le cadre global et demandent une activation:

+
+
+

set sarracenia.transfer.Transfer.logLevel debug

+

Peut contrôler le logLevel utilisé dans les classes de transfert, pour le définir plus bas ou plus haut +que le reste de sr3.

+
+
+

set sarracenia.moth.amqp.AMQP.logLevel debug

+

Imprimez les messages de débogage spécifiques à la fil d’attente de messages AMQP (classe sarracenia.moth.amqp.AMQP). +utilisé uniquement lors du débogage avec le MQP lui-même, pour traiter les problèmes de connectivité du courtier par exemple. +diagnostic et test d’interopérabilité.

+
+
+

set sarracenia.moth.mqtt.MQTT.logLevel debug

+

Imprimez les messages de débogage spécifiques à la fil d’attente de messages MQTT (classe sarracenia.moth.mqtt.MQTT). +utilisé uniquement lors du débogage avec le MQP lui-même, pour traiter les problèmes de connectivité du courtier par exemple. +diagnostic et test d’interopérabilité.

+
+
+

logEvents

+

valeur par défaut : after_accept, after_work, on_housekeeping +disponible: after_accept, after_work, all, gather, on_housekeeping, on_start, on_stop, post

+

implémenté par la classe sarracenia.flowcb.log.Log, on peut sélectionner les événements qui génèrent le journal +Messages. caractère générique : all génère des messages de journal pour chaque événement connu de la classe Log.

+
+
+

logMessageDump

+

mis en œuvre par sarracenia.flowcb.log, à chaque événement de journalisation, imprimer le contenu actuel +du message en cours de traitement.

+
+
+

logReject

+

imprimer un message de journal pour chaque message rejeté (normalement ignoré silencieusement).

+
+
+

messageDebugDump

+

Implémenté dans des sous-classes de moth, imprime les octets réellement reçus ou envoyés +pour le protocole MQP utilisé.

+
+
+
+

Extension des classes

+

On peut ajouter des fonctionnalités supplémentaires à Sarracenia en créant des sous-classes.

+
    +
  • sarra.moth - Messages organisés en hiérarchies de thèmes. (existants : rabbitmq-amqp)

  • +
  • sarra.identity - algorithmes de somme de contrôle (existants: md5, sha512, arbitraires, aléatoires)

  • +
  • sarra.transfer - protocoles de transport supplémentaires (https, ftp, sftp )

  • +
  • sarra.flow - création de nouveaux composants au-delà des composants intégrés. (post, sarra, shovel, etc…)

  • +
  • sarra.flowcb - personnalisation des flux de composants à l’aide de rappels.

  • +
  • sarra.flowcb.poll - personnalisation du rappel de poll pour les sources non standard.

  • +
+

On commencerait par l’une des classes existantes, on la copierait ailleurs dans le chemin python, +et on construirez notre extension. Ces classes sont ajoutées à Sarra à l’aide de l’option import +dans les fichiers de configuration. les fichiers __init__ dans les répertoires sources sont les bons +pour rechercher des informations sur l’API de chaque classe.

+
+
+

The Simplest Flow_Callback

+
+
+

Sample Extensions

+

Vous trouverez ci-dessous une classe d’exemple flowCallback minimale, qui se trouverait dans un sample.py. +Le fichier est placé dans n’importe quel répertoire du PYTHONPATH:

+
import logging
+import sarracenia.flowcb
+
+# this logger declaration  must be after last import (or be used by imported module)
+logger = logging.getLogger(__name__)
+
+class Sample(sarracenia.flowcb.FlowCB):
+
+    def __init__(self, options):
+
+        super().__init__(options)
+
+        # declare a module specific setting.
+        options.add_option('announce_list', list )
+
+    def on_start(self):
+
+        logger.info('announce_list: %s' % self.o.announce_list )
+
+
+

Tout ce qu’il fait est d’ajouter un paramètre appelé ‘announce-list’ à la configuration. +puis imprimer la valeur au démarrage.

+

Dans un fichier de configuration, on s’attendrait à voir

+
flowCallback sample.Sample
+
+announce_list https://tracker1.com
+announce_list https://tracker2.com
+announce_list https://tracker3.com
+
+
+

Et au démarrage, le message de journalisation s’imprimerait:

+
021-02-21 08:27:16,301 [INFO] sample on_start announce_list: ['https://tracker1.com', 'https://tracker2.com', 'https://tracker3.com']
+
+
+

Les développeurs peuvent ajouter des protocoles de transfert supplémentaires pour les messages ou +transport de données à l’aide de la directive import pour que la nouvelle classe soit +disponible:

+
import torr
+
+
+

serait un nom raisonnable pour que le protocole de transfert récupère les +ressources avec le protocole bittorrent. import peut également être utilisé +pour importer des modules python arbitraires à utiliser par des rappels.

+
+
+

Champs dans les Messages

+

les rappels reçoivent le paramètre sarracenia.options déjà analysé. +self est le message en cours de traitement. variables les plus utilisées :

+
+
msg[‘exchange’]

Échange par lequel le message est posté ou consommé.

+
+
msg[‘isRetry’]

S’il s’agit d’une tentative ultérieure d’envoi ou de téléchargement d’un message.

+
+
msg[‘new_dir’]

Le répertoire qui contiendra msg[‘new_file’]

+
+
msg[‘new_file’]

Une variable populaire dans les plugins on_file et on_part est : msg[‘new_file], +en donnant le nom de fichier dans lequel le produit téléchargé a été écrit. Lorsque +la même variable est modifiée dans un plugin after_accept, elle change le nom du +fichier à télécharger. De même, une autre variable souvent utilisée est +parent.new_dir, qui fonctionne sur le répertoire dans lequel le fichier +sera téléchargé.

+
+
msg[‘new_inflight_file’]

dans téléchargements et envois, ce champ sera défini avec le nom temporaire +d’un fichier utilisé pendant le transfert. Une fois le transfert terminé, +le fichier doit être renommé à qui se trouve dans msg[‘new_file’].

+
+
msg[‘pubTime’]

Heure à laquelle le message a été inséré dans le réseau (premier champ d’un avis).

+
+
msg[‘baseUrl’]

racine URL de l’arborescence de publication à partir de laquelle les chemins relatifs sont construits.

+
+
msg[‘relPath’]

Chemin d’accès relatif à partir de l’URL de base du fichier. +la concaténation des deux donne l’URL complète.

+
+
msg[‘retrievePath’]

Pour le supprimer le chemin logique resultant de baseUrl et relPath, on peut specifier +un url pour chercher la ressource distant.

+
+
msg[‘fileOp’]

pour les opérations de téléchargement de fichiers sans données, telles que la création de liens +symboliques, les changements de nom et les suppressions de fichiers. +Contenu décrit dans sr_post(7)

+
+
msg[‘identity’]

La structure de somme de contrôle, un dictionnaire python avec les champs ‘méthode’ et ‘valeur’.

+
+
msg[‘subtopic’], msg[‘new_subtopic’]

liste des chaînes (avec le préfixe de thème supprimé) +généré automatiquement à partir dur msg[´relPath´] inutile de le modifié.

+
+
msg[‘_deleteOnPost’]

lorsque l’état doit être stocké dans les messages, on peut déclarer des champs temporaires supplémentaires +à utiliser uniquement dans le cadre du processus en cours d’exécution. Pour les marquer pour suppression lors du transfert, +ce champ (de type: python set) est utilisé

+
msg['my_new_field'] = my_temporary_state
+msg['_deleteOnPost'] |= set(['my_new_field'])
+
+
+

Par exemple, tous les champs new_ sont dans _deleteOnPost par défaut.

+
+
+

msg[‘onfly_checksum’], msg[‘data_checksum’]

+
+

la valeur d’un champ de somme de contrôle Intégrité calculée au fur et à mesure que +les données sont téléchargées. Dans le cas où les données sont modifiées lors +du téléchargement, le onfly_checksum est de vérifier que les données +amont ont été correctement reçues, tandis que data_checksum est calculé +pour les consommateurs en aval.

+
+

Ce sont les champs de message qui sont le plus souvent d’intérêt, mais beaucoup d’autres +peuvent être consulté par les éléments suivants dans une configuration

+
logMessageDump True
+callback log
+
+
+

Ce qui garantit que la classe log flowcb est active et active le paramètre +pour imprimer des messages bruts pendant le traitement.

+
+
+

Accès aux options

+

Les paramètres résultant de l’analyse des fichiers de configuration sont également facilement disponibles. +Les plugins peuvent définir leurs propres options en appelant:

+
FIXME: api incomplete.
+Config.add_option( option='name_of_option', kind, default_value  )
+
+
+

Les options ainsi déclarées deviennent simplement des variables d’instance dans les options transmises à init. +Par convention, les plugins définissent self.o pour contenir les options passées au moment de l’initialisation, de sorte que +toutes les options intégrées sont traitées de la même manière. En consultant le sr_subscribe(1), +la plupart des options auront une variable d’instance corrélative.

+

Quelques exemples :

+
+
self.o.baseDir

le répertoire de base de l’emplacement des fichiers lors de la consommation d’une publication.

+
+
self.o.suppress_duplicates

Valeur numérique indiquant la durée de vie de la mise en cache (l’âge des entrées avant qu’elles ne vieillissent). +La valeur 0 indique que la mise en cache est désactivée.

+
+
self.o.inflight

Le paramètre actuel de inflight (voir Delivery Completion)

+
+
self.o.overwrite

qui contrôle si les fichiers déjà téléchargés doivent être remplacés sans condition.

+
+
self.o.discard

Si les fichiers doivent être supprimés après leur téléchargement.

+
+
+
+
+

Points de rappel de flux

+

Sarracenia interprétera les noms des fonctions comme des heures d’indication dans le de traitement lorsque +une routine donnée devrait être appelée.

+

Voir le FlowCB source +pour des informations détaillées sur les signatures d’appel et les valeurs de retour, etc.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Name

Quand/Pourquoi il est appelé

ack

accuser réception des messages d’un courtier.

after_accept +(self,worklist)

très fréquemment utilisé.

+

peut modifier les messages dans worklist.incoming +ajout d’un champ ou modification d’une valeur.

+

Déplacez les messages entre les listes de messages +dans worklist. pour rejeter un message, il est +déplacé de worklist.incoming -> worklist.rejected. +(sera reconnu et rejeté.)

+

Pour indiquer qu’un message a été traité, déplacez +worklist.incoming -> worklist.ok +(sera reconnu et rejeté.)

+

Pour indiquer l’échec du traitement, déplacez : +worklist.incoming -> worklist.failed +ira dans la fil d’attente pour réessaie plus tard

+

Exaeples: msg_* dans le répertoire exemples

+

msg_delay - assurez-vous que les messages sont +anciens avant de les traiter.

+

msg_download - modifier les messages pour utiliser +différent téléchargeurs en fonction de la taille du +fichier (intégré pour les petits, téléchargeurs +binaires pour les fichiers volumineux.)

+

after_work +(self,worklist)

appelé après qu’un transfert a été tenté.

+

A ce point, tous les messages sont reconnus. +worklist.ok contient des transferts réussis +worklist.failed contient des transferts échoué +worklist.rejected contient des transferts rejetés +pendant le transfert.

+

généralement à propos de faire quelque chose avec +le fichier après que le téléchargement est terminé.

+

destfn_script

changer msg[‘new_file’] a son gout +appelé lors du changement de nom du fichier en vol +son nom permanent

+

NOT IMPLEMENTED? FIXME?

+

download(self,msg)

remplacer le téléchargeur intégré, retourner true +pour un succès. Prends un messafe comme argument.

gather(self)

Rassembler les messages a la source, retourne une +une liste de messages. +on peut également retourner un tuple dont le +première élément est une valeur booléen keep_going +qui peut arreter l´execution des gather.

on_housekeeping +(self)

Appelé à chaque intervalle housekeeping (minutes). +utilisé pour nettoyer le cache, vérifier les +problèmes occasionnels. Gérer les files d’attentes

+

retourne False pour abandonner le traitement +ultérieur. Retourne True pour continuer.

+

on_start(self)

Quand un composant (e.g. sr_subscribe) est démarré. +Peut être utlisé pour lire l’état a partir d’un +fichier. +fichier d’état dans self.o.user_cache_dir

+

valeur retourné ignoré

+

exemple: file_total_save.py [1]

+

on_stop(self)

+
Quand un composant (e.g. sr_subscribe) est arrêté.

peut être utilisé pour conserver l’état.

+

fichier d’état dans self.o.user_cache_dir

+
+
+

valeur retourné ignoré

+

poll(self)

remplace la méthode d’interrogation (poll) intégrée +retourne une liste de messages.

post(self,worklist)

remplacez la routine de publication (post) intégrée

send(self,msg)

remplacez la routine d’envoi (send) intégrée

+
+

Personnalisation du Callback de Flux de Poll

+

Une sous-classe intégrée de flowcb, sarracenia.flowcb.poll.Poll implémente la majorité du +sondage (poll) sr3. Il existe de nombreux types de ressources à interroger, et +tant d’options pour les personnaliser sont nécessaires. La personnalisation est accomplie +avec la sous-classification, de sorte que le haut d’un tel rappel ressemble à:

+
...
+from sarracenia.flowcb.poll import Poll
+....
+
+class Nasa_mls_nrt(Poll):
+
+
+

Plutôt que d’implémenter une classe flowcb, on sous-classe la classe +flowcb.poll.Poll. Voici les sous classes commune du sondage avec des +points d’entrée spécifiques généralement implémentés dans les sous-classes :

+ + + + + + + + + +

handle_data

dans sr_poll si vous souhaitez uniquement modifier +la façon dont l’URL html téléchargée est analysée +remplacez ceci. +action: +analyser parent.entries pour faire self.entries

+

Exemples: html_page* dans le répertoire exemples

+

on_line

dans sr_poll si les sites ont des formats distants +différents, appelé pour analyser chaque ligne dans +parent.entries. +Travaille sur parent.line

+

retourner False pour abandonner le traitement +retourner True pour continuer

+

Exemples: line_* dans le répertoire exemples

+
+

Voir les classes intégrés flowcb Poll +est utile.

+ +
+
+
+
+

Meilleure réception des fichiers

+

Par exemple, plutôt que d’utiliser le système de fichiers, sr_subscribe pourrait indiquer quand chaque fichier est prêt +en écrivant dans un canal nommé

+
blacklab% sr_subscribe edit dd_swob.conf
+
+broker amqps://anonymous@dd.weather.gc.ca
+subtopic observations.swob-ml.#
+
+flowcb sarracenia.flowcb.work.rxpipe.RxPipe
+rxpipe_name /tmp/dd_swob.pipe
+
+directory /tmp/dd_swob
+mirror True
+accept .*
+
+# rxpipe is a builtin on_file script which writes the name of the file received to
+# a pipe named '.rxpipe' in the current working directory.
+
+
+

Avec l’option flowcb, on peut spécifier une option de traitement telle que rxpipe. Avec rxpipe, +chaque fois qu’un transfert de fichier est terminé et est prêt pour le post-traitement, son nom est écrit +au canal Linux (nommé .rxpipe) dans le répertoire de travail actuel. Donc le code pour le post-traitement +devient:

+
do_something <.rxpipe
+
+
+

Aucun filtrage des fichiers de travail par l’utilisateur n’est requis, et l’ingestion de fichiers partiels est +complètement évité.

+
+

Note

+

Dans le cas où un grand nombre d’instances sr_subscribe fonctionnent +sur la même configuration, il y a une légère probabilité que les notifications +peuvent se corrompre mutuellement dans le canal nommé. +Nous devrions probablement vérifier si cette probabilité est négligeable ou non.

+
+
+

Réception avancée des fichiers

+

Le point d’entrée after_work dans une classe sarracenia.flowcb est une action à effectuer +après réception d’un fichier (ou après l’envoi, dans un sender.) Le module RxPipe en est un exemple +fourni avec sarracenia:

+
import logging
+import os
+from sarracenia.flowcb import FlowCB
+
+logger = logging.getLogger(__name__)
+
+class RxPipe(FlowCB):
+
+    def __init__(self,options):
+
+        super().__init__(options,logger)
+        self.o.add_option( option='rxpipe_name', kind='str' )
+
+    def on_start(self):
+        if not hasattr(self.o,'rxpipe_name') and self.o.file_rxpipe_name:
+            logger.error("Missing rxpipe_name parameter")
+            return
+        self.rxpipe = open( self.o.rxpipe_name, "w" )
+
+    def after_work(self, worklist):
+
+        for msg in worklist.ok:
+            self.rxpipe.write( msg['new_dir'] + os.sep + msg['new_file'] + '\n' )
+        self.rxpipe.flush()
+        return None
+
+
+

Avec ce fragment de Python, lorsque sr_subscribe est appelé pour la première fois, il s’assure que +un canal nommé npipe est ouvert dans le répertoire spécifié en exécutant +la fonction __init__ dans la classe python RxPipe déclarée. Puis, chaque fois qu’une +réception de dossier est terminée, l’attribution de self.on_file assure que +la fonction rx.on_file est appelée.

+

La fonction rxpipe.on_file écrit simplement le nom du fichier téléchargé dans +le canal nommé. L’utilisation du canal nommé rend la réception des données asynchrone +du traitement des données. Comme le montre l’exemple précédent, on peut alors +démarrer une seule tâche do_something qui traite la liste des fichiers alimentés +en tant qu’entrée standard, à partir d’un canal nommé.

+

Dans les exemples ci-dessus, la réception et le traitement des fichiers sont entièrement séparés. S’il y a +un problème de traitement, les répertoires de réception de fichiers se rempliront, potentiellement +atteignant une taille encombrante et causent de nombreuses difficultés pratiques. Quand un plugin comme +on_file est utilisé, le traitement de chaque fichier téléchargé est exécuté avant de continuer +au fichier suivant.

+

Si le code du script on_file est modifié pour effectuer du traitement réel, alors +plutôt que d’être indépendant, le traitement pourrait fournir une contre-pression au +mécanisme de livraison des données. Si le traitement est bloqué, le sr_subscriber +arrêtera le téléchargement et la fil d’attente sera sur le serveur, plutôt que de créer +un énorme répertoire local sur le client. Différents modèles s’appliquent dans différents +Situations.

+

Un point supplémentaire est que si le traitement des fichiers est appelé +dans chaque cas, fournissant un traitement parallèle très facile construit +dans sr_subscribe.

+
+

Utilisation des Identifiants dans les Plugins

+

Pour mettre en œuvre la prise en charge de protocoles supplémentaires, il faut souvent des informations d’identification +dans le script avec le code :

+
    +
  • ok, details = self.o.credentials.get(msg.urlcred)

  • +
  • if details : url = details.url

  • +
+

Le détails des options sont des éléments de la classe de détails (hardcoded) :

+
    +
  • print(details.ssh_keyfile)

  • +
  • print(details.passive)

  • +
  • print(details.binary)

  • +
  • print(details.tls)

  • +
  • print(details.prot_p)

  • +
+

Pour les informations d’identification qui définissent le protocole de téléchargement (upload), +la connexion, une fois ouverte, reste ouverte. Il est réinitialisé +(fermé et rouvert) uniquement lorsque le nombre de téléchargements (uploads) +atteint le nombre donné par l’option batch (100 par défaut).

+

Toutes les opérations de téléchargement (upload) utilisent un buffer. La taille, en octets, +du buffer utilisé est donné par l’option bufsize (8192 par défaut).

+
+
+

Pourquoi l’API v3 doit être utilisée dans la mesure du possible

+
    +
  • utilise importlib de python, un moyen beaucoup plus standard d’enregistrer des plugins. +maintenant les erreurs de syntaxe seront détectées comme n’importe quel autre module python importé, +avec un message d’erreur raisonnable.

  • +
  • pas de décoration étrange à la fin des plugins (self.plugin = , etc… juste du python ordinaire.) +Modules python entièrement standard, uniquement avec des méthodes/fonctions connues

  • +
  • Le choix étrange de parent comme lieu de stockage des paramètres est déroutant pour les gens. +La variable d’instance parent devient options, self.parent devient self.o

  • +
  • les rappels d’événements pluriels remplacent les rappels singuliers. after_accept remplace on_message

  • +
  • les messages ne sont que des dictionnaires python. champs définis par json.loads( format de charge utile v03 ) +les messages ne contiennent que les champs réels, pas de paramètres ou d’autres choses… +données simples.

  • +
  • ce qu’on appelait autrefois les plugins, ne sont plus qu’un type de plugins, appelés flowCallbacks. +Ils déplacent maintenant les messages entre les listes de travail.

  • +
+

Avec cette API, traiter différents nombres de fichiers d’entrée et de sortie devient beaucoup +plus naturel, lors du décompression d’un fichier tar, des messages pour les fichiers décompressés peuvent être ajoutés +à la liste ok, de sorte qu’ils seront affichés lorsque le flux arrivera là-bas. +De même, un grand nombre de petits fichiers peuvent être regroupés pour en créer un +fichier volumineux, donc plutôt que de transférer tous les fichiers entrants vers la liste, +seul le seau de tar résultant sera placé dans ok.

+

Le mécanisme d’importation import décrit ci-dessous fournit un moyen simple +d’étendre Sarracenia en créant des enfants des classes principales

+
    +
  • moth (messages organisés en hiérarchies de thèmes) pour traiter les nouveaux protocoles de message.

  • +
  • transfert … pour ajouter de nouveaux protocoles pour les transferts de fichiers.

  • +
  • flux .. nouveaux composants avec un flux différent de ceux intégrés.

  • +
+

Dans la v2, il n’y avait pas de mécanisme d’extension équivalent et l’ajout de protocoles +aurait nécessité une refonte du code de base de manière personnalisée pour chaque ajout.

+
+
+
+
+

Notification de fichier sans téléchargement

+

Si la pompe de données existe dans un environnement partagé de grande taille, tel qu’un +centre de supercalcul avec un système de fichiers de site, +le fichier peut être disponible sans téléchargement. Donc, juste +obtenir la notification de fichier et le transformer en fichier local est suffisant

+
blacklab% sr_subscribe edit dd_swob.conf
+
+broker amqps://anonymous@dd.weather.gc.ca
+subtopic observations.swob-ml.#
+document_root /data/web/dd_root
+download off
+flowcb msg_2local.Msg2Local
+flowcb do_something.DoSomething
+
+accept .*
+
+
+

Il devrait y avoir deux fichiers dans le PYTHONPATH quelque part contenant des +classes dérivées de FlowCB avec des routines after_accept déclarées. +Le traitement dans ces routines se fera à la réception d’un lot +de messages. Un message correspondra à un fichier.

+

les routines after_accept acceptent une liste de travail comme argument.

+
+

Note

+

FIXME: peut-être montrer un moyen de vérifier l’en-tête des pièces +avec une instruction afin d’agir uniquement sur le message de première partie +pour les fichiers longs.

+
+
+

Ajout de Dépendance Python dans les Callbacks

+

Certains callback doivent utiliser d’autres modules Python. Alors que les +importations normales sont bien, on peut mieux les intégrer pour les +utilisateurs sr3 en prenant en se servant du mécanisme features

+
from sarracenia.featuredetection import features
+#
+# Support for features inventory mechanism.
+#
+features['clamd'] = { 'modules_needed': [ 'pyclamd' ], 'Needed': True,
+        'lament' : 'cannot use clamd to av scan files transferred',
+        'rejoice' : 'can use clamd to av scan files transferred' }
+
+try:
+    import pyclamd
+    features['clamd']['present'] = True
+except:
+    features['clamd']['present'] = False
+
+
+

Cela permet aux utilisateurs de savoir quelles features sont disponibles dans leur installation. +Lorsqu’ils exécutent sr3 features, sr3 fournit une liste facile à comprendre des éléments manquants:

+
fractal% sr3 features
+2023-08-07 13:18:09,219 1993037 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'dcpflow', 'log', 'post.message', 'clamav']
+2023-08-07 13:18:09,224 1993037 [INFO] dcpflow __init__ really I mean hi
+2023-08-07 13:18:09,224 1993037 [WARNING] sarracenia.config add_option multiple declarations of lrgs_download_redundancy=['Yes', 'on'] choosing last one: on
+2023-08-07 13:18:09,225 1993037 [INFO] dcpflow __init__  lrgs_download_redundancy is True
+2023-08-07 13:18:09,225 1993037 [INFO] sarracenia.flowcb.log __init__ flow initialized with: {'post', 'on_housekeeping', 'after_work', 'after_accept', 'after_post'}
+2023-08-07 13:18:09,226 1993037 [CRITICAL] sarracenia.flow loadCallbacks flowCallback plugin clamav did not load: 'pyclamd'
+
+Status:    feature:   python imports:      Description:
+Installed  amqp       amqp                 can connect to rabbitmq brokers
+Installed  appdirs    appdirs              place configuration and state files appropriately for platform (windows/mac/linux)
+Installed  filetypes  magic                able to set content headers
+Installed  ftppoll    dateparser,pytz      able to poll with ftp
+Installed  humanize   humanize             humans numbers that are easier to read.
+Absent     mqtt       paho.mqtt.client     cannot connect to mqtt brokers
+Installed  redis      redis,redis_lock     can use redis implementations of retry and nodupe
+Installed  sftp       paramiko             can use sftp or ssh based services
+Installed  vip        netifaces            able to use the vip option for high availability clustering
+Installed  watch      watchdog             watch directories
+Installed  xattr      xattr                on linux, will store file metadata in extended attributes
+MISSING    clamd      pyclamd              cannot use clamd to av scan files transferred
+
+ state dir: /home/peter/.cache/sr3
+ config dir: /home/peter/.config/sr3
+
+
+

On peut voir que le module Python pyclamed nécessary pour que le callback fonctionne n´est pas +installé.

+
+
+

Idées d’extension

+

Exemples de choses qui seraient amusantes à faire avec les plugins:

+
    +
  • Common Alerting Protocol (CAP), est un format XML qui fournit des avertissements +pour de nombreux types d’événements, en indiquant la zone de couverture. Il y a un +champ ‘polygone’ dans l’avertissement, que la source pourrait ajouter aux messages en utilisant +un plugin on_post. Les abonnés auraient accès à l’en-tête ‘polygone’ +grâce à l’utilisation d’un plugin after_accept, leur permettant de déterminer si l’avertissement +affecté une zone d’intérêt sans télécharger l’intégralité de l’avertissement.

  • +
  • Une source qui applique la compression aux produits avant de poster, pourrait ajouter un +en-tête tel que ‘uncompressed_size’ et ‘uncompressed_sum’ pour permettre aux +abonnés avec un plugin after_accept de comparer un fichier qui a été localement +non compressé dans un fichier en amont proposé sous forme compressée.

  • +
  • ajouter Bittorrent, S3, IPFS comme protocoles de transfert (sous-classification Transfer)

  • +
  • ajouter des protocoles de message supplémentaires (sous-classification Moth)

  • +
  • des sommes de contrôle supplémentaires, sous-classification de l’intégrité. Par exemple, pour obtenir des données GOES DCP +provenant de sources telles que l’USGS Sioux Falls, les rapports ont une remorque +qui montre quelques statistiques d’antenne du site de réception. Donc, si l’un d’entre eux +reçoit GOES DCP de Wallops, par exemple, la bande-annonce sera différente. +Ainsi, la somme de contrôle de l’ensemble du contenu aura des résultats différents pour le +même rapport.

  • +
+
+
+
+

Polling

+

Pour implémenter un sondage personnalisé, déclarez-le en tant que sous-classe de Sondage +(sarracenia.flowcb.poll.Poll), et seulement la routine nécessaire (dans ce cas +l’analyse html « handle_data ») doit être écrite pour remplacer le comportement fourni +par la classe parente.

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/__init__.py )

+

Le plugin a une routine principale « parse », qui appelle la classe html.parser, dans laquelle +le data_handler est appelé pour chaque ligne, construisant progressivement les self.entries +dictionnaire où chaque entrée à une structure SFTPAttributes décrivant un fichier en cours d’interrogation.

+

Donc, le travail dans handle_data est juste de remplir une structure paramiko.SFTPAttributes. +Étant donné que le site Web ne fournit pas réellement de métadonnées, il est simplement rempli avec des données raisonnables +par défaut, qui fournissent suffisamment d’informations pour créer un message et l’exécuter au travers de la +suppression des doublons.

+

Voici le rappel complet du poll:

+
import logging
+import paramiko
+import sarracenia
+from sarracenia import nowflt, timestr2flt
+from sarracenia.flowcb.poll import Poll
+
+logger = logging.getLogger(__name__)
+
+class Nasa_mls_nrt(Poll):
+
+    def handle_data(self, data):
+
+        st = paramiko.SFTPAttributes()
+        st.st_mtime = 0
+        st.st_mode = 0o775
+        st.filename = data
+
+        if 'MLS-Aura' in data:
+               logger.debug("data %s" %data)
+               self.entries[data]=st
+
+               logger.info("(%s) = %s" % (self.myfname,st))
+        if self.myfname == None : return
+        if self.myfname == data : return
+
+
+

Le fichier est ici:

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/flowcb/poll/nasa_mls_nrt.py )

+

et le fichier de configuration correspondant fourni ici :

+

( https://github.com/MetPX/sarracenia/blob/development/sarracenia/examples/poll/nasa-mls-nrt.conf )

+
+
+

Accès aux messages à partir de Python

+

Jusqu’à présent, nous avons présenté des méthodes d’écriture de personnalisations de traitement Sarracenia, +où l’on écrit des extensions, via des rappels ou une extension +pour modifier ce que font les instances de flux de sarracénia.

+

Certains peuvent ne pas vouloir utiliser le langage de Sarracenia et des configurations. +Ils peuvent avoir du code existant, à partir duquel ils veulent appeler une sorte de code d’ingestion de données. +On peut appeler des fonctions liées à sarracenia directement à partir de programmes python existants.

+

Pour l’instant, il est préférable de consulter le Tutoriels inclus avec Sarracenia, +qui ont quelques exemples d’une telle utilisation.

+
+

Note

+

FIXME, lien vers amqplib ou liaisons java, et pointeur vers les pages de manuel sr3_post et sr_report section 7.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/StrategieDetectionFichiers.html b/fr/Explication/StrategieDetectionFichiers.html new file mode 100644 index 000000000..3a095f7c1 --- /dev/null +++ b/fr/Explication/StrategieDetectionFichiers.html @@ -0,0 +1,572 @@ + + + + + + + File Detection Strategies — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

File Detection Strategies

+

Le travail fondamental de watch est de remarquer quand les fichiers sont +disponibles pour être transférés. La stratégie appropriée varie en fonction de:

+
+
    +
  • le nombre de fichiers de l’arbre à surveiller,

  • +
  • le délai minimum pour signaler les changements aux fichiers qui est acceptable, et

  • +
  • la taille de chaque fichier dans l’arbre.

  • +
+
+

L’arbre le plus facile à surveiller est le plus petit Avec un seul répertoire à surveiller où l’on +affiche un message pour un composant sr_sarra, alors l’utilisation de l’option delete gardera en tout temps +le nombre minimale de fichiers dans le répertoire et minimisera le temps de remarquer les nouveaux. Dans ces +conditions optimales, l’observation des fichiers dans un centième de seconde, c’est raisonnable +de s’y attendre. N’importe quelle méthode fonctionnera bien pour de tels arbres, mais… les charge imposé +sur l´ordinateur par la méthode par défaut de watch (inotify) sont généralement les plus basses.

+

Lorsque l’arborescence devient grande, la décision peut changer en fonction d’un certain nombre de facteurs, +décrit dans le tableau suivant. Il décrit les approches qui seront les plus basses en +latence et en débit le plus élevé d’abord, et éventuellement l’option la moins efficace +qui cause le plus de retard par détection.

+
+

Tableau de stratégie de détection de fichiers

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Stratégies de détection de fichiers (ordre : de la plus rapide à la plus lente) +Le Méthodes plus rapides marchent sur les plus grands arborescences.

Méthode

Description

Application

Implicite +publier +avec biblio +thque de cale

+

(LD_PRELOAD)

+

(en C)

+

Livraison de fichiers annoncée par +libsrshim

+
+
    +
  • nécessite le paquet C.

  • +
  • export LD_PRELOAD=libsrshim.so.1

  • +
  • usage accru de reject

  • +
  • fonctionne sur n´importe quelle +taille d´arbre de fichiers.

  • +
  • très multi-tâches.

  • +
  • E/S par origine (plus efficace)

  • +
+
+

Beaucoups de travaux d´utilisateur qui +ne peuvent pas être modifié afin de +publier explicitement.

+
+
    +
  • arbres de millions de fichiers.

  • +
  • efficacité maximale.

  • +
  • complexité maximale.

  • +
  • ou python3 n´est pas disponible.

  • +
  • pas de sr_watch.

  • +
  • pas de plugins.

  • +
+
+

Publication +explicite par +clients

+

C: sr_cpost +où +Python: +sr_post

+

Publications d´avis via +sr_post(1) +où d´autres composants sr_ +une fois écriture complété.

+
+
    +
  • publieur fait la somme de contrôle

  • +
  • Moins de aller-retouers

  • +
  • un peu plu len que le bibliothèque

  • +
  • pas de balayage de répertoire.

  • +
  • très multi-tâches.

  • +
+
+

l´usager publie quand il a fini d´ +écrire le fichier.

+
+
    +
  • contrôle plus fine.

  • +
  • d´habitude meilleur.

  • +
  • meilleur approche que sr_watch.

  • +
  • L´usager doit publier explicitement +dans ces scripts/jobs.

  • +
+
+

sr_cpost

+

(en C)

+

fonctionne comme watch si sleep > 0

+
+
    +
  • plus vite que sr_watch.

  • +
  • utilise moins de mémoire vive que +watch

  • +
  • peut marcher avec des arbres +plus grand que watch

  • +
+
+
    +
  • ou python3 est dure a avoir.

  • +
  • ou la vitesse est important.

  • +
  • ou on n´a pas besoin de plugins.

  • +
  • limité sues with tree size +as watch, just a little later. +(see following methods)

  • +
+

sr_watch avec +reject +.*.tmp$ +(suffix)

+
+

(defaut)

+
+

(en Python)

+

Fichier transférés avec .tmp suffixe. +lorsque complete, renommé pour enlevé +suffix. Suffix est programmable.

+
+
    +
  • require aller-retour pour renommage +(un peu plus lent)

  • +
  • on peu présumer 1500 fichier/second

  • +
  • gros arborescences auras de delais +au démarrage

  • +
  • chaque noeud dans un grappe a besoin +de tourner un instance.

  • +
  • chaque watch est une seul tâche.

  • +
+
+

Réception de livraisons d´autres +systèmes ( .tmp étant standard) +Pour recevoir de Sundew.

+

Meilleur choix pour des arbres de +taille modéré sur un seul serveur. +les plugins sont fonctionnent

+

Va bien avec quelques milliers de +fichiers avec seulement quelques +secondes de delai au démarrage.

+

trop lent pour des arbres de millions +fichiers.

+

sr_watch avec +reject +^\..* +(Prefix)

utilisez conventsion linux pour cacher +des fichiers avec un prefix ‘.’

+

compatabilité +performance identique à la méthode +précédente.

+

envoi à des systèmes qui ne tolérent +pas des suffix.

sr_watch avec +inflight +numéro +(mtime)

+
+

+
+

nodupe__fileAgeMin

+

Age minimal (de modification)du fichier +avant qu´il est considéré complet.

+
+
    +
  • rajout ce délai sur chaque transfert

  • +
  • Vulnérable à des pannes réseau.

  • +
  • Vulnérable à des horloges désynchr +onizés

  • +
+
+

Dernier choix, impose un delai fix. +Seulement si aucune autre méthode +marche.

+

Réception de sources non-coopératives

+

(choix valable avec PDS)

+

Si un processus re-écrit un fichier +souvent, mtime peut servire à réduire +le rhythme de publication d´avis.

+

force_polling +avec reject +où mtime

Tel que les 3 méthodes précedentes +mais en se servant de listings de +répertoires

+
+
    +
  • Gros arbres plus lents

  • +
  • le plus compatbile (marchera +n´importe où)

  • +
+
+

Seulement quand INOTIFY ne marche pas +Comme dans une grappe multi-noeud.

+

Nécessaire sur des systèmes avec +NFS sure plusieurs noeuds qui écrivent +en parallèle.

+
+

sr_watch est sr3_post avec l’option sleep qui lui permettra de boucler les répertoires donnés en arguments. +sr_cpost est une version C qui fonctionne de manière identique, sauf qu’elle est plus rapide et +utilise beaucoup moins de mémoire, à l’adresse le coût de la perte du support des plugins. Avec +sr_watch (et sr_cpost) La méthode par défaut de la remarque les changements dans les répertoires +utilisent des mécanismes spécifiques au système d’exploitation (sous Linux : INOTIFY) +pour reconnaître les modifications sans avoir à analyser manuellement l’arborescence complète des répertoires. +Une fois amorcés, les changements de fichiers sont remarqués instantanément, mais nécessitent +une première marche à travers l’arbre, une passe d’amorçage.

+

Par exemple, supposons qu’un serveur peut examiner 1500 fichiers/seconde. Si un arbre de taille +moyenne est de 30 000 fichiers, alors il faudra 20 secondes pour une passe d’amorçage**. En utilisant +la méthode la plus rapide disponible, on doit supposer qu’au démarrage d’une telle arborescence de répertoires, +il faudra environ 20 secondes avant qu’elle ne démarre de façon fiable. L’affichage de tous les fichiers +dans l’arborescence. Après cette analyse initiale, les fichiers sont remarqués avec une latence inférieure à la seconde. +Donc un sommeil de 0.1 (vérifiez les changements de fichiers toutes les dixièmes de seconde) +est raisonnable, à condition que nous acceptions l’amorçage initial. Si l’on choisit +l’option force_polling, alors ce délai de 20 secondes est encouru pour chaque passe de balayage, +plus le temps nécessaire pour effectuer l’affichage lui-même. Pour le même arbre, un réglage sleep de +30 secondes serait le minimum à recommander. Attendez-vous à ce que les fichiers seront remarqués +environ 1,5*, les paramètres sleep en moyenne. Dans cet exemple, environ 45 secondes. Certains seront +ramassés plus tôt, d’autres plus tard. A part les cas spéciaux où la méthode par défaut manque de +fichiers, force_polling est beaucoup plus lente sur des arbres de taille moyenne que la méthode par +défaut et ne devrait pas être utilisé si la rapidité d’exécution est une préoccupation.

+

Dans les clusters de supercalculateurs, des systèmes de fichiers distribués sont utilisés, et les +méthodes optimisées pour le système d’exploitation les modifications de fichiers (INOTIFY sous Linux) +ne franchissent pas les limites des nœuds. Pour utiliser watch avec la stratégie par défaut +sur un répertoire dans un cluster de calcul, on doit généralement avoir un processus sr_watch +sr_watch s’exécutant sur chaque noeud. Si cela n’est pas souhaitable, alors on peut le déployer sur +un seul nœud avec force_polling mais le timing sera le suivant être limité par la taille du répertoire.

+

Au fur et à mesure que l’arbre surveillé prend de l’ampleur, la latence au démarrage de sr_watch´s +augmente, et si le sondage ( force_polling ) est utilisé, la latence à la modification des fichiers d’avis augmentera +également. Par exemple, avec un arbre avec 1 million de fichiers, il faut s’attendre, au mieux, à +une latence de démarrage de 11 minutes. S’il s’agit d’un sondage, alors une attente raisonnable +du temps qu’il faut pour remarquer les nouveaux fichiers serait de l’ordre de 16 minutes.

+

Si la performance ci-dessus n’est pas suffisante, alors il faut considérer l’utilisation de la +librairie de cales ( shim library ) à la place de sr_watch. Tout d’abord, il faut installer la version C de Sarracenia, +et en suite rajouter à l’environnement pour tous les processus qui vont écrire des fichiers à publier +pour l’appeler:

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

shimpost.conf est un fichier de configuration sr_cpost dans le répertoire ~/.config/sarra/post/. +Un sr_cpost est le même que celui de sr_post, sauf que les plugins ne sont pas supportés. Avec la +librairie en place, chaque fois qu’un fichier est écrit, les clauses accept/reject du fichier +shimpost.conf sont les suivantes consulté, et s’il est accepté, le fichier est publié tel qu’il le serait par sr_watch.

+

Jusqu’à présent, la discussion a porté sur le temps nécessaire pour remarquer qu’un fichier +a changé. Un autre facteur à prendre en considération est le temps d’afficher les fichiers une +fois qu’ils ont été remarqués. Il y a des compromis basés sur l’algorithme de checksum choisi. +Le choix le plus robuste est le choix par défaut : s ou SHA-512. Lorsque vous utilisez la +méthode de la somme s, l’ensemble du fichier sera lue afin de calculer sa somme de contrôle, +ce qui est susceptible de déterminer le temps jusqu’à l’affichage. la somme de contrôle sera +utilisé par les consommateurs en aval pour déterminer si le fichier annoncé est nouveau ou s’il +s’agit d’un fichier qui a déjà été vu, et c’est vraiment pratique.

+

Pour les fichiers plus petits, le temps de calcul de la somme de contrôle est négligeable, mais +il est généralement vrai que les fichiers plus volumineux Lorsque en utilisant la méthode shim library, +le processus qui a écrit le fichier est le même que celui qui a écrit le fichier. En calculant la somme de contrôle, +la probabilité que les données du fichier se trouvent dans un cache +accessible localement est assez élevée, de sorte qu’il est aussi peu coûteux que possible**. +Il convient également de noter que la commande sr_watch/sr_cpost Les processus de surveillance +des répertoires sont à thread unique, alors que lorsque les jobs utilisateur appellent sr_post, +ou utilisent le shim. il peut y avoir autant de processus d’affichage de fichiers qu’il y a +de rédacteurs de fichiers.

+

Pour raccourcir les temps d’enregistrement, on peut sélectionner des algorithmes sum qui ne +lisent pas la totalité de l’enregistrement comme N (SHA-512 du nom du fichier seulement), mais +on perd alors la capacité de différenciation entre les versions du fichier.

+
+
note ::

devrait penser à utiliser N sur sr_watch, et à faire recalculer les sommes de contrôle par des pelles multi-instance. +pour que cette pièce devienne facilement parallélisable. Devrait être simple, mais pas encore exploré. +à la suite de l’utilisation de la bibliothèque de cales. FIXME.

+
+
+

Une dernière considération est que dans de nombreux cas, d’autres processus sont en train +d’écrire des fichiers dans des répertoires surveillés par sr_watch. Le fait de ne pas établir +correctement les protocoles de complétion de fichiers est une source commune de +problèmes intermittents et difficiles à diagnostiquer en matière de transfert de fichiers. +Pour des transferts de fichiers fiables, Il est essentiel que les processus qui écrivent +des fichiers et watch s’entendent sur la façon de représenter un fichier qui n’est pas complet.

+
+
+

SHIM LIBRARY USAGE

+

Rather than invoking a sr3_post to post each file to publish, one can have processes automatically +post the files they right by having them use a shim library intercepting certain file i/o calls to libc +and the kernel. To activate the shim library, in the shell environment add:

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

where shimpost.conf is an sr_cpost configuration file in +the ~/.config/sarra/post/ directory. An sr_cpost configuration file is the same +as an sr3_post one, except that plugins are not supported. With the shim +library in place, whenever a file is written, the accept/reject clauses of +the shimpost.conf file are consulted, and if accepted, the file is posted just +as it would be by sr3_post. If using with ssh, where one wants files which are +scp’d to be posted, one needs to include the activation in the .bashrc and pass +it the configuration to use:

+
expoert LC_SRSHIM=shimpost.conf
+
+
+

Then in the ~/.bashrc on the server running the remote command:

+
if [ "$LC_SRSHIM" ]; then
+    export SR_POST_CONFIG=$LC_SRSHIM
+    export LD_PRELOAD="libsrshim.so.1"
+fi
+
+
+

SSH will only pass environment variables that start with LC_ (locale) so to get it +passed with minimal effort, we use that prefix.

+
+

Shim Usage Notes

+

Cette méthode de notification nécessite une certaine configuration de l’environnement de l’utilisateur. +L’environnement de l’utilisateur doit être défini sur les variables d’environnement LD_PRELOAD +avant le lancement du processus. Il restent encore des complications qui restent qui ont été +testé pendant les deux dernières années depuis que la library shim a été implémenté :

+
    +
  • si nous voulons remarquer les fichiers créés par des processus scp distants (qui créent des shells sans connexion), +alors le hook d’environnement doit être dans .bashrc. et il faut utiliser une variable d’environnement +qui commence par LC_ pour que ssh transmette la valeur de la configuration sans +avoir à modifier la configuration sshd dans les distributions Linux typiques. +( discussion complète: https://github.com/MetPX/sarrac/issues/66 )

  • +
  • un code qui présente certaines faiblesses, comme dans FORTRAN un manque de NONE IMPLICITE +https://github.com/MetPX/sarracenia/issues/69 peut se bloquer lorsque la bibliothèque shim +est introduite. La correction nécessaire dans ces cas, jusqu’à présent, consiste à corriger +l’application, et non la librarie. +( aussi: https://github.com/MetPX/sarrac/issues/12 )

  • +
  • les codes qui utilisent l’appel exec à tcl/tk, considère par défaut que toute +sortie vers le descripteur de fichier 2 (type d’erreur) est une condition d’erreur. +Ces messages peuvent être étiquetés comme INFO, ou priorité d’AVERTISSEMENT, mais ca va causer +l’appelant tcl à indiquer qu’une erreur irrécupérable s’est produite. Additionnant +-ignorestderr aux invocations de exec évite de tels avortements injustifiés.

  • +
  • Les scripts shell complexes peuvent avoir un impact démesuré sur les performances. +Puisque les scripts shell de haute performance est un oxymore, la meilleure solution +en termes de performance, est de réécrire les scripts avec un langage de scripting plus efficace +tel que python ( https://github.com/MetPX/sarrac/issues/15 )

  • +
  • Des bases de code qui déplacent des hiérarchies de fichiers volumineux (par exemple, mv tree_with_thousands_of_files new_tree ) +aura un coût beaucoup plus élevé pour cette opération, car elle est mise en œuvre en tant qu’un +changement de nom de chaque fichier de l’arborescence, plutôt qu’une seule opération sur la racine. +Ceci est actuellement considéré comme nécessaire car la correspondance de modèle d’acceptation/rejet +peut entraîner un arbre très différent sur la destination, plutôt que simplement le +même arbre en miroir. Voir ci-dessous pour plus de détails.

  • +
  • export SR_SHIMDEBUG=1 obtiendra plus de sortie que vous ne le souhaitez. utiliser avec précaution.

  • +
+
+
+

Processus de Renommage

+

C’est à noter que le changement de nom de fichier n’est pas aussi simple dans le cas de mise en miroir que dans le cas sous-jacent +du système d’exploitation. Alors que l’opération est une seule opération atomique dans un système d’exploitation, +avec l’aide de notifications, il existe des cas d’acceptation/rejet qui créent quatre effets possibles.

+ + + + + + + + + + + + + + + + + + +

L’ancien nom est:

Nouveau nom est

Accepted

Rejected

Accepted

renomme

copie

Rejected

supprime

rien

+

Lorsqu’un fichier est déplacé, deux notifications sont créées :

+
    +
  • Une notification a le nouveau nom dans le relpath, tout en gardant un champ oldname +qui pointe vers l’ancien nom. Cela déclenchera des activités dans la première moitié de +la table, soit un renommage, à l’aide du champ oldname, soit une copie si elle n’est pas présente à +la destination.

  • +
  • Une deuxième notification avec l’ancien nom dans relpath qui sera acceptée +encore une fois, mais cette fois, il y a le champ newname et traite l’action de suppression.

  • +
+

Alors que le renommage d’un répertoire à la racine d’un grand arbre est une opération atomique et peu cher +dans Linux/Unix, la mise en miroir de cette opération nécessite la création d’une publication de renommage pour chaque fichier +dans l’arbre, et est donc beaucoup plus cher.

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/SupprimerLesDoublons.html b/fr/Explication/SupprimerLesDoublons.html new file mode 100644 index 000000000..ff2908852 --- /dev/null +++ b/fr/Explication/SupprimerLesDoublons.html @@ -0,0 +1,332 @@ + + + + + + + Suppression de Doublons — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Suppression de Doublons

+

Lors qu’on achemine les données à travers de réseaux, il faut se préoccuper des “orages” de données +causé par des boucles de transfert (un serveur A envoit à B, qui renvoit à C, qui renvoit à A. Si A +ne sait pas qu´il l´a déjà vu, on a un boucle infini, qui peut etre décrit comme un orage le volume +est dérangeant.)

+

Un attribut commun des flots de données critiques est d´avoir plusieurs serveurs qui produisent ces +données. Si la stratégie de disponibilité requiert que les deux serveurs soient actifs en même temps, +on a besoin de supprimer la production de la deuxième source. le potentiel pour des boucles, ou +des transmissions redondantes de données est haut.

+

Il s´avère supprimer les duplicat d´envoi a plein de cas d´usage et qu´un seule méthode ne suffit +pas, alors sr3 permet la modification de la méthode implanté dans le module sarracenia.flowcb.nodupe +pour augmenter la fléxibilité.

+

La supression de doublons:

+
* crée un clé à partir d´un message d´annonce.
+* crée un chemin (path) à partir d´un message d´annonce.
+
+
+

Quand un message est à propos d´un message jugé un doublons, on cesse de le traiter.

+

la clé d´un message d´annonce est préférablement simplement la somme de contrôle +du champs Identity. Si la source de données ne fournit pas de champs Identity, +on se fie sur d´autres champes dans le message: mtime, size, pubTime. +Le champs pubTime étant mandatoire assure qu´un clé peut toujour être généré pour +chaque message, mais des fois peut être inefficace.

+

On peut allumer la suppresion de doublons, ou bien le supprimer avec la +ligne suivante dans le fichier de configuraation:

+
nodupe_ttl 300|off|on
+
+
+

(les | indique les choix… alors soit nodupe_ttl 300 où nodup_ttl on, où nodupe_ttl off ) +quand on y fournit un chiffre c´est l´intervalle, en secondes, dont on se souvient +d´un messages.

+
+

Standard (basé sur le chemin et contenu)

+

méthode: Quand les produits on le même clé et chemin, ils sont des doublons.

+

Deux serveur peuvent envoyer le même produit, aven le même relPath à un serveur plus +loin.

+
+
+

Data (basé uniquement sur le contenu)

+

method: Quand les produits on le même clé, ils sont des doublons.

+

dans le ficher de configuration on devrait voir:

+
nodupe_basis data
+
+
+

ou bien:

+
flowcb_prepend sarracenia.flowcb.nodupe.data.Data
+
+
+

remplace la génération de clé de suppression des doublons standard pour inclure uniquement +la somme de contrôle du contenu du fichier. Ce module ajuste le champ path (chemin) utilisé par +le champ standard de suppression des doublons. La directive flowcb_prepend assure qu’elle +est appelée avant la traitement de doublons integrée.

+

Le produits identiques ont le même somme de contrôle. à utiliser lorsque deux sources +génèrent le même produit.

+
+
+

Name (basé uniquement sur le nom)

+

method: Quand les fichiers ont le même nom, ils sont identiques.

+

dans le fichier de configuration, soit:

+
nodupe_basis data
+
+
+

ou bien:

+
flowcb_prepend sarracenia.flowcb.nodupe.name.Name
+
+
+

remplace la générations de clé de suppression de doublons en utilisant uniquement +le nom de fichier.

+

Quand plusieurs source génèrent un produit, mais les résultats ne sont pas identique +au niveau binaire, il faut une autre approche.

+
+

URP

+

Dans le cas des systèmes de traitement de données RADAR, il y 6 serveurs qui génèrent +les mêmes produits (bien qu´il ne soient pas exactement le même séquence d´octets.) +Les produit sont les même parce qu´ils sont dérivé de les même données en amont, +ont suivi une traitement semblable, mais les détails de traitement font en sorte +que les résultats ne sont pas identiques au niveau binaire.

+

Les serveur sont identifié commes sources distinctes, alors le chemin d´acces relatif +(relPath) diffèrent. Mais les noms de fichier pour les produits sont les mêmes, +alors on peut utiliser le nom dans ce cas.

+
+
+
+

Le fichiers trop changeant (mdelaylatest)

+

method: retard les fichiers x secondes pour assurer qu´il n´y a pas de nouvelle version.

+

NB: +Il s’agit d’un filtre supplémentaire à la suppression des doublons, et ce qui précède +méthodes peuvent être utilisées conjointement avec mdelaylatest. ce filtre est idéalement +appliqué avant la suppression des doublons pour réduire la taille de la base de données.

+

Dans le fichier de configuration, soit:

+
mdelay 30
+flowcb_prepend sarracenia.flowcb.mdelaylatest.MDelayLatest
+
+
+

va retarder la distribution des messages d´annonce par 30 secondes. +Si plusieurs versions sont produites avant que le fichier a 30 secondes d´age, +on envoie uniquement la dernière version lorsqu´il aura 30 secondes d´age.

+
+

cas d´usage

+

Dans certains cas, certaines sources de données re-écrivent très souvent les fichiers. +Si les fichiers sont volumineux (la copie prend beaucoup de temps) ou s’il y a une fil d’attente (l’abonné +a un certain temps de retard sur le producteur.), un algorithme pourrait écraser un fichier, ou +y ajouter trois ou quatre fois avant d’avoir une version “finale” qui durera un certain temps.

+

si le réseau a un délai de propagation supérieur à la période de remplacement, alors par +le moment où le consommateur demande le fichier, il sera différent (provoquant potentiellement +des incompatibilités de somme de contrôle.) ou, si c’est assez rapide, copier un fichier qui +ne durera pas plus de quelques secondes pourrait être un gaspillage de bande passante et de traitement.

+
+
+

les citypages du site météo.gc.ca

+

( https://hpfx.collab.science.gc.ca/YYYYMMDD/WXO-DD/citypage_weather/ )

+

là, les citypages sont un produit composé (dérivé de nombreux produits en amont distincts.) +le script qui crée les produits citypage semble écrire un en-tête, puis un enregistrement, +puis à la toute fin, une bande-annonce. il y a eu de nombreux cas de transmission de fichiers +comme xml invalide parce que la bande-annonce était manquante. Il faut attendre que le script ait +fini d’écrire le fichier

+
+
+

Miroitage CHP

+

Dans la miroitage à grande vitesse des données entre des grappes de calcul haute performance, les programmes +passent souvent du temps à ajouter des enregistrements aux fichiers, peut-être des centaines de fois par seconde. +Une fois le script terminé, le fichier devient en lecture seule pour les consommateurs. Ce n’est pas utile +transmettre ces valeurs intermédiaires. Un fichier de 100 octets surveillé à l’aide de la bibliothèque shim +ou un sr_watch, pourrait être modifié des centaines de fois, provoquant une copie pour chaque modification potentiellement +déclenchant des centaines d’exemplaires. Il vaut mieux attendre la fin du processus de mise à jour, +pour que le fichier soit inactif, avant de poster un message d´annonce.

+
+
+
+

Fichiers trop vieux

+

method: les messages à propos des fichiers trop vieux sont supprimés.

+

dans les fichiers de configuration:

+
fileAgeMax 600
+
+
+

Les messages notificationsfichiers pour des fichiers qui sont agés de plus que 600 secondes (10 minutes) seront +supprimés.

+

Ceci est généralement utilisé avec des sondages (poll) qui ont des répertoires de très longue durée. +Exemple : un serveur distant dispose d’une base de données permanente de fichiers distants. ca ne sert à rien +de reexaminer de fichiers vieux de deux ans.

+

Il arrive souvent que nodupe_ttl soit supérieur à nodupe_fileAgeMax pour éviter +les fichiers soient oubliés par la cache avant d’être considérés comme “trop vieux” et ensuite d’être +(à tort) ré-ingéré. Un message d’avertissement est émis si c’est le cas dans un sondage +au démarrage.

+
+
+

A votre gout!

+

Dans le fichier de configuration:

+
vos_parametres
+flowcb_prepend votre_class.VotreClass
+
+
+

Si aucune des méthodes intégrées de suppression des doublons ne fonctionne pour votre cas d’utilisation, vous pouvez +sous-classe sarracenia.flowcb.nodupe et dériver les clés d’une manière différente. Voir le +les classes sarracenia.flowcb.nodupe.name et sarracenia.flowcb.nodupe.data pour des exemples de +comment faire.

+

On peut également implémenter un filtre qui définit le champ nodupe_override dans le message

+
msg['nodupe_override] = { 'key': votre_clé, 'path': votre_chemin }
+
+
+

et la méthode standard de suppression des doublons utilisera la clé et la valeur fournies. +Il existe également une fonction d’assistance disponible dans la classe nodupe

+
def deriveKey(self, msg) --> str
+
+
+

qui examinera les champs du message et dérivera la clé normale qui sera +généré pour un message, que vous pouvez ensuite modifier si vous ne recherchez qu’un petit changement.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/index.html b/fr/Explication/index.html new file mode 100644 index 000000000..27e94da5c --- /dev/null +++ b/fr/Explication/index.html @@ -0,0 +1,261 @@ + + + + + + + Explication — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Explication

+
+

Contenu:

+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Explication/sftps.html b/fr/Explication/sftps.html new file mode 100644 index 000000000..7bc55d6c1 --- /dev/null +++ b/fr/Explication/sftps.html @@ -0,0 +1,521 @@ + + + + + + + Pourquoi SFTP est plus souvent choisi que FTPS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Pourquoi SFTP est plus souvent choisi que FTPS

+

Il y a eu des discussions sur l’endroit où placer un S afin de +transferts de fichiers sécurisés. Parmi une variété de technologies concurrentes, il y en +a deux qui peuvent sembler assez équivalents : SFTP et FTPS. Les deux +technologies sont des candidates pour remplacer la vénérable méthode FTP +de transfert de fichiers.

+

Il est pertinent d’examiner les raisons pour lesquelles SFTP a été choisi à la place de FTPS. +Dans cette discussion, veuillez garder à l’esprit :

+
    +
  • Je ne suis pas un expert en sécurité, cela reflète simplement ma compréhension actuelle

  • +
  • Nous n’avons pas déployé FTPS, nous avons donc peu d’expérience avec cela.

  • +
+

S’il y a des malentendus, alors nous serions heureux d’apprendre +à leur sujet.

+
+

FTP

+

Dans les premiers jours d’Internet, avant même l’avènement +du World Wide Web, l’une des premières applications était un +transfert de fichiers, tel que défini pour la première fois par Internet Engineering Task +Force standard Request for Comments (IETF RFC 114 d’origine, +datant de 1971 ). Il a évolué au cours des décennies suivantes avec +les mêmes idées de base. Sur Internet, tous les appareils +ont une adresse internet, et pour chaque adresse il y a +de l’ordre de 65 000 ports disponibles (type de sous-adresses).

+

Si Alice veut communiquer avec Bob, elle initie +une connexion à l’appareil de Bob sur le port 21. Bob indique qu’il +est prêt à recevoir le dossier, puis ils se mettent d’accord sur une seconde +connexion établie sur laquelle les données du fichier sont réellement établies. +La façon dont la deuxième connexion est établie a beaucoup varié +au fil du temps. Les informations pour configurer le chemin de données naturellement +passe sur le chemin de contrôle précédemment configuré.

+

À cette époque, Alice et Bob avaient deux serveurs. +directement sur internet. Internet était un lieu collégial, et +les firewall n’existaient pas. Trente ans plus tard, et la réalité est +tout à fait différent.

+
+
+

Échangeurs de produits : prise en charge d’un grand nombre de transferts

+

Dans les temps modernes, si l’on transfère des centaines de milliers ou des millions +de fichiers, il y aura probablement un grand nombre d’appareils de réseau +entre les serveurs d’Alice et bob. Le diagramme suivant est un diagramme typique +d’un cas simple que l’on est susceptible de rencontrer dans la réalité:

+../../_images/ftp_proxy_today.svg

Pour envoyer un grand nombre de fichiers et résister aux défaillances de +serveurs individuels, il y aura généralement un tableau de serveurs +pour envoyer des fichiers. Appelons-les un tableau d’échange de produits. +Le but de ces tableaux de serveurs est de traiter et/ou +transférer plus de données que ce qui pourrait être transféré dans un temps donné +par un seul serveur, et pour fournir une redondance transparente +pour les défaillances de composants.

+

Pour faire paraître aux étrangers qu’un serveur est unique, il y aura +un équilibreur de charge devant les vrais serveurs. Lors de la +connexion initiale ou la réception de connexions entrantes, l’équilibreur de charge +mappera les adresses réelles des serveurs réels à un serveur public. +L’équilibreur de charge maintient une table des personnes connectées +à qui, de sorte que lorsqu’un paquet est reçu d’un serveur externe, +peut le mapper au bon serveur réel. +C’est ce qu’on appelle la traduction d’adresses réseau, ou NAT.

+

Rappelons qu’il y a 65 000 ports disponibles, donc plusieurs serveurs +pourraient choisir, à partir de leurs adresses réelles, d’utiliser la même source +pour initier leur connexion. Lorsque l’équilibreur de charge +mappe la connexion à une adresse visible publiquement, vous pouvez +avoir plusieurs serveurs revendiquant le même port de source. +L’équilibreur de charge doit donc mapper les ports ainsi que les adresses. +C’est ce qu’on appelle la traduction d’adresse de port, ou PAT.

+

Il y a donc une table stockée d’une manière ou d’une autre (sur les appareils haut de gamme) +dans la mémoire adressable de contenu, ou CAM)

+ + + + + + +

10.2.3.4:67

134.67.7.8:45

+

Qui maintient le mappage d’une adresse et d’un port à une adresse et un port public +pour chaque connexion traversant un appareil donné.

+

Lorsque nous arrivons à un firewall, en particulier lorsque les organisations utilisent un +adressage privé, le processus est répété. Donc l’adresse +et le port peuvent être à nouveau re-mappé à des différents.

+

Les mêmes opérations se produisent à l’autre extrémité, avec le firewall +et l’équilibreur de charge qui gère les tables des personnes connectées +à quoi, et mappe des adresses et des numéros de port dans l’ordre +pour permettre aux serveurs à chaque extrémité de communiquer.

+

Avec un protocole simple, comme Secure Shell ou Hypertext +Protocole de transfert, il n’y a qu’un seul canal et une seul +entrée de table dans chacun de ces périphériques qui est nécessaire par connexion. +La complexité algorithmique de telles opérations est constante et minime, +et petite en termes de mémoire et de processeur.

+

Rappelez-vous que le chemin de contrôle indique la méthode par laquelle la connexion de données est établie. +Afin que le canal de données FTP connecte les deux serveurs, chaque firewall ou équilibreur de charge entre les deux serveurs +observe le chemin de contrôle, recherche la spécification du port de données, +et crée de manière proactive une cartographie pour cela, créant une deuxième paire de relations +à gérer dans le tableau pour chaque connexion. +Cette activité est appelée proxy FTP. +Un proxy doit être exécuté à n’importe quel point où la traduction du réseau ou du port se produit, avec au moins le double de la surcharge de mémoire (encore petite) +que d’autres protocoles, mais le travail du processeur est à peu près le même (il suffit d’utiliser +deux entrées dans les tables de mappage au lieu d’une). De manière optimale, une sorte de +proxy ou observateur sur les firewall est capable d’ajuster les règles de firewall +dynamiquement pour n’autoriser que des connexions de données spécifiques en cours d’utilisation +quand ils en ont besoin, minimisant l’exposition à internet pour ne doubler que +celle des autres protocoles.

+

En pratique, il peut y avoir de nombreux firewall NAT/PAT entre A +et B, et ni Alice ni Bob ne seront en charge de la configuration +de tous les dispositifs intervenants. Parce que le chemin d’accès du contrôle FTP +est facilement inspecté, il y a juste un peu de code proxy couramment +mis en œuvre sur les périphériques réseau pour couvrir ce cas particulier.

+
+
+

FTPS

+

FTPS - est FTP avec la sécurité de niveau de transport ajoutée. +Le chemin de contrôle est maintenant chiffré et n’est donc pas disponible +aux proxys sur chacun des firewall et équilibreurs de charge (tels que +LVS (serveur virtuel linux), ou produits standard de F5, barracuda ou +d’autres). La réponse simple à ce problème est de +mettre fin au chiffrement de chaque firewall et équilibreur de charge, de sorte que +le trafic de contrôle peut être visualisé pour obtenir les numéros de port nécessaires, +puis rechiffré. Essentiellement, il faudrait décrypter et +rechiffrer les informations de contrôle au moins quatre fois entre les serveurs source et destination.

+

Idéalement, on ne déchiffrerait que le chemin de contrôle, le routeur intercepterait la +demande de connexion et renvoie une réponse de chiffrement local. Mais si le chemin de données +n’est pas également intercepté, alors le serveur aura une clé de chiffrement pour le contrôle +et une autre pour le chemin de données, qui cassera TLS. Donc, très probablement, +chaque routeur et firewall intervenant est obligé de déchiffrer et de rechiffrer +toutes les données qui passent aussi.

+

Essentiellement, cela signifie qu’Alice et Bob acceptent que chaque +périphérique NAT / PAT entre eux est autorisé à usurper l’identité de l’autre +entité (effectuant ce que l’on appelle une attaque de l’homme du milieu). +C’est la seule façon de traverser une longue chaîne de périphériques NAT/PAT.

+

Comme ce qui précède est assez onéreux, ma supposition est que ceux qui déploient FTPS +ne corrige pas le proxy comme décrit ci-dessus. Au lieu de cela, ils +réservent une plage de ports pour ces types de trafic et mettent un PEU DE règles NAT/PAT statique +en place, réservant probablement une plage de ports par nœud derrière un équilibreur de charge. +C’est lourd et difficile à gérer, et cela fonctionne pour un niveau profond, mais +il ne se généralise pas. En outre, la réservation de ports augmente la superficie d’une +attaque contre des étrangers, car les ports sont ouverts en permanence, plutôt que d’avoir un +mappage de ports spécifiques à des moments spécifiques, car le firewall +ne peux pas lire le trafic du chemin de contrôle.

+

Dans le cas actif d’origine de FTPS, le client initie le contrôle +et le serveur initie la connexion de données, nécessitant le +firewall du client pour autoriser une connexion entrante arbitraire. Cette méthode +est essentiellement limité au fonctionnement où il n’y a pas de NAT du tout, et +du firewalling extrêmement limité dans les deux sens pour qu’un transfert ait meme lieu.

+

Le cas passif, où le client initie à la fois le contrôle et les données de +connexions, est beaucoup plus commun dans les environnements modernes. Celui-là +complique le NAT/PAT et l’utilisation d’équilibreurs de charge sur le +côté serveur de la destination. Lors de la mise en place d’échanges bidirectionnels, +FTPS complique l’utilisation par les deux parties des équilibreurs de charge ou NAT +et réduit l’efficacité des mesures de firewall disponibles.

+

FTPS est fondamentalement plus difficile à configurer pour de nombreuses configurations courantes. Il faut +construire un cluster différemment, et sans doute pire, parce que les mécanismes standard +utilisés pour d’autres protocoles ne fonctionnent pas. Cela réduit une variété de +choix de configuration disponibles uniquement pour prendre en charge FTPS, avec moins de protection +de que ce qui est offert lors de l’utilisation d’autres protocoles.

+

Enfin, tous les pairs avec lesquels on échange du trafic seront confrontés à la même chose +et auront du mal à le déployer. Il est rare de trouver une pair qui préfère FTPS.

+
+
+

Coût

+

Si l’on suppose que le chemin de contrôle seul peut être intercepté, en laissant +le chemin des données, alors du point de vue de la complexité computationnelle, +le chemin de contrôle, plutôt que de simplement passer des paquets à travers chaque périphérique NAT/PAT +doit être déchiffré et rechiffré, ce qui est probablement encore petit, mais beaucoup plus important +que d’autres protocoles exigent. Cela place une charge plus élevée sur les équilibreurs de charge et les firewall, +qui sont plus complexes à paralléliser et généralement plus chers que +les serveurs à usage général utilisés dans une baie d’échange de produits. Cet effet +est plus prononcé pour les sessions courtes (principalement liées à la connexion +d’établissement, plutôt que de transfert durable).

+

En réalité, il est plus probable que les données doivent également être rechiffrées +comme chemin de contrôle, dans ce cas la capacité de chiffrement de plusieurs +serveurs doivent être égalés au périphérique du réseau pour éviter la formation d’un bottleneck. +Le but d’un tableau d’échange de produits est de distribuer la charge de calcul +sur une variété de serveurs à faible coût. La puissance de traitement dans les +serveurs de commodité est de plusieurs ordres de grandeur au-delà de ce qui est disponible pour +les périphériques réseau. Dans le cas du déchargement du chiffrement, la charge imposée sur +les périphériques réseau sont exponentiellement plus élevés que ce qui est requis pour +d’autres protocoles.

+

Les fournisseurs d’équipements réseau peuvent vanter le déchargement du cryptage, mais ce que cela représente +dans la pratique, est de décharger le travail du processeur à partir d’un éventail de serveurs redondants, +à une grande boîte relativement chère et propriétaire.

+

On peut éviter le coût du cryptage et du décryptage sur les périphériques réseau +en limitant toute la configuration à être placée dans un DMZ sans équilibreur de charge, +ou un équilibreur de charge avec des mappages de ports statiques par serveur réel et une protection de firewall inférieur. +Dans ce cas, le coût de l’équipement n’est probablement pas différent, +mais la charge de maintenance sera légèrement plus lourde (informations d’identification plus fréquentes), +mises à jour, besoin de maintenir des cartes statiques supplémentaires, plus de surveillance de firewall).

+
+
+

Fonctionnalité: Plages d’octets

+

En plus de la complexité beaucoup plus grande de la prise en charge du firewall pour +FTPS, et la charge supplémentaire sur les firewall coûteux, il y a également uen fonctionnalité réduite +disponible par rapport aux protocoles plus récents, +tels que le protocole de transfert SSH et HTTP(S). Les deux sont sécurisés en +utilisant les mêmes algorithmes de chiffrement que FTPS, mais sont à canal unique, +et ils offrent tous deux la possibilité de récupérer des plages d’octets +dans les fichiers. Certaines technologies de transfert plus récentes utilisent des plages d’octets fournies +par HTTP et SFTP pour permettre le transfert de fichiers par plusieurs flux parallèles, +ce qui n’est pas possible avec FTP ou FTPS.

+
+
+

Sécurité/Vulnérabilité

+

FTPS, comme FTP, est généralement authentifié à l’aide de mots de passe secrets. +Si la pair dans un échange subit une intrusion, le hachage du mot de passe +peut être utilisé pour obtenir le mot de passe lui-même en utilisant ce que l’on appelle la méthode brute +de force dans un délai raisonnable en raison de sa complexité limité. +La plupart des mots de passe sont beaucoup plus courts que les clés typiques de +SSH.

+

On est également confronté au problème de l’interception du secret lorsqu’il est partagé. +Ce problème est aggravé par les normes de sécurité modernes qui forcent le +changement de ces secrets à des intervalles fréquentes, augmentant l’opportunité +de l’interception, ainsi que pour imposer une charge de travail supplémentaire au personnel +pour l’entretien.

+

Il existe des configurations où les mots de passe sont autorisés avec SSH/SFTP, mais pour des +applications de transfert de données ces options sont systématiquement désactivées, ce qui +est possible de manière propre et simple. Habituellement, l’échange de données +implique l’échange de clés publiques (aucun secret n’a besoin d’être échangé.) +Les clés publiques sont plus fortes que les mots de passe, et la plupart des organisations de sécurité +permettent des intervalles beaucoup plus longues avant qu’un changement d’informations d’identification ne soit requis.

+

Avec SSH, si un serveur distant est compromis, la partie malveillante +obtient uniquement la clé publique. Comme elle est déjà public, l’attaquant +n’a rien gagné de valeur. Comme les touches sont beaucoup plus longues +qu’un mot de passe, le trafic est plus susceptible d’être sécurisé en transit (bien que +dans la pratique, il existe de nombreux détails qui peuvent rendre ce point discutable.)

+

Selon IETF RFC 2228, les serveurs FTPS peuvent être des serveurs FTP avec plus de +sécurité disponible sur demande explicite, appelée le mode explicite. +Il est donc possible de se connecter à des serveurs FTPS et de transférer en FTP +(mode non sécurisé). Une configuration minutieuse des serveurs est nécessaire pour +s’assurer que cela n’est pas permis par inadvertance.

+

Sur les systèmes récepteurs, il est vrai qu’une configuration OpenSSH par défaut permet +l’accès au niveau du shell, mais l’utilisation restreinte de shells et de prisons chroot est +courante dans les configurations FTP et SFTP. Il n’y a pas de pratique +différente entre FTPS et SFTP du point de vue du compte serveur.

+

En termes de firewall, en supposant que la méthode de mappage de port statique est utilisée, alors +une attaque relativement simple sur un serveur FTPS avec ce type de configuration +serait de DDOS les ports de données. En supposant la capacité de surveiller le trafic à +un point entre les points d’extrémité, un malfaiteur pourrait déterminer la plage de ports +mappé, puis envoyer constamment du trafic vers les ports de données avec des données incorrecte, +ou pour fermer immédiatement la connexion empêchant le transfert de données réel. +Il s’agit d’une surface supplémentaire à défendre par rapport à d’autres protocoles.

+

L’utilisation du deuxième port chiffré, où la plage de ports utilisée est variable +d’un site à l’autre, signifie que la plupart des firewall normaux fonctionnant au niveau TCP +distinguera moins facilement le trafic de transfert de fichiers du trafic Web ou autre +car il n’y a pas de numéro de port spécifique impliqué. Par exemple, notez ce +rapport de bug du point de contrôle qui dit que pour permettre à FTPS de le traverser, +il faut désactiver diverses vérifications:

+
"FTP over SSL is not supported.
+
+Since FTP over SSL is encrypted, there is no way to inspect the port command
+to decide what port to open and therefore the traffic is blocked.  ...
+
+If you still cannot get this traffic through the gateway, there are several
+ways to disable FTP enforcement. Usually this is done through SmartDefense/IPS,
+by disabling the FTP Bounce attack protection.
+Note that this is NOT recommended.  [*]_
+
+
+ +
+
+

Fiabilité/Complexité

+

Il existe plusieurs modes de FTP: ascii / binaire, actif / passif, qui créent plus de cas. +FTPS ajoute plus de cas: explicite / implicite au nombre à autoriser. Le cryptage peut être +activé et désactivé à différents points de chemins de contrôle et de données.

+

Exemple du mode causant une complexité supplémentaire : actif ou passif ? Problème très courant. Oui, la question +peut être répondu en pratique, mais il faut se demander: pourquoi faut-il répondre à cette question? Aucun autre protocole +en a besoin.

+

Exemple de mode causant de la complexité d’il y a dix ans: un serveur FTP commun sur les systèmes Linux est défini par +défaut, ignorez le paramètre ‘ascii’ sur les sessions ftp pour des raisons de performances. Il a fallu beaucoup de temps +pour comprendre pourquoi l’acquisition de données à partir de machines VAX/VMS échouait.

+

L’exigence inhérente pour tous les périphériques NAT/PAT intervenants d’être configurés juste ainsi +pour prendre en charge FTPS le rend, en pratique, moins susceptible d’être fiable. Même dans les cas où +tout est correctement configuré, il y a de la place pour les difficultés. Rappel que pour FTP et FTPS, +les tables doivent être maintenues pour associer les connexions de contrôle et de données aux points de terminaison corrects. +Lorsque les connexions sont fermées, les entrées doivent être arrêtées.

+

Exemple de configuration correcte ayant encore des problèmes: selon notre expérience, très rarement, les tables de mappage se +confondes. Dans le réseau principal canadien d’échange de produits de données météorologiques, parfois, un fichier sur plusieurs millions, +le nom du fichier ne correspondrait pas au contenu du fichier. Bien que ni le nom du fichier, ni le contenu n’aient été corrompus, +l’ensemble de données ne correspondait pas au nom donné au fichier. De nombreuses sources possibles ont été examinées, mais les sources suspectées +de la cause était une sorte de problème de synchronisation avec les ports réutilisés et le mappage sur les équilibreurs de charge, où le +nom de fichier circule sur le chemin de contrôle et les données circulent sur l’autre port. À titre de test, les transferts +ont été migrés vers SFTP et les symptômes ont disparu.

+
+
+

Résumé

+

Soit le proxy FTPS est effectué de manière entièrement générale :

+
    +
  • les dispositifs intervenants doivent effectuer un décryptage de l’homme du milieu +sur au moins le chemin de contrôle, ce qui est tout à fait indésirable d’une +perspective de sécurité. Le déchiffrement du seul chemin de contrôle n’est probablement pas +possible sans casser TLS, de sorte que l’ensemble du flux de données doit +être déchiffré et rechiffré à chaque firewall ou équilibreur de charge.

  • +
  • FTPS nécessite une configuration complexe de tous les périphériques intervenants +qui sont courants dans les configurations modernes. Dans de nombreux cas, +les propriétaires des appareils intervenants refuseront de soutenir la technologie.

  • +
  • FTPS impose une charge de calcul plus élevée à tous les intervenants +que la plupart des alternatives disponibles. En imposant une charge accrue +sur les appareils spécialisés, il est généralement plus coûteux à déployer à grande échelle.

  • +
  • Étant donné que ce qui précède est peu pratique et indésirable, il est rarement fait. +Il existe donc des situations banales où l’on ne peut tout simplement pas déployer +le protocole.

  • +
+

Ou, si seul le mappage de port statique est effectué :

+
    +
  • Les configurations de firewall FTPS habituelles laissent une plus grande surface d’attaque pour +les malfaiteurs parce que le manque de visibilité sur le chemin de contrôle force +le firewall à ouvrir plus de ports que ce qui est strictement nécessaire, augmentant la +surface d’attaque.

  • +
  • Le mappage de port de données statique par serveur réel sur les équilibreurs de charge est plus +complexe à maintenir que ce qui est requis pour d’autres protocoles.

  • +
+

Dans les deux cas :

+
    +
  • On utilise généralement des mots de passe, qui ont tendance à être de longueur limitée, réduisant +la sécurité globale par rapport à SSH/SFTP où l’utilisation de longues paires de clés publiques/privées +est monnaie courante, et l’allongement de l’exigence de longueur de clé est simple.

  • +
  • FTPS ne prend pas en charge les plages d’octets qui sont utiles dans certaines applications, +et est pris en charge par SFTP et HTTP (avec ou sans (S)).

  • +
  • En cas d’un serveur distant compromis, le mot de passe du compte +est facile à déterminer. Alors que la meilleure pratique signifierait que ce mot de passe est de peu +ou aucune valeur, certaines mauvaises habitudes, telles que la réutilisation du mot de passe, peuvent signifier que le mot de passe a +une certaine valeur. Contraste avec SFTP: seules des informations déjà publiques sont divulguées.

  • +
  • Certains logiciels de serveur FTPS ont des mécanismes et des options de secours qui peuvent causer des +utilisateurs ou administrateurs à envoyer involontairement des informations non chiffrées. +Cela pourrait entraîner la révélation de mots de passe. Dans SFTP, les mots de passe sont généralement +non envoyé, les clés sont un élément de cryptage, il n’y a donc pas de mots de passe +pour intercepter.

  • +
  • FTPS est intrinsèquement plus complexe, ce qui le rend plus difficile à déployer et à exploiter.

  • +
  • Les limites des configurations prises en charge limitent les approches de firewall, +réduisant probablement la protection offerte aux serveurs internet.

  • +
+

Contrairement à FTPS, SFTP :

+
    +
  • parcourra n’importe quel nombre de points NAT/PAT sur un réseau intermédiaire sans difficulté.

  • +
  • fonctionne derrière tout type d’équilibreurs de charge, ce qui simplifie la mise à l’échelle des tableaux d’échange de produits.

  • +
  • n’exige aucune partie intervenante pour déchiffrer quoi que ce soit.

  • +
  • met moins de charge (à la fois cpu et mémoire) sur les périphériques réseau intermédiaires.

  • +
  • a des méthodes courantes similaires pour sécuriser les comptes sur les serveurs (par exemple, des shells restreints dans les prisons chroot).

  • +
  • prend en charge les plages d’octets, qui sont utiles.

  • +
  • est plus simple, avec moins d’options, donc plus fiable.

  • +
  • est plus simple à surveiller et à firewaller, et permet des configurations de firewall plus limitées.

  • +
  • est beaucoup plus courant (par exemple, Microsoft annonçant le support intégré dans une prochaine version de Windows [*] ).

  • +
  • utilise normalement des paires de clés publiques / privées, qui sont généralement considérées comme plus fortes que les mots de passe.

  • +
  • ne nécessite aucun secret partagé (ou un mécanisme pour les envoyer), et généralement les informations d’identification doivent être remplacées moins +souvent.

  • +
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/glossary.html b/fr/Reference/glossary.html new file mode 100644 index 000000000..0183bdad3 --- /dev/null +++ b/fr/Reference/glossary.html @@ -0,0 +1,265 @@ + + + + + + + Traduction de terminologie — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Traduction de terminologie

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

English

Français

Advertisement +Notification +post +message +notification

annonce +publication +message +message d´annonce +n.b voir en bas de la table

Data Pump

Pompe de données

Broker

Courtier

checksum

checksum (somme de controle?)

Component

Composant

File Path

Chemin d’accès

Flow

Flux

keyword

mot-clé(?)

load balancing

équilibrage de charge

Logs

Journaux

mirror

miroir

notification +message

message d’annonce +n.b voir en bas de la table

pattern

modèle

payload

charge utile

Poll

nom: sonde, verbe: sonder, +résultat: sondage

queue

fil d’attente

regexp pattern

Modèle regexp

Remote

Distant

Run

Exécuter

scope

portée (global scope? portée globale)

checksum

somme de controle

Plugins

modules externes

Post

Publier

String

chaîne de caractères

topic

thème

Tree

Arborescence

wildcards

caractère de remplacement

+
+

Note

+

Documentation is inconsistent in that it uses a variety of words +to describe the same object: messages that contain notifications +of the new availability of data. It may be called any of:

+

message, notification message, post, notification, announcement -> message, publicité? , annonce? , article?

+

The canonical term is now:

+

notification message in English, and message d’annonce in French.

+

There is an issue to standardize this usage, but it may not have +propagated to all occurrences. ( https://github.com/MetPX/sarracenia/issues/552 )

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/index.html b/fr/Reference/index.html new file mode 100644 index 000000000..b03691512 --- /dev/null +++ b/fr/Reference/index.html @@ -0,0 +1,182 @@ + + + + + + + Référence — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Référence

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr3.1.html b/fr/Reference/sr3.1.html new file mode 100644 index 000000000..f167ac98f --- /dev/null +++ b/fr/Reference/sr3.1.html @@ -0,0 +1,376 @@ + + + + + + + SR3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3

+
+

sr3 Sarracenia CLI

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr3 options action [ composant/config … ]

+
+
+

DESCRIPTION

+

Sr3 est un outil pour gérer une flotte de démons, ou les résultats/sortie du programme est principalement +dans les fichiers logs. Sr3 permet à l’utilisateur de commencer, arrêter, et se renseigner sur +le statut d’un flux de sarracenia déjà configuré. C’est le point d’entrée principal de la ligne de commande pour +Sarracenia 3 ( https://metpx.github.io/sarracenia/ )

+

Lorsque sr3 est démarré, l’intégralité des configurations sont lu et on peut faire une requête sur l’état +de tous les flux, avec sr3 status par exemple. Lorsque composant/config est utilisé, sr3 doit +fonctionner sur un sous-ensemble de toutes les configurations présentes.

+
    +
  • Si vous êtes déjà familier avec Sarracenia et que vous cherchez de l’information spécifique par rapport a des +options ou des directives, il vaut mieux regarder sr3 Options (7)

  • +
  • Pour commencer plus facilement, jetez un coup d’œil sur the Subscriber Guide on GitHub

  • +
  • Pour un guide général de l’interface : voir le Command Line Guide

  • +
+

La ligne de commande a trois éléments importants: +* options +* action +* composant/config

+

Un flux est un groupe de processus qui roule en utilisant un composant/config commun.

+
+
+

OPTIONS

+

La majorité des options son stockés dans des fichiers de configuration. Ceci est dénoté +par compsant/config indiqué par le nom du fichier, mais de temps en temps, on peut utiliser la ligne +de commande pour remplacer une valeur dans le fichier de configuration. Les options sont défini +Sr3 Options (7) Reportez-vous à cette page de manuel pour une discussion complète. +Il y a une exception

+
-h (or --help)
+
+
+

L’option d’aide est seulement disponible sur la ligne de commande. Elle est utilisée pour obtenir +une description d’une sélection d’options disponible pour remplacer les valeurs du fichier +de configuration.

+
+
+

ACTIONS

+

Les types d’actions disponible. Une seule parmi:

+
+
    +
  • add: copier a la liste de configurations disponible.

  • +
  • cleanup: supprimer les ressources des composants sur le serveur.

  • +
  • convert: copie et mets à jour un ou plusieurs configuration de v2 dans le répertoire sr3.

  • +
  • declare: crée les ressources d’un composant sur le serveur.

  • +
  • disbale: marquer une configuration comme non-éligible à exécuter.

  • +
  • edit: modifier une configuration existante.

  • +
  • enable: marquer une configuration comme éligible à exécuter.

  • +
  • foreground: rouler une seule instance avec le mode foreground avec des logs sur stderr

  • +
  • liste: lister toutes les configurations disponible.

  • +
  • liste plugins: lister tout les plugins disponible.

  • +
  • list examples: lister tout les examples disponible.

  • +
  • remove: supprimer une configuration.

  • +
  • run: comme start mais on attend que les sous-processus reviennent.

  • +
  • restart: arrêter et ensuite commencer une configuration.

  • +
  • sanity: cherche des instances qui sont perdus/coincées/en panne et les redémarres.

  • +
  • show afficher une version interprétée d’un fichier de configuration.

  • +
  • start: partir l’execution d’une ensemble de configurations

  • +
  • status: vérifier si une configuration est en train de rouler.

  • +
  • stop: arrêter une configuration en cours d’exécution.

  • +
+
+
+
+

COMPOSANTS

+

The Flow Algorithme est ce qui est exécuté +par tout les processus de sr3. Le comportement de l’algorithme du flux est adaptable aves les options, +dont certaines contrôlent des modules optionnels (flowcallbacks). Chaque composant possède de +différents ensembles de paramètres d’options par défaut pour couvrir un cas d’utilisation courant.

+
    +
  • cpump - copier un message d’annonce d’une pompe a une deuxième pompe (une implémentation C d’un shovel.)

  • +
  • flow - flux générique, pas de valeurs par défaut, bonne base pour convenir à la construction personnalisée les flux

  • +
  • poll - poller une page web non-sarracenia ou des fichiers sur un serveur pour créer des messages d’annonce pour un traitement.

  • +
  • post|sr3_post|sr_cpost|watch - créer es messages d’annonce pour les fichiers qui doivent être traités.

  • +
  • sarra - télécharger un fichier d’un serveur distant vers un serveur local et les republier pour les autres.

  • +
  • sender - envoyer des fichiers d’un serveur local à un serveur distant.

  • +
  • shovel - copier seulement des messages d’annonce et non des fichiers.

  • +
  • watch - crée un message d’annonce pour chaque nouveau fichier qui arrive dans un répertoire.

  • +
  • winnow - copier des messages d’annonce et supprime les doublons.

  • +
+
+
+

CONFIGURATIONS

+

Quand une paire de composant/configuration et spécifiée sur la ligne de commande, +la configuration se fait construire a partir de:

+
+
    +
  1. default.conf

  2. +
  3. admin.conf

  4. +
  5. <component>.conf (subscribe.conf, audit.conf, etc…)

  6. +
  7. <component>/<config>.conf

  8. +
+
+

Les paramètres d’un fichier .conf sont lu après le fichier default.conf, +et les valeurs initiales choisi par défaut peuvent éventuellement être replacer. +Les options spécifiées sur la ligne de commande remplacent les options spécifiées dans le +fichier de configuration.

+

Les fichiers de configurations peuvent être gérer en utilisant les actions add, remove, +list, edit, disable, et enable. Il est également possible de faire +les mêmes activités manuellement en manipulant les fichiers dans le répertoire des paramètres. +Les fichiers de configuration pour une configuration de sr3 appelé myflow +se trouverait ici:

+
+
    +
  • linux: ~/.config/sarra/subscribe/myflow.conf (selon: XDG Open Directory Specication )

  • +
  • Windows: %AppDir%/science.gc.ca/sarra/myflow.conf , ca pourrait être: +C:UserspeterAppDataLocalscience.gc.casarramyflow.conf

  • +
  • MAC: FIXME.

  • +
+
+

A la base, le répertoires ~/.config/sarra/default.conf contient des paramètres +qui sont lus par défaut pour tout composant au démarrage. Dans le même répertoire, +~/.config/sarra/credentials.conf, il y a les identifiants (mots de passe) qui doivent +être utilisé par sarracenia ( CREDENTIALS pour plus de détails ).

+

Il est également possible de définir la valeur de la variable XDG_CONFIG_HOME pour remplacer +le répertoire de base, ou sinon un fichier de configuration peut être placé dans n’importe quel +répertoire est peut être invoqué en utilisant le chemin du fichier au complet. +Quand un composant est invoqué, le fichier fourni est interprété en tant que chemin de fichier +(il est assumé que l’extension .conf est employé.) Si le chemin du fichier est introuvable, +le composant va regarder dans le répertoire de configuration du composant +( repertoire_config / composant ) pour un fichier .conf correspondant.

+

Si il est toujours introuvable, il le recherchera dans le répertoire de configuration du site +(linux : /usr/share/default/sarra/composant).

+

Finalement, si l’utilisateur a défini l’option remote_config a True et si il y a des +sites Web configurés où les configurations peuvent être trouvées (option remote_config_url), +le programme essaiera de télécharger le fichier à partir de chaque site jusqu’à ce qu’il en trouve un.

+

En cas de succès, le fichier est téléchargé sur repertoire_config/Téléchargements et interprété +par le programme à partir de là. Il existe un processus similaire pour tous les plugins qui peuvent +être interprétés et exécutés dans les composants de sarracenia. Les composants vont d’abord +regarder dans le répertoire plugins dans l’arborescence de configuration des utilisateurs, puis dans le site, +ensuite dans le paquet sarracenia lui-même, et enfin il regardera à distance.

+
+

Configurations a Distance

+

Il est possible de spécifier des URI en tant que fichiers de configuration, plutôt que des fichiers locaux. Exemple:

+
+
    +
  • –config http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

Au démarrage, sr3 vérifie si le fichier local cap.conf existe dans le +répertoire de configuration local. Si c’est le cas, le fichier sera lu pour trouver +une ligne comme celle-ci :

+
+
    +
  • –remote_config_url http://dd.weather.gc.ca/alerts/doc/cap.conf

  • +
+
+

Dans ce cas, il vérifiera l’URL distant et comparera le temps de modification +du fichier distant par rapport au fichier local. Si le fichier distant n’est pas plus récent ou ne peut pas +être atteint, le composant continuera avec le fichier local.

+

Si le fichier distant est plus récent ou s’il n’y a pas de fichier local, le fichier distant sera téléchargé, +et la ligne remote_config_url sera rajouté, de sorte qu’elle continuera +de se mettre à jour automatiquement à l’avenir.

+
+
+

Logs

+

Pour les fichiers de logs, il faut regarder dans ~/.cache/sr3/logs (pour linux. Cela va varier sur d’autres +plateformes.) Pour les trouver sur n’importe quel plateforme:

+
fractal% sr3 list
+User Configurations: (from: /home/peter/.config/sr3 )
+admin.conf                       credentials.conf                 default.conf
+logs are in: /home/peter/.cache/sr3/log
+
+
+

La dernière ligne indique le répertoire.

+
+
+
+

EXEMPLES

+

Voici un exemple complet de fichier de configuration:

+
broker amqps://dd.weather.gc.ca/
+
+subtopic model_gem_global.25km.grib2.#
+accept .*
+
+
+

Ce fichier se connectera au broker dd.weather.gc.ca en tant qu’anonyme avec mot de passe +anonyme (par défaut) pour obtenir des annonces à propos des fichiers dans le répertoire +http://dd.weather.gc.ca/model_gem_global/25km/grib2. +Tous les fichiers qui arrivent dans ce répertoire ou en dessous seront téléchargés +dans le répertoire courant (ou simplement imprimé en sortie standard si l’option -n +a été spécifié.)

+

Divers exemples de fichiers de configuration sont disponibles ici :

+
+
+
+
+

VOIR AUSSI

+

Commande de l’utilisateur:

+

sr3_post(1) - poste des annoncements de fichiers (implémentation en Python.)

+

sr3_cpost(1) - poste des annoncements de fichiers (implémentation en C.)

+

sr3_cpump(1) - copie les messages d’annonce ( implémentation en C du composant shovel. )

+

Formats:

+

sr3_credentials(7) - Convertir les lignes du fichier log au format .save pour le rechargement/le renvoi.

+

sr3_options(7) - Convertir les lignes du fichier log au format .save pour le rechargement/le renvoi.

+

sr3_post(7) - Format des messages d’annonce.

+

Page d’acceuil:

+

https://metpx.github.io/sarracenia - Sarracenia : une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr3_cpump.1.html b/fr/Reference/sr3_cpump.1.html new file mode 100644 index 000000000..705eb9a78 --- /dev/null +++ b/fr/Reference/sr3_cpump.1.html @@ -0,0 +1,258 @@ + + + + + + + SR_CPUMP — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR_CPUMP

+
+

sr_shovel en C

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+

sr_cpump foreground|start|stop|restart|reload|status configfile +sr_cpump cleanup|declare configfile

+
+
+

DESCRIPTION

+

sr_cpump est une implémentation alternative du composant shovel de sr3(1) +avec quelques limites.

+
+
    +
  • ne télécharge pas de données, ne fait que diffuser des publications.

  • +
  • fonctionne comme une seule instance (pas d’instances multiples).

  • +
  • ne supporte aucun plugin.

  • +
  • ne prend pas en charge vip pour une haute disponibilité.

  • +
  • différentes bibliothèques d’expressions régulières : POSIX vs python.

  • +
  • ne prend pas en charge les regex pour la commande strip (pas de regex non gourmand).

  • +
+
+

Il peut donc agir comme un remplacement direct pour :

+
+

sr3 shovel - traite les messages shovel.

+

sr3 winnow - traite les messages winnow.

+
+

L’implémentation C peut être plus facile à mettre à disposition dans des environnements spécialisés, +comme le calcul haute performance, car il a beaucoup moins de dépendances que la version python. +Il utilise également beaucoup moins de mémoire pour un rôle donné. Normalement la version python +est recommandé, mais il y a des cas où l’utilisation de l’implémentation C est judicieuse.

+

sr_cpump se connecte à un courtier (broker) (souvent le même que le courtier de publication) +et s’abonne aux notifications d’intérêt. A la réception d’une annonce, +il recherche sa sum dans son cache. S’il est trouvé, le fichier est déjà arrivé, +la notification est donc ignorée. Si ce n’est pas le cas, le fichier est nouveau et la sum est ajoutée +dans le cache et la notification est publiée.

+

sr_cpump peut être utilisé, comme sr3 winnow(1), pour couper les messages +de sr3_post(1), sr3 poll(1) +ou sr3 watch (1) etc… Il est utilisé lorsqu’il y a plusieurs +sources des mêmes données, de sorte que les clients ne téléchargent les données source qu’une seule fois à partir de +la première source qui l’a posté.

+

La commande sr3_cpump prend deux arguments: une action start|stop|restart|reload|status… (autodécrit) +suivi d’un fichier de configuration.

+

L’action foreground est utilisée lors du débogage d’une configuration, lorsque l’utilisateur veut +exécuter le programme et son fichier de configuration de manière interactive… L’instance foreground +n’est pas concerné par d’autres actions. L’utilisateur cesserait d’utiliser l’instance foreground +en simplement <ctrl-c> sous linux ou utilisez d’autres moyens pour envoyer SIGINT ou SIGTERM au processus.

+

Les actions cleanup, declare, peuvent être utilisées pour gérer les ressources sur +le serveur rabbitmq. Les ressources sont soit des files d’attente, soit des échanges. déclarer crée +les ressources.

+

Les actions ajouter, supprimer, modifier, lister, activer, désactiver agissent +sur les configurations.

+
+
+

CONFIGURATION

+

En général, les options de ce composant sont décrites par +la page sr3_options(7) qui doit être lue en premier. +Elle explique en détail la langue de configuration des options et comment trouver +les paramètres des options.

+

REMARQUE: La bibliothèque d’expressions régulières utilisée dans l’implémentation C est celle POSIX +et la grammaire est légèrement différente de l’implémentation de python. Quelques +ajustements peuvent être nécessaires.

+
+
+

VARIABLES ENVIRONNEMENTALES

+

Si la variable SR_CONFIG_EXAMPLES est définie, la directive add peut être utilisée +pour copier des exemples dans le répertoire de l’utilisateur à des fins d’utilisation et/ou de personnalisation.

+

Une entrée dans ~/.config/sarra/default.conf (créé via sr_subscribe edit default.conf ) +pourrait être utilisé pour définir la variable:

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

La valeur doit être disponible à partir de la sortie d’une commande de liste à partir de +l’implémentation python.

+
+
+

VOIR AUSSI

+

User Commands:

+

sr3(1) - Interface de ligne de commande principale de Sarracenia.

+

sr3_post(1) - publication des annonces de fichiers (implémentation Python.)

+

sr3_cpost(1) - publication des annonces de fichiers (implémentation C.)

+

Formats:

+

sr3_credentials(7) - le format des informations d’authentification.

+

sr3_options(7) - liste de toute les options dans la langue de configuration.

+

sr3_post(7) - le format des annonces

+

Page d’acceuil:

+

https://metpx.github.io/sarracenia - Sarracenia: une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr3_credentials.7.html b/fr/Reference/sr3_credentials.7.html new file mode 100644 index 000000000..1a14cef28 --- /dev/null +++ b/fr/Reference/sr3_credentials.7.html @@ -0,0 +1,251 @@ + + + + + + + SR3 CREDENTIALS — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR3 CREDENTIALS

+
+

SR3 Credential: Format du Fichier

+
+
manual section:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

CONFIGURATION

+

Normalement, les mots de passe ne sont pas spécifiés dans les fichiers de configuration. Ils sont plutôt placés +dans le fichier d’identification (credentials)

+
edit ~/.config/sr3/credentials.conf
+
+
+

Pour chaque URL spécifiée qui nécessite un mot de passe, on place une entrée correspondante dans credentials.conf. +L’option Broker définit toutes les informations d’identification pour se connecter au serveur RabbitMQ

+
    +
  • broker amqp{s}://<user>:<pw>@<brokerhost>[:port]/<vhost>

  • +
+
(défaut: amqps://anonymous:anonymous@dd.weather.gc.ca/ )
+
+
+

Pour tous les programmes sarracenia, les parties confidentielles des identifiants sont stockées +uniquement dans ~/.config/sarra/credentials.conf. Cela inclut la destination et le mot de passe du broker +ainsi que les paramètres nécessaires aux composants. Le format est d’une entrée par ligne. Exemples:

+
    +
  • amqp://user1:password1@host/

  • +
  • amqps://user2:password2@host:5671/dev

  • +
  • amqps://usern:passwd@host/ login_method=PLAIN

  • +
  • sftp://user5:password5@host

  • +
  • sftp://user6:password6@host:22 ssh_keyfile=/users/local/.ssh/id_dsa

  • +
  • ftp://user7:password7@host passive,binary

  • +
  • ftp://user8:password8@host:2121 active,ascii

  • +
  • ftps://user7:De%3Aize@host passive,binary,tls

  • +
  • ftps://user8:%2fdot8@host:2121 active,ascii,tls,prot_p

  • +
  • https://ladsweb.modaps.eosdis.nasa.gov/ bearer_token=89APCBF0-FEBE-11EA-A705-B0QR41911BF4

  • +
+

Dans d’autres fichiers de configuration ou sur la ligne de commande, l’url n’a tout simplement pas le +spécification du mot de passe ou de la clé. L’url donné dans les autres fichiers est recherchée +dans credentials.conf.

+
+

Identifiants et Details

+

Vous devrez peut-être spécifier des options supplémentaires pour des identifiants +spécifiques. Ces détails peuvent être ajoutés après la fin de l’URL, avec plusieurs +détails séparés par des virgules (voir les exemples ci-dessus).

+

Détails pris en charge :

+
    +
  • ssh_keyfile=<path> - (SFTP) Chemin du SSH keyfile

  • +
  • passive - (FTP) Utiliser le mode passif

  • +
  • active - (FTP) Utiliser le mode actif

  • +
  • binary - (FTP) Utiliser le mode binaire

  • +
  • ascii - (FTP) Utiliser le mode ASCII

  • +
  • ssl - (FTP) Utiliser le mode SSL/FTP standard

  • +
  • tls - (FTP) Utiliser FTPS avec TLS

  • +
  • prot_p - (FTPS) Utiliser une connexion de données sécurisée pour les connexions TLS (sinon, du texte clair est utilisé)

  • +
  • bearer_token=<token> (ou bt=<token>) - (HTTP) Jeton Bearer pour l’authentification

  • +
  • login_method=<PLAIN|AMQPLAIN|EXTERNAL|GSSAPI> - (AMQP) Par défaut, la méthode de connexion sera automatiquement

  • +
+

déterminée. Cela peut être remplacé en spécifiant une méthode Particulière de connexion, ce qui peut être +nécessaire si un broker prend en charge plusieurs méthodes et qu’une méthode incorrecte est automatiquement +sélectionnée.

+

Note:

+
Les informations d’identification SFTP sont facultatives. Sarracenia cherchera dans le répertoire .ssh
+et va utiliser les informations d’identification SSH normales qui s’y trouvent.
+
+Ces chaînes sont encodées en URL, donc si il y a un compte avec un mot de passe qui contient un caractère spécial,
+son équivalent encodé par URL peut être fourni. Dans le dernier exemple, **%2f** signifie que le
+mot de passe réel est: **/dot8**. L’avant-dernier mot de passe est : **De:olonize**.
+( %3a étant la valeur encodée url pour un caractère deux-points. )
+
+
+
+
+
+

VOIR AUSSI

+

sr3(1) - Sarracenia ligne de commande principale.

+

sr3_post(1) - poste des annoncements de fichiers (implémentation en Python.)

+

sr3_cpost(1) - poste des annoncements de fichiers (implémentation en C.)

+

sr3_cpump(1) - implémentation en C du composant shovel. (Copie des messages)

+

Formats:

+

sr3_options(7) - Les options de configurations

+

sr3_post(7) - Le formats des annonces.

+

Page d’Accueil:

+

https://metpx.github.io/sarracenia - Sarracenia : une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr3_options.7.html b/fr/Reference/sr3_options.7.html new file mode 100644 index 000000000..f817ec9b8 --- /dev/null +++ b/fr/Reference/sr3_options.7.html @@ -0,0 +1,1862 @@ + + + + + + + OPTIONS SR3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

OPTIONS SR3

+
+

Format de fichier de configuration SR3

+
+
section de manuel:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
group de Manuel:
+

MetPX-Sarracenia

+
+
+
+

SYNOPSIS

+
nom valeur
+nom valeur d’utilisation
+nom valeur_${substitution}
+.
+.
+.
+
+
+
+
+

DESCRIPTION

+

Les options sont placées dans des fichiers de configuration, une par ligne, avec le format:

+
option <valeur>
+
+
+

Par exemple:

+
debug true
+debug
+
+
+

définit l’option debug pour activer une sortie d’éxécution plus détaillée. Si aucune valeur n’est spécifiée, +la valeur true est assigné, donc les valeurs ci-dessus sont équivalentes. Un deuxième exemple:

+
broker amqps://anonymous@dd.weather.gc.ca
+
+
+

Dans l’exemple ci-dessus, broker est le mot-clé de l’option, et le reste de la ligne est la +valeur attribuée au paramètre. Les fichiers de configuration sont une séquence de paramètres, +avec un paramètre par ligne. +Remarque:

+
    +
  • les fichiers sont lus de haut en bas, surtout pour directory, strip, mirror, +et les options flatten s’appliquent aux clauses accept qui se trouvent dans la suite du fichier.

  • +
  • La barre oblique (/) est utilisée comme séparateur de chemin dans les fichiers de configuration Sarracenia sur tous les +systèmes d’exploitation. L’utilisation de la barre oblique inverse comme séparateur () (tel qu’utilisé dans la +cmd shell de Windows) risque de ne pas fonctionner correctement. Lorsque des fichiers sont lu dans Windows, le chemin d’accès +est immédiatement converti en utilisant la barre oblique. Ceci est pour s’assurer que les options reject, accept, et +strip peuvent filtrer des expressions correctement. C’est pour cela qu’il est toujours important d’utiliser la barre +oblique (/) quand un séparateur est nécessaire.

  • +
  • # est le préfixe des lignes de descriptions non fonctionnelles de configurations ou de commentaires. +C’est identique aux scripts shell et/ou python

  • +
  • Toutes les options sont sensibles aux majuscules et minuscules. Debug n’est pas la même chose que debug ni DEBUG. +Ces trois options sont différentes (dont deux n’existent pas et n’auront aucun effet et créeront un avertissement +d’« option inconnue »).

  • +
+

Le fichier a un ordre important. Il est lu de haut en bas, donc les options qui sont assignée sur une ligne on tendance +a affecter les lignes qui suivent:

+
mirror off
+directory /data/just_flat_files_here_please
+accept .*flatones.*
+
+mirror on
+directory /data/fully_mirrored
+accept .*
+
+
+

Dans l’extrait ci-dessus, le paramètre mirror est désactivé, et la valeur de directory est définie. Les fichiers +dont le nom inclut flatones seront donc tous placés dans le répertoire /data/just_flat_files_here_please. +Pour les fichiers qui n’ont pas ce nom, ils ne seront pas récupérés par le premier accept. Ensuite, avec le mirror +activé et le nouveau paramètre de directory défini, le restant des fichiers atterrira dans +/data/fully_mirrored. Un deuxième exemple :

+

séquence #1:

+
reject .*\.gif
+accept .*
+
+
+

séquence #2:

+
accept .*
+reject .*\.gif
+
+
+
+

Note

+

FIXME: cela ne correspond-il qu’aux fichiers se terminant par ‘gif’ ou devrions-nous y ajouter un $ ? +cela correspondra-t-il à quelque chose comme .gif2 ? y a-t-il un .* supposé à la fin ?

+
+

Dans la séquence #1, tous les fichiers se terminant par ‘gif’ sont rejetés. Dans la séquence #2, le +accept .* (qui accepte tout) est lu avant la déclaration du rejet, +donc le rejet n’a aucun effet. Certaines options ont une portée globale, plutôt que d’être +interprété dans l’ordre. Dans ces cas, la dernière déclaration remplace celle qu’il y avait plus tôt dans le fichier..

+
+
+

Variables

+

Il est possible de faire une substitution dans la valeur d’une option. Les valeurs sont représentées par ${name}. +Le nom peut être une variable d’environnement ordinaire, ou choisi parmi un certain nombre de variables déjà +intégrés:

+
varTimeOffset -5m
+directory /monrépertoirelocal/${%Y%m%d_%Hh%m:%S.%f}/mydailies
+accept    .*observations.*
+
+rename lala_${%o-1h%Y%m%d%H%m%S.%f}
+
+
+

Il est possible les substitution de date et heure, avec des décalage, dans le premier cas avec varTimeOffset, +un décalage de 5 minutes dans le passé, dans le deuxième cas, c’est une heure dans le passé. +Il est également possible de spécifier des substitutions de variables sur les arguments du paramètre du directory +en utilisant la notation *${..} * :

+
    +
  • %… - un patron tel qu’accepté par datetime.strftime()

    +
    +
      +
    • avec l’ajout du décalage au début avec o+- et une durée.

    • +
    • exemple: ${%Y/%m/%d_%Hh%M:%S.%f} –> 2022/12/04_17h36:34.123479

    • +
    +
    +
  • +
  • SOURCE - l’utilisateur amqp qui a injecté des données (extraites du message d’annonce).

  • +
  • BD - le répertoire de base.

  • +
  • BUP - le composant du chemin de baseUrl (ou : baseUrlPath).

  • +
  • BUPL - le dernier élément du chemin du baseUrl. (ou: baseUrlPathLast).

  • +
  • PBD - le “post base dir”.

  • +
  • var - n’importe quelle variable d’environnement.

  • +
  • BROKER_USER - le nom d’utilisateur pour l’authentification auprès du courtier (par exemple, anonyme)

  • +
  • POST_BROKER_USER - le nom d’utilisateur pour l’authentification auprès du courtier de destination (post_broker)

  • +
  • PROGRAM - le nom du composant (subscribe, shovel, etc…)

  • +
  • CONFIG - le nom du fichier de configuration en cours d’exécution.

  • +
  • HOSTNAME - le hostname qui exécute le client.

  • +
  • RANDID - Un ID aléatoire qui va être consistant pendant la duration d’une seule invocation.

  • +
+

Les horodatages %Y%m%d et %H font référence à l’heure à laquelle les données sont traitées par +le composant, ceci n’est pas décodé ou dérivé du contenu des fichiers livrés. +Toutes les dates/heures de Sarracenia sont en UTC. Le paramètre varTimeOffset peut spécifier +une déviation par rapport à l’heure actuelle.

+

note:

+
Lorsque les substitutions de date ${% sont présentes, l'interprétation des modèles % dans les noms de fichiers
+par strftime, peut signifier qu'il faut leur échapper les caractères précédents via le doublage : %%
+
+
+

Référez à sourceFromExchange pour un exemple d’utilisation. Notez que toute valeur déjà intégrée +dans Sarracenia a priorité par rapport à une variable du même nom dans l’environnement. +Notez que les paramètres de flatten peuvent être modifiés entre les options de directory.

+
+

Substitutions Compatible Sundew

+

Dans MetPX Sundew, le format de la nomination de fichier est beaucoup plus +stricte, et est spécialisée pour une utilisation aves les données du World Meteorological Organization (WMO). +Notez que la convention du format des fichiers est antérieure, et n’a aucun rapport avec la convention de +dénomination des fichiers de WMO actuellement approuvée, et est utilisé strictement comme format interne. Les fichiers sont +séparés en six champs avec deux points. Le premier champ, DESTFN, est le “Abbreviated Header Line (AHL)” de WMO +(style 386) ou les blancs sont remplacé avec des traits de soulignement

+
TTAAii CCCC YYGGGg BBB ...
+
+
+

(voir le manuel de WMO pour plus de détails) suivis de chiffres pour rendre le produit unique (cela est vrai en +théorie, mais pas en pratique vu qu’il existe un grand nombre de produits qui ont les mêmes identifiants). +La signification du cinquième champ est une priorité, et le dernier champ est un horodatage. +La signification des autres champs varie en fonction du contexte. Exemple de nom de fichier

+
SACN43_CWAO_012000_AAA_41613:ncp1:CWAO:SA:3.A.I.E:3:20050201200339
+
+
+

Si un fichier est envoyé à Sarracenia et qu’il est nommé selon les conventions de Sundew, +les champs de substitution suivants seront disponibles:

+
${T1}    remplacer par le bulletin T1
+${T2}    remplacer par le bulletin T2
+${A1}    remplacer par le bulletin A1
+${A2}    remplacer par le bulletin A2
+${ii}    remplacer par le bulletin ii
+${CCCC}  remplacer par le bulletin CCCC
+${YY}    remplacer par le bulletin YY   (obs. jour)
+${GG}    remplacer par le bulletin GG   (obs. heure)
+${Gg}    remplacer par le bulletin Gg   (obs. minute)
+${BBB}   remplacer par le bulletin bbb
+${RYYYY} remplacer par l'année de réception
+${RMM}   remplacer par le mois de réception
+${RDD}   remplacer par le jour de réception
+${RHH}   remplacer par l'heure de réception
+${RMN}   remplacer par la minute de réception
+${RSS}   remplacer par la seconde de réception
+${YYYY}         année actuelle (utilisé %Y est préféré)
+${MM}           mois actuel (utilisé %M est préféré)
+${JJJ}          julian actuelle (utilisé %j est préféré)
+${YYYYMMDD}     date actuelle (utilisé %Y%M%D est préféré)
+
+
+

Les champs ‘R’ proviennent du sixième champ, et les autres viennent du premier champ. +Lorsque des données sont injectées dans Sarracenia à partir de Sundew, l’en-tête du message d’annonce sundew_extension +fournira la source de ces substitions même si ces champs ont été supprimés des fichiers livrés.

+
+
note::

les versions périmés de spécification temporelles éventuellement vont cessé d´être interprétés +ands une version ultérieur.

+
+
+
+
SR_DEV_APPNAME
+

La variable d’environnement SR_DEV_APPNAME peut être définie pour que la configuration de l’application et les répertoires +d’état soient créés sous un nom différent. Ceci est utilisé dans le développement pour pouvoir avoir de nombreuses configurations +actives à la fois. Cela permet de faire plus de tests au lieu de toujours travailler avec la configuration réelle du développeur.

+

Exemple : export SR_DEV_APPNAME=sr-hoho… lorsque vous démarrez un composant sur un système Linux, il +va rechercher les fichiers de configuration dans ~/.config/sr-hoho/ et va placer les fichiers d’état dans le +répertoire ~/.cache/sr-hoho.

+
+
+
+
+

TYPES D’OPTIONS

+

Les options de sr3 ont plusieurs types :

+
+
count

type de nombre entier. Même format que size détaillé plus bas.

+
+
duration

un nombre à virgule flottante qui indique une quantité en secondes (0.001 est 1 milliseconde) +modifié par un suffixe unitaire ( m-minute, h-heure, w-semaine ).

+
+
flag

une option qui a la valeur soit Vrai (True ou on) ou Faux (False ou off) (une valeur booléenne).

+
+
float

un nombre à virgule flottante, (séparateur de décimale étant un point.)

+
+
list

une liste de chaîne de caractères, chaque occurrence successive se rajoute au total. +Tous les options plugins de v2 sont déclarée du type list.

+
+
set

un assortissement de chaîne de caractères, chaque occurrence successive s’unionise au total.

+
+
size

taille entière. Suffixes k, m et g pour les multiplicateurs kilo, méga et giga (base 10). +si on rajoute ´b’ … c´est base 2 : 1k=1000, 1kb=1024

+
+
str

une chaîne de caractères.

+
+
+
+
+

OPTIONS

+

Les options actuelles sont énumérées ci-dessous. Notez qu’elles sont sensibles aux majuscules, et +seulement un extrait est disponible sur la ligne de commande. Celles qui sont disponibles +sur la ligne de commande ont le même effet que lorsqu’elles sont spécifiés dans un fichier de configuration.

+

Les options disponibles dans les fichiers de configuration :

+
+

accelTreshold <size> défaut: 0 (désactiver.)

+

L’option accelThreshold indique la taille minimale d’un fichier transféré pour +qu’un téléchargeur binaire puisse être lancé.

+
+
+

accelXxxCommand

+

On peut spécifier d’autres fichiers binaires pour les téléchargeurs pour des cas particuliers,

+ + + + + + + + + + + + + + + + + + + + + +

Option

Valeur par Défaut

accelWgetCommand

/usr/bin/wget %s -O %d

accelScpCommand

/usr/bin/scp %s %d

accelCpCommand

/usr/bin/cp %s %d

accelFtpgetCommand

/usr/bin/ncftpget %s %d

accelFtpputCommand

/usr/bin/ncftpput %s %d

+

utilisez %s pour remplacer le nom du fichier source et %d pour le fichier en cours d’écriture. +Un exemple de paramètre à remplacer

+
accelCpCommand dd if=%s of=%d bs=4096k
+
+
+
+
+

accept, reject et acceptUnmatched

+
    +
  • accept <modèle regexp> (optionnel) [<mot-clés>]

  • +
  • reject <modèle regexp> (optionnel)

  • +
  • acceptUnmatched <booléen> (défaut: True)

  • +
+

Les options accept et reject traitent les expressions régulières (regexp). +Le regexp est appliqué à l’URL du message d’annonce pour trouver une correspondance.

+

Si l’URL d’un fichier correspond à un modèle reject, le message d’annonce +est reconnu comme consommé par le courtier et est ignoré.

+

Celui qui correspond à un modèle accept est traité par le composant.

+

Dans de nombreuses configurations, les options accept et reject sont mélangé +avec l’option directory. Ces options associent les messages d’annonce acceptés +à la valeur du directory sous laquelle elles sont spécifiées.

+

Une fois que toutes les options accept / reject sont traitées, normalement +le message d’annonce est accepté. Pour changer ce comportement, +il est possible de définir acceptUnmatched à False. Les paramètres de accept/reject +sont interprétés dans l’ordre. Chaque option est traitée de manière ordonnée +de haut en bas. Par exemple:

+

séquence #1:

+
reject .*\.gif
+accept .*
+
+
+

séquence #2:

+
accept .*
+reject .*\.gif
+
+
+

Dans la séquence #1, tous les fichiers se terminant par ‘gif’ sont rejetés. Dans la séquence #2, +le accept .* (qui accepte tout) est lu avant la déclaration de reject, de sorte que le reject n’a aucun effet.

+

Il est recommandé d’utiliser le filtrage côté serveur pour réduire le nombre d’annonces envoyées au composant, +et a la place, envoyer un sur ensemble de ce qui est pertinent, et de seulement régler les mécanismes côté client, +économisant du bandwidth et du traitement pour tous. Plus de détails sur les directives:

+

Les options accept et reject utilisent des expressions régulières (regexp) pour trouver +une correspondance avec l’URL. +Ces options sont traitées séquentiellement. +L’URL d’un fichier qui correspond à un modèle reject n’est pas publiée. +Les fichiers correspondant à un modèle accept sont publiés. +Encore une fois, un rename peut être ajouté à l’option accept… les produits qui correspondent +a l’option accept seront renommé comme décrit… à moins que le accept corresponde à +un fichier, l’option rename doit décrire un répertoire dans lequel les fichiers +seront placé (en préfix au lieu de remplacer le nom du fichier).

+

L’option permDefault permet aux utilisateurs de spécifier un masque d’autorisation octal numérique +de style Linux:

+
permDefault 040
+
+
+

signifie qu’un fichier ne sera pas publié à moins que le groupe ait l’autorisation de lecture +(sur une sortie ls qui ressemble à : —r—–, comme une commande chmod 040 <fichier> ). +Les options permDefault spécifient un masque, c’est-à-dire que les autorisations doivent être +au moins ce qui est spécifié.

+

Le regexp pattern peut être utilisé pour définir des parties du répertoire si une partie du message d’annonce est placée +entre parenthèses. sender peut utiliser ces parties pour générer le nom du répertoire. +Les chaînes de parenthèses entre les guillemets rst remplaceront le mot-clé ${0} dans le nom du répertoire… +le second {1} $ etc.

+

Exemple d’utilisation

+
filename NONE
+
+directory /ce/premier/répertoire/ciblé
+
+accept .*fichier.*type1.*
+
+directory /ce/répertoire/ciblé
+
+accept .*fichier.*type2.*
+
+accept .*fichier.*type3.*  DESTFN=fichier_de_type3
+
+directory /ce/${0}/modèle/${1}/répertoire
+
+accept .*(2016....).*(RAW.*GRIB).*
+
+
+

Un message d’annonce sélectionné par le premier accept sera remis inaltérée dans le premier répertoire.

+

Un message d’annonce sélectionné par le deuxième accept sera remis inaltérée dans deuxième répertoire.

+

Un message d’annonce sélectionné par le troisième *accept sera renommé « fichier_de_type3 » dans le deuxième répertoire.

+

Un message d’annonce sélectionné par le quatrième accept sera remis inaltérée à un répertoire.

+

Ça sera appelé /ce/20160123/modèle/RAW_MERGER_GRIB/répertoire si la notice du message d’annonce ressemble à cela:

+

20150813161959.854 http://this.pump.com/ relative/path/to/20160123_product_RAW_MERGER_GRIB_from_CMC

+
+
+

acceptSizeWrong: <booléen> (défaut: False)

+

Lorsqu’un fichier est téléchargé et que sa taille ne correspond pas à celle annoncée, il est +normalement rejeté, comme un échec. Cette option accepte le fichier même avec la mauvaise +taille. Cela est utile lorsque le fichier change fréquemment, et qu’il passe en fil d’attente, donc +le fichier est modifié au moment de sa récupération.

+
+
+

attempts <count> (défaut: 3)

+

L’option attempts indique combien de fois il faut tenter le téléchargement des données avant d’abandonner. +Le défaut de 3 tentatives est approprié dans la plupart des cas. Lorsque l’option retry a la valeur false, +le fichier est immédiatement supprimé.

+

Lorsque l’option attempts est utilisé, un échec de téléchargement après le numéro prescrit +des attempts (ou d’envoi, pour un sender) va entrainer l’ajout du message d’annonce à un fichier de fil d’attente +pour une nouvelle tentative plus tard. Lorsque aucun message d’annonce n’est prêt à être consommé dans la fil d’attente AMQP, +les requêtes se feront avec la fil d’attente de “retry”.

+
+
+

baseDir <chemin> (défaut: /)

+

baseDir fournit le chemin d’accès au répertoire, et lorsqu’il est combiné avec le chemin d’accès relatif +de la notification sélectionnée, baseDir donne le chemin absolu du fichier à envoyer. +Le défaut est None, ce qui signifie que le chemin dans la notification est le chemin absolu.

+

Parfois, les senders s’abonnent à xpublic local, qui sont des URL http, mais le sender +a besoin d’un fichier local, alors le chemin d’accès local est construit en concaténant:

+
baseDir + chemin d'accès relatif dans le baseUrl + relPath
+
+
+
+
+

baseUrl_relPath <flag> (défaut: off)

+

Normalement, le chemin d’accès relatif (baseUrl_relPath est False, ajouté au répertoire de base) pour +les fichiers téléchargés seront définis en fonction de l’en-tête relPath inclus +dans le message d’annonce. Toutefois, si baseUrl_relPath est défini, le relPath du message d’annonce va +être précédé des sous-répertoires du champ baseUrl du message d’annonce.

+
+
+

batch <count> (défaut: 100)

+

L’option batch est utilisée pour indiquer le nombre de fichiers à transférer +sur une connexion, avant qu’elle ne soit démolie et rétablie. Sur de très bas volume de +transferts, où des délais d’attente peuvent se produire entre les transferts, cela devrait être +ajuster à 1. Pour la plupart des situations, le défaut est bien. Pour un volume plus élevé, +on pourrait l’augmenter pour réduire les frais généraux de transfert. Cette option est seulement utilisé pour les +protocoles de transfert de fichiers, et non HTTP pour le moment.

+
+
+

blocksize <size> défaut: 0 (auto)

+

REMARQUE: EXPERIMENTAL pour sr3, devrait revenir dans la version future +Cette option blocksize contrôle la stratégie de partitionnement utilisée pour publier des fichiers. +La valeur doit être l’une des suivantes

+
0 - calcul automatiquement une stratégie de partitionnement appropriée (défaut).
+1 - envoyez toujours des fichiers entiers en une seule partie.
+<blocksize> - utiliser une taille de partition fixe (taille d’exemple : 1M ).
+
+
+

Les fichiers peuvent être annoncés en plusieurs parties. Chaque partie à un somme de contrôle (checksum) distinct. +Les parties et leurs somme de contrôle sont stockées dans la cache. Les partitions peuvent traverser +le réseau séparément et en parallèle. Lorsque les fichiers changent, les transferts sont +optimisé en n’envoyant que les pièces qui ont changé.

+

L’option outlet permet à la sortie finale d’être autre qu’un poste. +Voir sr3_cpump(1) pour plus de détails.

+
+
+

Broker

+

broker [amqp|mqtt]{s}://<utilisateur>:<mot-de-passe>@<hoteDuCourtier>[:port]/<vhost>

+

Un URI est utilisé pour configurer une connexion à une pompe de messages d’annonce, soit +un courtier MQTT ou AMQP. Certains composants de Sarracenia fixent un défaut raisonnable pour +cette option. Il faut fournir l’utilisateur normal, l’hôte, et le port de connexion. +Dans la plupart des fichiers de configurations, +le mot de passe est manquant. Le mot de passe est normalement inclus seulement dans le fichier +credentials.conf.

+

Le travail de Sarracenia n’a pas utilisé de vhosts, donc vhost devrait presque toujours être /.

+

pour plus d’informations sur le format URI AMQP: ( https://www.rabbitmq.com/uri-spec.html )

+

soit dans le fichier default.conf, soit dans chaque fichier de configuration spécifique. +L’option broker indique à chaque composant quel courtier contacter.

+

broker [amqp|mqtt]{s}://<utilisateur>:<mot-de-passe>@<hoteDuCourtier>[:port]/<vhost>

+
+
::

(défaut: None et il est obligatoire de le définir )

+
+
+

Une fois connecté à un courtier AMQP, l’utilisateur doit lier une fil d’attente +aux échanges et aux thèmes pour déterminer le messages d’annonce en question.

+
+
+

bufsize <size> (défaut: 1m)

+

Les fichiers seront copiés en tranches de bufsize octets. Utilisé par les protocoles de transfert.

+
+
+

byteRateMax <size> (défaut: 0)

+
+
byteRateMax est supérieur à 0, le processus tente de respecter cette vitesse de livraison

en kilo-octets par seconde… ftp,ftps,ou sftp)

+
+
+

FIXME: byteRateMax… uniquement implémenté par le sender ? ou subscriber aussi, données uniquement, ou messages d’annonce aussi ?

+
+
+

callback <SpéficationDeClass>

+

La plupart des traitements personnalisables ou de la logique “plugin” sont implémentés à l’aide de la classe de flowCallback (“rappel de flux.”) À différents stades du traitement des messages de notification, les classes de flowCallback définissent +points d’entrée qui correspondent à ce point de traitement. pour chaque point de ce type dans le traitement, +il existe une liste de routines de rappel de flux à appeler.

+
+
+

Le SpécificationDeClass est similaire à une instruction import de python. +Il utilise le chemin de recherche standard pour les modules python, et inclut également ~/.config/sr3/plugins. +Il y a un raccourci pour faire usage plus court pour les cas courants. par exemple:

+
callback log
+
+
+

Sarracenia tentera d’abord de faire précéder log de sarracenia.flowcb.log puis +instancier l’instance de rappel en tant qu’élément de la classe sarracenia.flowcb.log.Log. +S’il ne trouve pas une telle classe, alors il tentera de trouver un nom de classe log, et instanciera un +objet log.Log.

+

Pour plus de détails sur ce genre de recherche, consulter (en anglais) +FlowCallback load_library

+
+
+

callback_prepend <SpécificationDeClass>

+

Identique à callback mais rajoute la class au début de la liste (pour éxecuter avant les point +d´entrée des autres classes FlowCB)

+
+
+

dangerWillRobinson (default: omis)

+

Cette option n’est reconnue qu’en tant qu’option de ligne de commande. Il est spécifié quand une opération +aura des effets irréversiblement destructeurs ou peut-être inattendus. par exemple:

+
sr3 stop
+
+
+

arrêtera d’exécuter les composants, mais pas ceux qui sont exécutés au premier plan. Arrêter ceux +peut surprendre les analystes qui les examineront, donc ce n’est pas fait par défaut

+
sr3 --dangerWillRobinson stop
+
+
+

arrête arrête tous les composants, y compris ceux de premier plan. Un autre exemple serait le nettoyage +action. Cette option supprime les files d’attente et les échanges liés à une configuration, qui peuvent être +destructeur pour les flux. Par défaut, le nettoyage ne fonctionne que sur une seule configuration à la fois. +On peut spécifier cette option pour faire plus de ravages.

+
+
+

declare

+
+
env NAME=Value

On peut également référer à des variables d’environnement dans des fichiers de configuration, +en utilisant la syntaxe ${ENV}. Si une routine de Sarracenia doit utiliser +une variable d’environnement, elles peuvent être définis dans un fichier de configuration

+
declare env HTTP_PROXY=localhost
+
+
+
+
exchange exchange_name

à l’aide de l’URL d’administration, déclarez l’échange avec exchange_name

+
+
subscriber

Un abonné (subsciber) est un utilisateur qui peut seulement s’abonner aux données et renvoyer des messages de rapport. +Les abonnés n’ont pas le droit d’injecter des données. Chaque abonné dispose d’un xs_<utilisateur> qui +s’appelle “exchange” sur la pompe. Si un utilisateur est nommé Acme, l’échange correspondant sera xs_Acme. +Cet échange est l’endroit où un processus d’abonnement enverra ses messages de rapport.

+

Par convention/défaut, l’utilisateur anonyme est créé sur toutes les pompes pour permettre l’abonnement sans abonnement +a un compte spécifique.

+
+
source

Un utilisateur autorisé à s’abonner ou à générer des données. Une source ne représente pas nécessairement +une personne ou un type de données, mais plutôt une organisation responsable des données produites. +Donc, si une organisation recueille et met à disposition dix types de données avec un seul contact, +e-mail, ou numéro de téléphone, toute question sur les données et leur disponibilité par rapport aux +activités de collecte peuvent alors utiliser un seul compte “source”.

+

Chaque source reçoit un échange xs_<utilisateur> pour l’injection de publications de données. Cela est comme un abonné +pour envoyer des messages de rapport sur le traitement et la réception des données. La source peut également avoir +un échange xl_<utilisateur> où, selon les configurations de routage des rapports, les messages de rapport des +consommateurs seront envoyés.

+
+
feeder

Un utilisateur autorisé à écrire à n’importe quel échange. Une sorte d’utilisateur de flux administratif, destiné à pomper +des messages d’annonce lorsque aucune source ou abonné ordinaire n’est approprié pour le faire. Doit être utilisé de +préférence au lieu de comptes d’administrateur pour exécuter des flux.

+
+
+

Les informations d’identification de l’utilisateur sont placées dans le credentials.conf +et sr3 --users declare mettra à jour le courtier pour accepter ce qui est spécifié dans ce fichier, tant que le +mot de passe de l’administrateur est déjà correct.

+
    +
  • Par défaut, tous les utilisateurs sont déclarés. Toutefois, des flux peuvent être spécifiés sur +la ligne de commande pour limiter les utilisateurs déclarés à ceux du flux donné. Par exemple,

    +
      +
    • sr3 --users declare déclarera tous les utilisateurs

    • +
    • sr3 --users declare subscribe/dd_amis ne déclarera que les utilisateurs spécifiés dans subscribe/dd_amis

    • +
    +
  • +
+
+
+

debug

+

Définir l’option debug est identique a utilisé logLevel debug

+
+
+

delete <booléen> (défaut: off)

+

Lorsque l’option delete est définie, une fois le téléchargement terminé avec succès, l’abonné +supprimera le fichier à la source. Par défaut, l’option est false.

+
+
+

discard <booléen> (défaut: off)

+

L’option discard, si elle est définie a true, supprime le fichier une fois téléchargé. Cette option peut être +utile lors du débogage ou pour tester une configuration.

+
+
+

directory <chemin> (défaut: .)

+

L’option directory définit où placer les fichiers sur votre serveur. +Combiné avec les options accept / reject, l’utilisateur peut sélectionner +les fichiers d’intérêt et leurs répertoires de résidence (voir le mirror +pour plus de paramètres de répertoire).

+

Les options accept et reject utilisent des expressions régulières (regexp) pour trouver une correspondance avec l’URL. +Ces options sont traitées séquentiellement. +L’URL d’un fichier qui correspond à un modèle reject n’est jamais téléchargée. +Celui qui correspond à un modèle accept est téléchargé dans le répertoire +déclaré par l’option directory la plus proche au-dessus de l’option accept correspondante. +acceptUnmatched est utilisé pour décider quoi faire lorsque aucune clause de rejet ou d’acceptation corresponde.

+
ex.   directory /monrépertoirelocal/mesradars
+      accept    .*RADAR.*
+
+      directory /monrépertoirelocal/mesgribs
+      reject    .*Reg.*
+      accept    .*GRIB.*
+
+
+
+
+

destfn_script <script> (défaut: None)

+

L’option de compatibilité Sundew définit un script à exécuter lorsque tout est prêt +pour la livraison du produit. Le script reçoit une instance de la classe sender. +Le script prends le parent comme argument, et par exemple, une +modification de parent.msg.new_file changera le nom du fichier écrit localement.

+
+
+

download <flag> (défaut: True)

+

utilisé pour désactiver le téléchargement dans le composant subscribe et/ou sarra. +Se définit a False par défaut dans les composants de shovel ou de winnow.

+
+
+

dry_run <flag> (défaut: False)

+

Exécuter en mode simulation par rapport aux transferts de fichiers. Se connecte toujours à un courtier et télécharge et traite +les messages d´annonce, mais les transferts de fichiers corréspondants sont désactivés, à utiliser lors du test d’un expéditeur +ou d’un téléchargeur, par exemple pour s’exécuter en parallèle avec un fichier existant, et comparez les journaux pour voir +si l’expéditeur est configuré pour envoyer les mêmes fichiers que l’ancien (implémenté avec un autre système.)

+
+
+

durable <flag> (défaut: True)

+

L’option AMQP durable, sur les déclarations de fil d’attente. Si la valeur est True, +le courtier conservera la fil d’attente lors des redémarrages du courtier. +Cela signifie que la fil d’attente est sur le disque si le courtier est redémarré.

+

Remarque: seuls les messages persistants resteront dans une file d’attente durable après le redémarrage du courtier. +Les messages persistants peuvent être publiés en activant l’option persistant (elle est activée par défaut).

+
+
+

fileEvents <évènement, évènement,…>

+

ensemble séparée par des virgules de d’événements de fichiers à surveiller. +Événements de fichiers disponibles : create, delete, link, modify, mkdir, rmdir +Si on commence la liste avec plus (+) ca signifie un rajout à l´ensemble actuel +Les événements create, modify et delete reflètent ce qui est attendu : un fichier en cours de création, +de modification ou de suppression. +Si link est défini, des liens symboliques seront publiés sous forme de liens afin que les consommateurs puissent choisir +comment les traiter. S’il n’est pas défini, aucun événement de lien symbolique sera publié.

+
+

Note

+

déplacer ou renommer des événements entraîne un modèle spécial de double publication, avec une publication en +utilisant l’ancien nom et définissant le champ newname, et un deuxième message d’annonce avec le nouveau nom, et un champ oldname. +Cela permet aux abonnés d’effectuer un renommage réel et d’éviter de déclencher un téléchargement lorsque cela est possible.

+
+

FIXME : algorithme de renommage amélioré en v3 pour éviter l’utilisation de double post…

+
+
+

exchange <nom> (défaut: xpublic) et exchangeSuffix

+

La norme pour les pompes de données est d’utiliser l’échange xpublic. Les utilisateurs peuvent établir un +flux de données privées pour leur propre traitement. Les utilisateurs peuvent déclarer leurs propres échanges +qui commencent toujours par xs_<nom-d’utilisatueur>. Pour éviter d’avoir à le spécifier à chaque +fois, on peut simplement régler exchangeSuffix kk qui entraînera l’échange +à être défini a xs_<nom-d’utilisatueur>_kk (en remplaçant le défaut xpublic). +Ces paramètres doivent apparaître dans le fichier de configuration avant les paramètres topicPrefix et subtopic.

+
+
+

exchangeDeclare <flag>

+

Au démarrage, par défaut, Sarracenia redéclare les ressources et les liaisons pour s’assurer qu’elles +sont à jour. Si l’échange existe déjà, cet indicateur peut être défini a False, +donc aucune tentative d’échange de la fil d’attente n’est faite, ou il s’agit de liaisons. +Ces options sont utiles sur les courtiers qui ne permettent pas aux utilisateurs de déclarer leurs échanges.

+
+
+

expire <duration> (défaut: 5m == cinq minutes. RECOMMENDE DE REMPLACER)

+

L’option expire est exprimée sous forme d’une duration… ça fixe combien de temps une fil d’attente devrait +vivre sans connexions.

+

Un entier brut est exprimé en secondes, et si un des suffixe m,h,d,w est utilisés, l’intervalle est en minutes, +heures, jours ou semaines respectivement. Après l’expiration de la fil d’attente, le contenu est supprimé et +des différences peuvent donc survenir dans le flux de données de téléchargement. Une valeur de +1d (jour) ou 1w (semaine) peut être approprié pour éviter la perte de données. Cela dépend de combien de temps +l’abonné est sensé s’arrêter et ne pas subir de perte de données.

+

Si aucune unité n’est donnée, un nombre décimal de secondes peut être fourni, tel que +0,02 pour spécifier une durée de 20 millisecondes.

+

Le paramètre expire doit être remplacé pour une utilisation opérationnelle. +Le défaut est défini par une valeur basse car il définit combien de temps les ressources vont être +assigné au courtier, et dans les premières utilisations (lorsque le défaut était de de 1 semaine), les courtiers +étaient souvent surchargés de très longues files d’attente pour les tests restants.

+
+
+

filename <mots-clé> (défaut:None)

+

De MetPX Sundew, le support de cette option donne toutes sortes de possibilités +pour définir le nom de fichier distant. Certains keywords sont basés sur le fait que +les noms de fichiers MetPX Sundew ont cinq (à six) champs de chaîne de caractères séparés par des deux-points.

+

La valeur par défaut sur Sundew est NONESENDER, mais dans l’intérêt de décourager l’utilisation +de la séparation par des deux-points dans les fichiers, le défaut dans Sarracenia est WHATFN.

+

Les mots-clés possibles sont :

+
+
None
    +
  • Aucune modification du nom de fichier (enlever toute interprétation de style Sundew) +N.B. différent de NONE décrit plus loin.

  • +
+
+
WHATFN
    +
  • la première partie du nom de fichier Sundew (chaîne de caractères avant le premier : )

  • +
+
+
HEADFN
    +
  • Partie EN-TETE du nom de fichier Sundew

  • +
+
+
SENDER
    +
  • le nom de fichier Sundew peut se terminer par une chaîne SENDER=<string> dans ce cas, +la <string> sera le nom de fichier distant

  • +
+
+
NONE
    +
  • livrer avec le nom du fichier Sundew complet (sans :SENDER=…)

  • +
+
+
NONESENDER
    +
  • livrer avec le nom de fichier Sundew complet (avec :SENDER=…)

  • +
+
+
TIME
    +
  • horodatage ajouté au nom de fichier. Exemple d’utilisation : WHATFN:TIME

  • +
+
+
DESTFN=str
    +
  • déclaration str direct du nom de fichier

  • +
+
+
SATNET=1,2,3,A
    +
  • Paramètres d’application satnet interne cmc

  • +
+
+
DESTFNSCRIPT=script.py
    +
  • appeler un script (identique à destfn_script) pour générer le nom du fichier à écrire

  • +
+
+
+
+
+

flatten <string> (défaut: ‘/’)

+

L’option flatten permet de définir un caractère de séparation. La valeur par défaut ( ‘/’ ) +annule l’effet de cette option. Ce caractère remplace le ‘/’ dans l’url +et crée un nom de fichier « flatten » à partir de son chemin d’accès dd.weather.gc.ca. +Par exemple, récupérer l’URL suivante, avec les options

+
http://dd.weather.gc.ca/model_gem_global/25km/grib2/lat_lon/12/015/CMC_glb_TMP_TGL_2_latlon.24x.24_2013121612_P015.grib2
+
+  flatten   -
+  directory /monrépertoirelocal
+  accept    .*model_gem_global.*
+
+
+

entraînerait la création du chemin d’accès au fichier:

+
/monrépertoirelocal/model_gem_global-25km-grib2-lat_lon-12-015-CMC_glb_TMP_TGL_2_latlon.24x.24_2013121612_P015.grib2
+
+
+
+
+

flowMain (défaut: None)

+

Par défaut, un flux exécutera la classe sarracenia.flow.Flow, qui implémente l’algorithme Flow de manière générique. +La version générique ne transfère pas de données, crée et manipule uniquement des messages. Cela convient pour +pelle, vanner, poster et surveiller les composants, mais les composants qui transfèrent ou transforment les données ont besoin +pour définir un comportement supplémentaire en sous-classant Flow. Exemples : sarracenia.flow.sender, sarracenia.flow.poll, sarracenia.flow.subscribe.

+

L’option flowMain permet à une configuration de flux d’exécuter une sous-classe de flux, au lieu du parent par défaut +classer. Exemple:

+
flowMain subscribe
+
+
+

Dans un fichier de configuration de flux générique, le flux sera configuré pour agir en tant que composant d’abonné (subscribe.) +On peut créer des composants personnalisés en sous-classant Flow et en utilisant la directive flowMain pour invoquer +la nouvelle sous-classe.

+
+ +
+

force_polling <flag> (défaut: False)

+

Par défaut, « watch » sélectionne une méthode optimale (dépendante du système d’exploitation) pour regarder un +répertoire.

+

Pour les grandes arborescences, la méthode optimale peut être plusieurs fois (10x ou même +100x) plus rapide à reconnaître lorsqu’un fichier a été modifié. Dans certains cas, +les méthodes optimales de plateforme ne fonctionnent pas (comme avec certains réseaux, +partages, ou systèmes de fichiers distribués), il faut donc utiliser un système plus lent mais avec une méthode +de « polling » plus fiable et portable. Le mot-clé force_polling oblige « watch » a sélectionner +la méthode de « polling » malgré le fait qu’il y ait une meilleur option de disponible.

+
+
Pour une discussion détaillée, voir:

Detecting File Changes

+
+
+

REMARQUE:

+
Lorsque les répertoires sont consommés par des processus en utilisant l’option *delete* de l’abonné, ils restent vides, et
+chaque fichier doit être signalé à chaque passage.  Lorsque les abonnés n’utilisent pas *delete*, « watch » doit
+savoir quels fichiers sont nouveaux.  Il le fait en notant l’heure du début de la dernière passe du « polling ».
+Les fichiers sont publiés si leur heure de modification est plus récente que cela. Cela se traduira par de
+nombreux postes de « watch », qui peuvent être minimisés avec l’utilisation de la cache. On pourrait même dépendre
+de la cache entièrement et activez l’option *delete*, ou « watch » pourra tenter de publier l’arborescence entière
+à chaque fois (en ignorant mtime).
+
+**LIMITATION CONNUE** : Lorsque *force_polling* est défini, le paramètre *sleep* doit être
+au moins 5 secondes. À l’heure actuelle, on ne sait pas pourquoi.
+
+
+
+
+

header <nom>=<valeur>

+

Ajoutez un en-tête <nom> avec la valeur donnée aux publicités. Utilisé pour transmettre des chaîne de caractères en tant +que métadonnées dans les publicités pour améliorer la prise de décision des consommateurs. Doit être utilisé +avec parcimonie. Il y a des limites sur le nombre d’en-têtes pouvant être utilisés, et la réduction de la +taille des messages d’annonce a des impacts importants sur la performance.

+
+
+

housekeeping <intervalle> (défaut: 300 secondes)

+

L’option housekeeping définit la fréquence d’exécution du traitement périodique tel que déterminé par +la liste des plugins on_housekeeping. Par défaut, il imprime un message de journal à chaque intervalle de housekeeping.

+
+
+

include config

+

inclure une autre configuration dans cette configuration.

+
+
+

inflight <string> (défaut: .tmp ou NONE si post_broker est définit)

+

L’option inflight définit comment ignorer les fichiers lorsqu’ils sont transférés +ou (en plein vol entre deux systèmes). Un réglage incorrect de cette option provoque des +transferts peu fiables, et des précautions doivent être prises. Voir +Delivery Completion pour plus de détails.

+

La valeur peut être un suffixe de nom de fichier, qui est ajouté pour créer un nom temporaire pendant +le transfert. Si inflight est défini a ., alors il s’agit d’un préfixe pour se conformer à +la norme des fichiers « cachés » sur unix/linux. +Si inflight se termine par / (exemple : tmp/ ), alors il s’agit d’un préfixe, et spécifie un +sous-répertoire de la destination dans lequel le fichier doit être écrit pendant qu’il est en vol.

+

Si un préfixe ou un suffixe est spécifié, lorsque le transfert est +terminé, le fichier est renommé à son nom permanent pour permettre un traitement ultérieur.

+

Lors de la publication d’un fichier avec sr3_post, sr3_cpost, watch, ou poll, l’option inflight +peut également être spécifié comme une intervalle de temps, par exemple, 10 pour 10 secondes. +Lorsque l’option est défini sur une intervalle de temps, le processus de publication de fichiers attends +jusqu’à ce que le fichier n’ai pas été modifié pendant cet intervalle. Ainsi, un fichier +ne peux pas être traité tant qu’il n’est pas resté le même pendant au moins 10 secondes. +C’est un effet pareil à un choix de valeur pou fileAgeMin.

+

Enfin, inflight peut être réglé a NONE. Dans ce cas, le fichier est écrit directement +avec le nom final, où le destinataire attendra de recevoir un poste pour notifier l’arrivée du fichier. +Il s’agit de l’option la plus rapide et la moins coûteuse lorsqu’elle est disponible. +C’est aussi le défaut lorsqu’un post_broker est donné, indiquant qu’un autre processus doit être +notifié après la livraison.

+

NOTE:

+
Lors de l'écriture d'un fichier, si vous voyez le message d'erreur ::
+
+paramètre en vol : 300, pas pour les téléchargements
+
+C'est parce que le réglage de l'intervalle de temps est uniquement pour la lecture des fichiers. Le processus
+qui écrit le fichier, ne peut pas contrôler combien de temps un processus lecteur ultérieur attendra pour
+regarder un fichier en cours téléchargé, il est donc inapproprié de spécifier un temps de modification minimum.
+en regardant les fichiers locaux avant de générer un post, ça ne sert pas comme disons, un moyen
+de retarder l'envoi des fichiers.
+
+
+
+
+

inline <flag> (défaut: False)

+

Lors de la publication de messages d’annonce, l’option inline est utilisée pour avoir le contenu du fichier +inclus dans le post. Cela peut être efficace lors de l’envoi de petits fichiers sur un niveau élevé de +liens de latence, un certain nombre d’allers-retours peuvent être enregistrés en évitant la récupération +des données utilisant l’URL. On ne devrait seulement utiliser inline pour des fichiers relativement petits. +Lorsque inline est actif, seuls les fichiers inférieurs à inlineByteMax octets +(défaut: 1024) auront réellement leur contenu inclus dans les messages d’annonce. +Si inlineOnly est défini et qu’un fichier est plus volumineux que inlineByteMax, le fichier +ne sera pas affiché.

+
+
+

inlineByteMax <taille>

+

la taille maximale des fichiers dont le contenu est à inclure dans un messages d’annonce (envoyé inline.)

+

Quand on inclut les données dans le message on l’encode dans un format choisi:

+
+
    +
  • text: le fichier doit être du utf-8 valide (ou érreur.)

  • +
  • binary: le fichier peut être encodé n’importe comment, il sera en format base64.

  • +
  • guess: essaie le format text en premier, utilise binary en cas d’erreur.

  • +
+
+
+
+

inlineOnly

+

ignorer les messages d´annonce si les données ne sont pas inline.

+
+
+

inplace <flag> (défaut: On)

+

Les fichiers volumineux peuvent être envoyés en plusieurs parties, plutôt que de tout en même temps. +Lors du téléchargement, si inplace est True, ces parties seront rajoutées au fichier +de manière ordonnée. Chaque partie, après avoir été insérée dans le fichier, est annoncée aux abonnés. +Cela peut être défini a False pour certains déploiements de Sarracenia où une pompe +ne voie que quelques parties, et non l’intégralité de fichiers en plusieurs parties.

+

L’option inplace est True par défaut. +Dépendamment de inplace et si le message d´annonce était une partie, le chemin peut +encore changer (en ajoutant un suffixe de pièce si nécessaire).

+
+
+

Instances

+

Parfois, une instance d’un composant et d’une configuration ne suffit pas pour traiter et envoyer toutes +les notifications disponibles.

+

instances <entier> (défaut:1)

+

L’option d’instance permet de lancer plusieurs instances d’un composant et d’une configuration. +Lors de l’exécution d’un sender par exemple, un nombre de fichiers d’exécution sont créés dans +le répertoire ~/.cache/sarra/sender/nomDeConfig

+
A .sender_nomDeConfig.state         est créé, contenant le nombre d’instances.
+A .sender_nomDeConfig_$instance.pid est créé, contenant le PID du processus $instance .
+
+
+

Dans le répertoire ~/.cache/sarra/log:

+
+

Un .sender_nomDeConfig_$instance.log est créé en tant que journal du processus $instance.

+
+
+

Note

+

Alors que les courtiers gardent les files d’attente disponibles pendant un certain temps, les files d’attente +prennent des ressources sur les courtiers, et sont nettoyés de temps en temps. Une fil d’attente qu’on +n’accède pas et a trop de fichiers (définis par l’implémentation) en fil d’attente seront détruits. +Les processus qui meurent doivent être redémarrés dans un délai raisonnable pour éviter la +perte de notifications. Une fil d’attente qu’on n’accède pas pendant une longue période +(dépendant de l’implémentation) sera détruite.

+
+
+
+

identity <string>

+

Tous les postes de fichiers incluent une somme de contrôle. Elle est placée dans l’en-tête du message amqp +et aura comme entrée sum avec la valeur de défaut ‘d,md5_checksum_on_data’. +L’option sum indique au programme comment calculer la somme de contrôle. +Dans la v3, elles sont appelées Identity methods (méthodes d’intégrité)

+
cod,x      - Calculer On Download en appliquant x
+sha512     - faire SHA512 sur le contenu du fichier (défaut)
+md5        - faire md5sum sur le contenu du fichier
+md5name    - faire la somme de contrôle md5sum sur le nom du fichier
+random     - inventer une valeur aléatoire pour chaque poste.
+arbitrary  - appliquer la valeur fixe littérale.
+
+
+

Les options v2 sont une chaîne de caractères séparée par des virgules. Les indicateurs de somme de contrôle valides sont :

+
    +
  • 0 : aucune somme de contrôle… la valeur dans le poste est un entier aléatoire (uniquement pour tester/débugger).

  • +
  • d : faire md5sum sur le contenu du fichier

  • +
  • n : faire la somme de contrôle md5sum sur le nom du fichier

  • +
  • p : faire la somme de contrôle SHA512 sur le nom du fichier et sur partstr [1]

  • +
  • s : faire SHA512 sur le contenu du fichier (défaut)

  • +
  • z,a : calculer la valeur de la somme de contrôle en utilisant l’algorithme a et l’assigner après le téléchargement.

  • +
+ +
+
+

logEvents ( défaut: after_accept,after_work,on_housekeeping )

+

l´ensemble des moments durant le traitement des message de notification ou on veut émettre des +messages de journal. Autres valeurs : on_start, on_stop, post, gather, … etc… +On peut débuter la valeur avec un plus (+) pour signifier un ajout au valeurs actuels. +la valeur moins (-) signifie la soustraction des valeurs de l´ensemble actuel.

+
+
+

logLevel ( défaut: info )

+

Niveau de journalisation exprimé par la journalisation de python. Les valeurs possibles sont : +critical, error, info, warning, debug.

+
+
+

logMetrics ( default: False )

+

écrire des métriques dans un fichier de quotidien pour la collecte de statistiques. +le fichier sera dans le même répertoire que les logs, et aura une suffix avec la date.

+
+
+

logReject ( défaut: False )

+

Normalement, le rejet des messages d´annonce se fait en silence. Lorsque logReject a la valeur True, un message +de journal est généré pour chaque message d´annonce rejeté et indiquant la raison du rejet.

+
+
+

logStdout ( défaut: False )

+

logStdout désactive la gestion des journaux. Il vaut mieux l’utiliser sur la ligne de commande, car il y a +certains risques de créer des fichiers stub avant que les configurations ne soient complètement analysées

+
sr3 --logStdout start
+
+
+

Tous les processus lancés héritent leurs descripteurs de fichier du parent. Donc toutes les sorties sont +comme une session interactive.

+

Cela contraste avec le cas normal, où chaque instance prend soin de ses journaux, en tournant et en purgeant +périodiquement. Dans certains cas, on veut que d’autres logiciels s’occupent de la journalisation, comme dans docker, +où c’est préférable que toute la journalisation soit une sortie standard.

+

Ça n’a pas été mesuré, mais il est probable que l’utilisation de logStdout avec de grandes configurations +(des dizaines d’instances configurés/processus) entraînera soit une corruption des journaux, ou limitera +la vitesse d’exécution de tous les processus qui écrivent à stdout.

+
+
+

logRotateCount <max_logs> ( défaut: 5 )

+

Nombre maximal de journaux de messages (logs) et statistiques (metrics) archivés.

+
+
+

logRotateInterval <intervalle>[<unité_de_temps>] ( défaut: 1d )

+

La durée de l’intervalle avec une unité de temps optionnel (soit 5m, 2h, 3d) +entre chaque changement de fichier journal (de messages et statistiques)

+
+
+

messageCountMax <count> (défaut: 0)

+

Si messageCountMax est supérieur à zéro, le flux se ferme après avoir traité le nombre de messages d´annonce spécifié. +Ceci est normalement utilisé pour le débogage uniquement.

+
+
+

messageRateMax <float> (défaut: 0)

+

Si messageRateMax est supérieur à zéro, le flux essaye de respecter cette vitesse de livraison en termes de +messages d´annonce par seconde. Notez que la limitation est sur les messages d´annonce obtenus ou générés par seconde, avant le +filtrage accept/reject. Le flux va dormir pour limiter le taux de traitement.

+
+
+

messageRateMin <float> (défaut: 0)

+

Si messageRateMin est supérieur à zéro et que le flux détecté est inférieur à ce taux, +un message d´annonce sera produit :

+
+
+

message_ttl <duration> (défaut: None)

+

L’option message_ttl définit un temps pour lequel un message d´annonce peut vivre dans la fil d’attente. +Après ce temps, le message d´annonce est retiré de la fil d’attente par le courtier.

+
+
+

mirror <flag> (défaut: off)

+

L’option miroir peut être utilisée pour mettre en miroir l’arborescence des fichiers de dd.weather.gc.ca. +Si l’option est défini a True le répertoire donné par l’option directory sera le nom de base +de l’arborescence. Les fichiers acceptés sous ce répertoire seront placé sous le sous-répertoire +de l’arborescence où il réside dans dd.weather.gc.ca. +Par exemple, récupérer l’URL suivante, avec des options:

+
http://dd.weather.gc.ca/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif
+
+  mirror    True
+  directory /monrépertoirelocal
+  accept    .*RADAR.*
+
+
+

entraînerait la création des répertoires et du fichier +/monrépertoirelocal/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif +Les paramètres de mirror peuvent être modifiés entre les options de répertoire.

+
+
+

no <count>

+

Présent sur les instances démarrées par l’interface de gestion sr3. +L’option no est seulement utilisée sur la ligne de commande et n’est pas destinée aux utilisateurs. +Il s’agit d’une option à utiliser par sr3 lors de la génération (spawning) d’instances pour informer chaque processus +de quelle instance il s’agit. Par exemple, l’instance 3 sera générée avec –no 3

+
+
+

nodupe_basis <donnes|nom|chemin> (défaut: chemin)

+

Une option sous forme de mot-clé (alternative: cache_basis ) pour identifier quels fichiers sont comparés +à des fins de suppression des doublons. Normalement, la suppression des doublons utilise l’intégralité du +chemin d’accès pour identifier les fichiers qui n’ont pas été modifiés. Cela permet aux fichiers avec un contenu +identique d’être publié dans différents répertoires et de ne pas être supprimé. Dans certains cas +cas, la suppression de fichiers identiques devrait être effectuée quel que soit l’endroit où se trouve +le fichier. Définissez ‘nom’ pour les fichiers de nom identique, mais qui sont dans des répertoires +différents pour qu’ils puissent être considéré comme des doublons. Définissez ‘données’ pour n’importe quel fichier, +quel que soit le nom, pour qu’il puisse être considéré comme un doublon si la somme de contrôle correspond.

+

Ceci est implémenté en tant qu’alias pour :

+
+

callback_prepend nodupe.name

+
+

ou:

+
+

callback_prepend nodupe.data

+
+

Pour plus d´information: Supprimer les doublons

+
+
+

fileAgeMax

+

Si les fichiers sont plus anciens que ce paramètre (défaut: 30d), ignorez-les, ils sont trop +ancien pour qu’il puisse être posté.

+
+
Dans un Poll :
    +
  • La valeur par défaut est 7 heures. doit être inférieur à nodupe_ttl pour empêcher la réabsorption de données en double.

  • +
+

(discussion complète ici (en anglais): https://github.com/MetPX/sarracenia/issues/904)

+
+
+
+
+

fileAgeMin

+

Si les fichiers sont plus neuf que ce paramètre (défaut: 0 … désactivé), ignorez-les, ils sont trop +neufs pour qu’ils puissent être postés.

+
+
+

nodupe_ttl <off|on|999[smhdw]>

+

Lorsque nodupe_ttl est défini à une intervalle de temps +qui est différente de zéro, chaque nouveau message d´annonce est comparé à ceux reçus dans cette intervalle, pour vérifier si +c’est un doublon. Les doublons ne sont pas traités ultérieurement. Qu’est-ce qu’un doublon ? Un fichier avec +le même nom (y compris l’en-tête des pièces) et la même somme de contrôle. A chaque intervalle de hearbeat, un +processus de nettoyage recherche les fichiers dans la cache qui n’ont pas été consultés pendant cache secondes, +et les supprime, afin de limiter la taille de la cache. De différents paramètres sont approprié pour de différents +cas d’utilisation.

+

Un intervalle d’entier brut est en secondes sauf si le suffixe m, h, d ou w est utilisé. Dans ce cas l’intervalle +est en minutes, heures, jours ou semaines respectivement. Après l’expiration de l’intervalle, le contenu est +abandonné, de sorte que les doublons séparés par une intervalle suffisamment grande passeront. +Une valeur de 1d (jour) ou 1w (semaine) est appropriée. Définir l’option sans spécifier +un temps correspondra à 300 secondes (ou 5 minutes) comme intervalle d’expiration.

+

L’utilisation de la cache est incompatible avec la stratégie de défaut *parts 0*, il faut spécifier une +stratégie alternative. Il faut utiliser soit une taille de bloc fixe, ou ne jamais partitionner les fichiers. +Il faut éviter l’algorithme dynamique qui modifiera la taille de la partition utilisée au fur et à +mesure qu’un fichier grandit.

+

Notez que le stockage de suppression de doublons est local à chaque instance. Lorsqu’un nombre N d’instances partagent +une fil d’attente, la première fois qu’une publication est reçue, elle peut se faire choisir par une instance, +et si un doublon est ensuite reçu, il sera probablement choisi par une autre instance. +Pour une suppression efficace des doublons avec les instances, il faut déployer deux couches d’abonnés. +Utiliser une première couche d’abonnés (shovels) avec la suppression de doublons éteinte et +utiliser post_exchangeSplit pour la sortie. Cela achemine les publications en utilisant la somme de contrôle vers +une deuxième couche d’abonnés (winnow) dont les caches de suppression des doublons sont actives.

+
+
+

outlet post|json|url (défaut: post)

+

REMARQUE: PAS IMPLEMENTÉ dans sr3, devrait revenir dans la version future +L’option outlet est utilisée pour permettre l’écriture d’un poste a un fichier au lieu de +l’afficher à un courtier. Les valeurs d’argument valides sont les suivantes :

+

post:

+
+

poster un messages d´annonce a un post_exchange

+

post_broker amqp{s}://<utilisateur>:<mot-de-passe>@<hoteDuCourtier>[:port]/<vhost> +post_exchange <nom> (OBLIGATOIRE) +post_topicPrefix <string> (défaut: “v03”) +on_post <script> (défaut: None)

+

Si aucun courtier n’est fourni, le post_broker sera défini par le courtier d’entrée par défaut. +Il suffit de définir l’option a un autre courtier si vous souhaitez envoyer les notifications +ailleurs.

+

Le post_exchange doit être défini par l’utilisateur. C’est l’échange sous lequel +les notifications seront publiées.

+
+

json:

+
+

écrire chaque message d´annonce en sortie standard, un par ligne dans le même format json que celui utilisé pour +l’enregistrement et la restauration de la fil d’attente par l’implémentation python.

+
+

url:

+
+

il suffit de sortir l’URL de récupération vers la sortie standard.

+
+

FIXME: L’option outlet provient de l’implémentation C ( sr3_cpump ) et elle n’a pas +a été beaucoup utilisé dans l’implémentation python.

+
+
+

overwrite <flag> (défaut: off)

+

L’option overwrite, si définie a false, évite les téléchargements inutiles sous ces conditions :

+

1- le fichier à télécharger se trouve déjà dans le système de fichiers de l’utilisateur et est au bon endroit

+

2- la somme de contrôle du message amqp correspond à celle du fichier.

+

Le défaut est False.

+
+
+

path <chemin>

+

post évalue le chemin d’accès du système de fichiers à partir de l’option path +et éventuellement post_baseDir si cette option est utilisée.

+

Si un chemin d’accès définit un fichier, ce fichier est surveillé.

+

Si ce chemin définit un répertoire, tous les fichiers de ce répertoire sont +surveillé et si watch trouve un (ou plusieurs) répertoire(s), il +les regarde de manière récursive jusqu’à ce que toute l’arborescence soit scanné.

+

Les annonces AMQP consistent des champs de l’arborescence, de l’heure d’annonce, +la valeur de l’option url, et FIXME: and the resolved paths to which were withdrawn +the post_baseDir present and needed.

+
+
+

permDefault, permDirDefault, permLog, permCopy

+

Les bits d’autorisation sur les fichiers de destination écrits sont contrôlés par les directives permCopy. +permCopy appliquera les autorisations de mode publiées par la source du fichier. +Si aucun mode de source est disponible, le permDefault sera appliqué aux fichiers et le +permLog sera appliqué aux répertoires. Si aucun défaut est spécifié, les défauts du système d’exploitation +(sur linux, contrôlé par les paramètres umask) déterminera les autorisations du fichier. +(Notez que l’option chmod est interprétée comme un synonyme pour permDefault, +et chmod_dir est un synonyme de permDirDefault).

+

Lorsqu’il est défini dans un composant de posting, permCopy peut soit inclure ou exclure +l’en-tête mode des messages d´annonce.

+

lorsqu’il est défini dans un composant de polling, permDefault définit les autorisations minimales pour +qu’un dossier puis être accepté.

+

(sur une sortie ls qui ressemble à : —r—–, comme une commande chmod 040 <fichier> ). +Les options permDefault spécifient un masque, c’est-à-dire que les autorisations doivent être +au moins ce qui est spécifié.

+
+
+

persistent <flag> (défaut: True)

+

L’option persistent définit le delivery_mode pour un message AMQP. Lorsqu’il +est choisi, les messages persistants seront publiés (delivery_mode=2). Lorsqu’ils +ne le sont pas, ils seront placés en mode transitoire (non durables, delivery_mode=1).

+

Les messages persistants sont écrits sur le disque par le courtier et survivront au +redémarrage du courtier. Tous les messages transitoires dans une file d’attente seront +perdus au redémarrage d’un courtier. Une remarque : les messages persistants ne survivront +pas le redémarrage du courtier quand ils résident dans une file d’attente durable. +Non-durable les files d’attente, y compris tous les messages qu’elles contiennent, +seront perdues au redémarrage d’un courtier.

+
+
+

pollUrl

+

Spécification de ressources d´une serveur à sonder +Voir Guide de ligne de commande pour plus +d´informations.

+
+
+

post_baseDir <chemin>

+

L’option post_baseDir fournit le chemin d’accès au répertoire qui, lorsqu’il est combiné (ou trouvé) +dans le path donné, donne le chemin absolu local au fichier de données à publier. +La partie post_baseDir du chemin d’accès sera supprimée de l’annonce publiée. +Pour les URL sftp, il peut être approprié de spécifier un chemin d’accès relatif à un compte d’utilisateur. +Exemple de cette utilisation serait: –post_baseDir ~utilisateur –url sftp:utilisateur@hote +Pour les fichiers : url, baseDir n’est généralement pas approprié. Pour publier un chemin absolu, +omettez le paramètre –post_baseDir et spécifiez simplement le chemin d’accès complet en tant qu’argument.

+
+
+

post_baseUrl <url>

+

L’option post_baseUrl définit comment obtenir le fichier… il définit le protocole, +l’hôte, le port et, l’utilisateur (facultatif). Il est recommandé de ne pas inclure de +mots de passe dans les URLs.

+
+
+

post_broker <url>

+

l’URL du courtier pour publier des messages d’annonce. Voir broker pour plus de détails.

+
+
+

post_exchange <name> (défaut: xpublic)

+

FIXME: L’option post_exchange est définie sous quelle échange la nouvelle notification +sera affiché. Lors de la publication sur une pompe en tant qu’administrateur, un +choix commun pour post_exchange est ‘xpublic’.

+

Lors de la publication d’un produit, un utilisateur peut démarrer un script en utilisant +un point d’entrée de rappel de flux (flow callback) tels que after_accept et after_work +pour modifier les messages d’annonce générés à propos des fichiers avant leur publication.

+
+
+

post_exchangeSplit <compte> (défaut: 0)

+

L’option post_exchangeSplit ajoute un suffixe à deux chiffres qui est crée en hachant le dernier caractère +de la somme de contrôle avec le nom de post_exchange, afin de répartir la production entre un certain nombre d’échanges. +Ceci est actuellement utilisé dans les pompes à trafic élevé pour avoir plusieurs instances de winnow, +qui ne peuvent pas être instancié de la manière normale. Exemple:

+
post_exchangeSplit 5
+post_exchange xwinnow
+
+
+

entraînera la publication de messages d’annonce sur cinq échanges nommés : xwinnow00, xwinnow01, +xwinnow02, xwinnow03 et xwinnow04, où chaque échange ne recevra qu’un cinquième +du flux total.

+
+
+

post_format <name> (défaut: v03)

+

Définit le format de message pour les messages publiés. les valeurs actuellement incluses sont :

+
    +
  • v02 … utilisé par toutes les pompes de données existantes dans la plupart des cas.

  • +
  • v03 … par défaut au format sr3 JSON plus facile à utiliser.

  • +
  • wis … un format expérimental geoJSON en flux pour l’Organisation météorologique mondiale

  • +
+

Lorsqu’elle est fournie, cette valeur remplace tout ce qui peut être déduit de post_topicPrefix.

+
+
+

post_on_start

+

Lors du démarrage de watch, on peut soit demander au programme de publier tous les fichiers dans les répertoires +surveillés, ou pas. (pas implanté en sr3_cpost)

+
+
+

post_topic <chaine>

+

Définissez explicitement une chaîne de sujet de publication, en remplaçant l’habituel +groupe de paramètres. Pour les pompes de données Sarracenia, cela ne devrait jamais être nécessaire, +car l’utilisation de post_exchange, post_topicPrefix et le relPath construit normalement le bon +valeur pour les sujets à la fois pour la publication et la liaison.

+
+
+

post_topicPrefix (défaut: topicPrefix)

+

Rajouter au subtopic pour former une hiérarchie complète des sujets. +Cette option s’applique à la publication. Elle indique la version des messages d’annonce publiés +dans les subtopics. (v03 fait référence à sr3_post.7.html) Cette valeur par défaut est défini par tout ce qui +a été reçue.

+
+
+

prefetch <N> (défaut: 1)

+

L’option prefetch définit le nombre de messages d’annonce à récupérer en même temps. +Lorsque plusieurs instances sont en cours d’exécution et que prefetch est égale à 4, chaque instance obtient jusqu’à quatre +messages d’annonce à la fois. Pour réduire le nombre de messages d’annonce perdus si une instance meurt et qu’elle a le +partage de charge optimal, prefetch doit être réglée le plus bas possible. Cependant, sur des long haul links (FIXME), +il faut augmenter ce nombre pour masquer la latence d’aller-retour, donc un réglage de 10 ou plus est nécessaire.

+
+
+

queueName|queue|queue_name|qn

+
    +
  • queueName <nom>

  • +
+

Par défaut, les composants créent un nom de fil d’attente qui doit être unique. Par défaut, le +queue_name crée par les composants suit la convention suivante :

+
+

q_<utilisateurDeCourtier>.<nomDuProgramme>.<nomDeConfig>.<aléatoire>.<aléatoire>

+
+

Ou:

+
    +
  • utilisateurDeCourtier est le nom d’utilisateur utilisé pour se connecter au courtier (souvent: anonymous )

  • +
  • nomDuProgramme est le composant qui utilise la fil d’attente (par exemple subscribe ),

  • +
  • nomDeConfig est le fichier de configuration utilisé pour régler le comportement des composants.

  • +
  • aléatoire n’est qu’une série de caractères choisis pour éviter les affrontements quand plusieurs +personnes utilisent les mêmes configurations

  • +
+

Les utilisateurs peuvent remplacer le défaut à condition qu’il commence par q_<utilisateurDeCourtier>.

+

Lorsque plusieurs instances sont utilisées, elles utilisent toutes la même fil d’attente, pour faire plusieurs +taches simples à la fois. Si plusieurs ordinateurs disposent d’un système de fichiers domestique partagé, le +queue_name est écrit à :

+
+

~/.cache/sarra/<nomDuProgramme>/<nomDeConfig>/<nomDuProgramme>_<nomDeConfig>_<utilisateurDeCourtier>.qname

+
+

Les instances démarrées sur n’importe quel nœud ayant accès au même fichier partagé utiliseront la +même fil d’attente. Certains voudront peut-être utiliser l’option queue_name comme méthode plus explicite +de partager le travail sur plusieurs nœuds.

+
+
+

queueBind

+

Au démarrage, par défaut, Sarracenia redéclare les ressources et les liaisons pour s’assurer qu’elles sont à jour. +Si la fil d’attente existe déjà, ces indicateurs peuvent être défini a False, afin qu’aucune tentative de déclaration +ne soit effectuée pour fil d’attente ou pour ses liaisons. Ces options sont utiles sur les courtiers qui ne +permettent pas aux utilisateurs de déclarer leurs files d’attente.

+
+
+

queueDeclare <flag> (défaut: True)

+

Avec l´option queueDeclare à True, un composant déclare un fil d´attente pour accumuler des messages d’annonce lors +de chaque démarrage. Des fois les permissions sont restrictifs sur les courtiers, alors on ne peut pas +faire de tels déclarations de ressources. Dans ce cas, il faut supprimer cette déclaration.

+
+
+

randomize <flag>

+

Actif si -r|–randomize apparaît dans la ligne de commande… ou randomize est défini +à True dans le fichier de configuration utilisé. S’il y a plusieurs postes parce que +le fichier est publié par bloc (l’option blocksize a été définie), les messages d’annonce de bloc +sont randomisés, ce qui signifie qu’ils ne seront pas affichés.

+
+
+

realpathAdjust <compte> (Experimental) (défaut: 0)

+

L’option realpathAdjust ajuste le nombre d’éléments de chemin de fichier résolus avec la routine realpath +provenant du bibliotech standard C. Le nombre indique combien d’éléments de chemin doivent être ignorés, en comptant +depuis le début du chemin avec des nombres positifs, ou la fin avec des nombres négatifs. Le défaut est zéro +indiquant que le chemin au complet est résolu.

+
+
+

realpathFilter <flag> (Expérimentale)

+

l’option realpathFilter résout les chemins à l’aide de la routine de bibliothèque realpath standard C, +mais uniquement dans le but d’appliquer des filtres d’acceptation de rejet. Ceci est utilisé uniquement pendant +affectation.

+

Cette option est utilisée pour étudier certains cas d’utilisation et pourrait disparaître à l’avenir.

+

Implémenté en C, mais pas python actuellement.

+
+
+

realpathPost <flag> (Expérimentale)

+

L’option realpathPost résout les chemins donnés à leurs chemins canoniques, éliminant ainsi +toute indirection via des liens symboliques. Le comportement améliore la capacité de watch à +surveiller l’arborescence, mais l’arborescence peut avoir des chemins complètement différents de ceux des arguments +donné. Cette option impose également la traversée de liens symboliques.

+

Cette option est utilisée pour étudier certains cas d’utilisation et pourrait disparaître à l’avenir.

+
+
+

sendTo <url>

+

Specification du serveur auquel on veut livrer des données (dans un sender)

+
+
+

rename <chemin>

+

Avec l’option renommer, l’utilisateur peut suggérer un chemin de destination pour ses fichiers. Si le +chemin se termine par ‘/’ il suggère un chemin de répertoire… Si ce n’est pas le cas, l’option +spécifie un changement de nom de fichier.

+
+
+

report et report_exchange

+

REMARQUE: PAS IMPLEMENTÉ dans sr3, devrait revenir dans la version future +Pour chaque téléchargement, par défaut, un message de rapport amqp est renvoyé au courtier. +Cela se fait avec l’option :

+
    +
  • rapport <flag> (défaut: True)

  • +
  • report_exchange <report_exchangename> (défaut: xreport|xs_*nomUtilisateur* )

  • +
+

Lorsqu’un rapport est généré, il est envoyé au report_exchange configuré. Les composants administratifs +publient directement sur xreport, tandis que les composants d’utilisateur publient sur leur +échanges (xs_*nomUtilisateur*). Les démons de rapport copient ensuite les messages dans xreport après validation.

+

Ces rapports sont utilisés pour le réglage de la livraison et pour les sources de données afin de générer des +informations statistiques. Définissez cette option a False, pour empêcher la génération de ces rapports.

+
+
+

reset <flag> (défaut: False)

+

Lorsque reset est défini et qu’un composant est (re)démarré, sa fil d’attente est +supprimé (si elle existe déjà) et recréé en fonction des options de fil d’attente. +C’est à ce moment-là qu’une option de courtier est modifiée, car le courtier refusera +l’accès à une fil d’attente déclarée avec des options différentes de celles qui étaient +défini à la création. Cette option peut également être utilisé pour supprimer rapidement une fil d’attente +lorsqu’un récepteur a été fermé pendant une longue période de temps. Si la suppression des doublons est active, alors +la cache de réception est également supprimé.

+

Le protocole AMQP définit d’autres options de fil d’attente qui ne sont pas exposées +via Sarracenia, parce que Sarracenia choisit soi-même des valeurs appropriées.

+
+
+

retryEmptyBeforeExit: <booléen> (défaut: False)

+

Utilisé pour les tests de flux de sr_insects. Empêche Sarracenia de quitter lorsqu’il reste des messages d’annonce dans la file +d’attente de nouvelles tentatives (retry queue). Par défaut, une publication quitte proprement une fois qu’elle a +créé et tenté de publier des messages d’annonce pour tous les fichiers du répertoire spécifié. Si des messages d’annonce ne sont pas +publiés avec succès, ils seront enregistrés sur le disque pour réessayer ultérieurement. Si une publication n’est +exécutée qu’une seule fois, comme dans les tests de flux, ces messages d’annonce ne seront jamais réessayés, sauf si +retryEmptyBeforeExit est défini à True.

+
+
+

retry_refilter <boolean> (par défaut : False)

+

L’option retry_refilter modifie la façon dont les messages sont rechargés lorsqu’ils sont récupérés à partir +d’une file d’attente pour une nouvelle tentive de transfer (retry). La méthode par défaut (valeur : False) +consiste à répéter le transfert en utilisant exactement le même message que précédemment. Si retry_refilter +est défini (valeur : True), alors tous les Les champs calculés du message + seront supprimés et le +traitement redémarrera à partir de la phase gather (le traitement d’acceptation/rejet sera +répété, les destinations recalculées.)

+

Le comportement normal de nouvelle tentative (retry) est utilisé lorsque la destination a subit une +panne et doit renvoyer plus tard, tandis que l’option retry_refilter est utilisée lors de la récupération +de la configuration

+
+
+

retry_ttl <duration> (défaut: identique à expire)

+

L’option retry_ttl (nouvelle tentative de durée de vie) indique combien de temps il faut continuer à essayer d’envoyer +un fichier avant qu’il ne soit rejeté de la fil d’attente. Le défaut est de deux jours. Si un fichier n’a pas +été transféré après deux jours de tentatives, il est jeté.

+
+
+

sanity_log_dead <interva;le> (défaut: 1.5*housekeeping)

+

L’option sanity_log_dead définit la durée à prendre en compte avant de redémarrer un composant.

+
+
+

scheduled_interval,scheduled_hour,scheduled_minute

+

Lorsque vous travaillez avec des flux cédulés, tels que des sondages, vous pouvez configurer une durée +(unité: seconde par défaut, suffixes : m-minute, h-heure) à laquelle exécuter un +sondage devrait être lancer:

+
scheduled_interval 30
+
+
+

Ceci partirai le flux ou sondage toutes les 30 secondes. Si aucune scheduled_interval n’est +définie, alors La classe flowcb.scheduled.Scheduled recherchera les deux autres +spécificateurs de temps

+
scheduled_hour 1,4,5,23
+scheduled_minute 14,17,29
+
+
+

afin de specifier de partir un sondage chaque jour à: 01h14, 01h17, 01h29, puis les mêmes minutes +après chacune des 4h, 5h et 23h.

+
+
+

shim_defer_posting_to_exit (EXPERIMENTAL)

+

(option spécifique à libsrshim) +Reporte la publication des fichiers jusqu’à ce que le processus se ferme. +Dans les cas où le même fichier est ouvert et modifiée à plusieurs reprises, ceci +peut éviter les publications redondantes. (défaut: False)

+
+
+

shim_post_minterval interval (EXPERIMENTAL)

+

(option spécifique à libsrshim) +Si un fichier est ouvert pour écriture et fermé plusieurs fois dans l’intervalle, +il ne sera affiché qu’une seule fois. Lorsqu’on écrit dans un fichier plusieurs fois, en particulier +dans un script shell, de nombreux postes sont créés, et les scripts shell affecte la performance. +Dans tous les cas, les abonnés ne seront pas en mesure de faire des copies assez rapidement, donc +il y a peu d’avantages à avoir 100 messages d’annonce du même fichier dans la même seconde pa exemple. +Il est prudent de fixer une limite maximale à la fréquence de publication d’un fichier donné. (défaut: 5s) +Remarque: si un fichier est toujours ouvert ou a été fermé après son post précédent, alors +pendant le traitement de sortie du processus, il sera à nouveau publié, même si l’intervalle +n’est pas respecté, afin de fournir le message d’annonce final le plus précis.

+
+
+

shim_skip_parent_open_files (EXPERIMENTAL)

+

(option spécifique à libsrshim) +L’option shim_skip_ppid_open_files signifie qu’un processus vérifie si le processus parent a le même fichier +ouvert et ne poste pas si c’est le cas. (défaut: Vrai)

+
+
+

sleep <temps>

+

Temps d’attente entre la génération d’événements. Lorsqu’on écrit fréquemment à des fichiers, c’est inutile +de produire un poste pour chaque changement, car il peut produire un flux continu de changements où les transferts +ne peut pas être fait assez rapidement pour suivre le rythme. Dans de telles circonstances, on peut regrouper toutes +les modifications apportées à un fichier pendant le temps de sleep, et produire un seul poste.

+

Lorsque sleep est donné une valeur >0 pour être utilisé avec un poll, cela a pour effet de +définir scheduled_interval, pour des raisons de compatibilité. +Il est préférable que le sondage utilise explicitement les paramètres scheduled_interval, +scheduled_hour, et/ou scheduled_minute plutôt que sleep.

+
+
+

statehost <booléen> ( défaut: False )

+

Dans les grands centres de données, le répertoire de base peut être partagé entre des milliers de +nœuds. Statehost ajoute le nom du nœud après le répertoire de cache pour le rendre +unique à chaque nœud. Ainsi, chaque nœud a ses propres fichiers d’état et journaux. +Par exemple, sur un nœud nommé goofy, ~/.cache/sarra/log/ devient ~/.cache/sarra/goofy/log/.

+
+
+

strip <count|regexp> (défaut: 0)

+

Il est possible de modifier les répertoires en miroir relatifs à l’aide de l’option strip. +Si elle est défini à N (un entier), les premiers répertoires ‘N’ du chemin relatif +sont supprimés. Par exemple:

+
http://dd.weather.gc.ca/radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.gif
+
+  mirror    True
+  strip     3
+  directory /monrépertoirelocal
+  accept    .*RADAR.*
+
+
+

entraînerait la création des répertoires et du fichier +/monrépertoirelocal/WGJ/201312141900_WGJ_PRECIP_SNOW.gif. +Lorsqu’un regexp est fourni à la place d’un nombre, cela indique un modèle à supprimer +du chemin relatif. Par exemple, si

+
strip  .*?GIF/
+
+
+

Le fichier sera aussi placé au même emplacement. +Notez que les paramètres de strip peuvent être modifiés entre les options de répertoire.

+
+
REMARQUE::

avec strip, l’utilisation du modificateur ? (pour éviter l’expression régulière greediness) est souvent utile. +Cela garantit que le match le plus court est utilisé.

+

Par exemple, avec un nom de fichier : radar/PRECIP/GIF/WGJ/201312141900_WGJ_PRECIP_SNOW.GIF +L’expression : .*?GIF correspond à : radar/PRECIP/GIF +alors que l’expression : .*GIF correspond au nom entier.

+
+
+
+
+

sourceFromExchange <flag> (défaut: off)

+

L’option sourceFromExchange est principalement destinée aux administrateurs. +Si les messages d’annonce reçus sont postés directement à partir d’une source, l’échange utilisé +est «xs_<nomUtilisateurSourceDuCourtier>». Ces messages d’annonce pourraient manquer les en-têtes source et from_cluster, +ou un utilisateur malveillant peut définir des valeurs incorrectes. +Pour se protéger contre ces deux problèmes, les administrateurs doivent définir l’option sourceFromExchange.

+

Lorsque l’option est définie, les valeurs des en-têtes de source et from_cluster du message d’annonce seront alors remplacées

+
self.msg.headers['source']       = <utilsateurDuCourtier>
+self.msg.headers['from_cluster'] = cluster
+
+
+

Cela va remplacer toutes les valeurs présentes dans le message d’annonce. Ce paramètre doit toujours être utilisé +lors de l’ingestion de données à partir d’un échange d’utilisateur. Ces champs sont utilisés pour renvoyer +les rapports à l’origine des données injectées. Cela est généralement combiné avec:

+
*mirror true*
+*sourceFromExchange true*
+*répertoire ${PBD}/${YYYYMMDD}/${SOURCE}*
+
+
+

Pour que les données arrivent dans l’arborescence de format standard.

+
+
+

sourceFromMessage <flag> (défaut: off)

+

L’option sourceFromMessage est principalement destinée aux administrateurs. +Normalement, le champ source d’un message entrant est ignoré. +Lorsque cette option est définie, le champ du message est accepté et utilisé +pour le traitement. (remplace source et sourceFromExchange )

+

Il est désactivé par défaut car les messages malveillants peuvent déformer les données +origine. À utiliser uniquement avec des flux de données fiables et organisés de manière responsable.

+
+
+

subtopic <modèle amqp> (défaut: #)

+

Dans les publications d’un échange, le paramètre de subtopic restreint la sélection du produit. +Pour donner la bonne valeur au subtopic, on a le choix de filtrer en utilisant subtopic seulement avec le +wildcarding limité d’AMQP et une longueur limitée à 255 octets encodés, ou de manière plus puissante, les expressions régulière +basés sur les mécanismes accept/reject décrits ci-dessous. La différence est que le +le filtrage AMQP est appliqué par le courtier lui-même, ce qui évite que les avis soient livrés. +aux clients. Les modèles accept/reject s’appliquent aux messages d’annonce envoyés par le +courtier à l’abonné. En d’autres termes, accept/reject sont des filtres côté client, +alors que subtopic est le filtrage côté serveur.

+

Il est recommandé d’utiliser le filtrage côté serveur pour réduire le nombre d’annonces envoyées +au client et envoyer seulement ce qui est pertinent, et seulement régler les mécanismes côté client, +économisant du bandwidth et du traitement pour tous.

+

topicPrefix est principalement utilisé lors des transitions de version de protocole, +où l’on souhaite spécifier une version de protocole non-commune des messages d’annonce auquel s’abonner.

+

Normalement, l’utilisateur spécifie un échange et plusieurs options de subtopic. subtopic est ce qui est +normalement utilisé pour indiquer les messages d’annonce d’intérêt. Pour utiliser subtopic pour filtrer les produits, +il faut que la chaîne de caractère subtopic corresponde au chemin relatif du produit.

+

Par exemple, en consommant à partir de DD, pour donner la bonne valeur au subtopic, il est possible de +parcourir le site Web http://dd.weather.gc.ca et noter tous les répertoires +d’intérêt. Pour chaque arborescence de répertoires d’intérêt, il faut écrire une option de subtopic +comme cela:

+

subtopic repertoire1.*.sous-repertoire3.*.sous-repertoire5.#

+
ou:
+      *                correspond à un nom de répertoire unique
+      #                correspond à toute arborescence de répertoires restants
+
+
+
+
Remarque:

Lorsque les répertoires ont ces wild-cards, ou espaces dans leurs noms, ils +sera encodé par l’URL ( ‘#’ devient %23 ). +Lorsque les répertoires ont des points dans leur nom, cela changera +la hiérarchie des sujets.

+
+
FIXME:

les marques de hachage sont substituées à l’URL, mais n’ont pas vu le code pour les autres valeurs. +Vérifiez si les astérisques dans les noms de répertoire dans les rubriques doivent être encodés par l’URL. +Vérifiez si les points dans les noms de répertoire dans les rubriques doivent être encodés par l’URL.

+
+
+
+
+

On peut utiliser plusieurs liaisons à plusieurs échanges comme cela:

+
échange A
+subtopic repertoire1.*.repertoire2.#
+
+échange B
+subtopic *.repertoire4.#
+
+
+

Cela va déclarer deux liaisons différentes à deux échanges différents et deux arborescences de fichiers différentes. +Alors que la liaison par défaut consiste à se lier à tout, certains courtiers pourraient ne pas permettre aux +clients à définir des liaisons, ou on peut vouloir utiliser des liaisons existantes. +On peut désactiver la liaison de fil d’attente comme cela:

+
subtopic None
+
+
+

(False, ou off marchera aussi.)

+
+
+

sundew_compat_regex_first_match_is_zero (default: Faux)

+

Lors de la numérotation des groupes dans les modèles de correspondance, les groupes Sundew commencent à 0. +Les expressions régulières Python utilisent le groupe zéro pour représenter la chaîne entière et chaque correspondance +le groupe commence à 1. Il est considéré comme moins surprenant de se conformer aux conventions Python, +mais le faire unilatéralement romprait la compatibilité. Voici donc un switch à utiliser +lors du pontage entre Sundew, sarra v2 et sr3. Finalement, cela devrait toujours être désactivé. +Exemples:

+ +

Pour obtenir le même résultat lorsque le option est faux:

+
    +
  • sundew_compat_regex_first_match_is_zero: False

  • +
  • directory corréspondant: /tmp/meteocode/${3}/${1}/${2}

  • +
+

Le monde qui fouille sur les ressources sur les expressions régulière de python +seront moins surpris par cette dernière convention.

+
+
+

timeCopy (défaut: on)

+

Sur les systèmes de type Unix, lorsque la commande ls ou un navigateur de fichiers affiche une modification ou un +temps d’accès, il s’agit d’un affichage des éléments posix st_atime et st_ctime d’un struct renvoyé par l’appel +stat(2). Lorsque timeCopy est activé, les en-têtes qui reflètent ces valeurs dans les messages d’annonce sont utilisés +pour restaurer l’accès et la modification des heures respectivement sur le système de l’abonné. Pour documenter +le retard de la réception des fichiers, cette option peut être désactivée, puis les temps du fichier sur la +source et la destination sont comparés.

+

Lorsqu’il est défini dans un composant de publication, les en-têtes atime et mtime des messages d’annonce sont éliminés.

+
+
+

timeout <intervalle> (défaut: 0)

+

L’option timeout définit le nombre de secondes à attendre avant d’interrompre un +transfert de connexion ou de téléchargement (appliqué pendant le transfert).

+
+
+

timezone <chaine> (défaut: UTC)

+

Établir le fuseau horaire pour les dates afficher pour les fichiers sur un serveur FTP. +La valeur est tel que décrit timezone as per pytz +exemples: Canada/Pacific, Pacific/Nauru, Europe/Paris +Seulement actif dans le contexte de sondage de serveur FTP.

+
+
+

tlsRigour (défaut: medium)

+

tlsRigour peut être réglé a : lax, medium ou strict, et donne un indice à l’application par rapport à la +configuration des connexions TLS. TLS, ou Transport Layer Security (autrefois appelée Secure Socket Layer (SSL)) +est l’encapsulation de sockets TCP normales en cryptage standard. Il existe de nombreux aspects de +négociations TLS, vérification du nom d’hôte, vérification des certificats, validation, choix de +chiffrement. Ce qui est considéré comme sécuritaire évolue au fil du temps, de sorte que les paramètres +qui étaient considérés comme sécuritaire il y a quelques années, sont actuellement déconseillés. +Ceci mène naturellement à des difficultés de communication à cause de différents +niveaux de conformité par rapport à ce qui est couramment défini comme un cryptage rigoureux.

+

Par exemple, si on se connecte à un site et que son certificat est expiré, et +qu’il est quand même nécessaire de l’utiliser, alors définir tlsRigour a lax pourra +permettre la connexion de réussir.

+
+
+

topic <chaine>

+

Définissez explicitement une chaîne de sujet d’abonnement ou de publication, en remplaçant la valeur +dériver à partir de l’habituel groupe de paramètres. Pour les pompes de données Sarracenia, cela ne +devrait jamais être nécessaire, car l’utilisation de l’exchange, topicPrefix et subtopic +construit normalement le bon valeur.

+
+
+

topicPrefix (défaut: v03)

+

rajouté au subtopic pour former une hiérarchie complète de thèmes (topics). +Cette option s’applique aux liaisons d’abonnement. +Indique la version des messages d’annonce reçus dans les subtopics. (V03 fait référence à sr3_post.7.html)

+
+
+

topicCopy (défaut: False)

+

Définir topicCopy à true indique à sarracenia de transmettre les topic des messages sans modification. +Sarracenia a une convention sur la manière dont les topic des produits sont organisés. Il y a +un topicPrefix, suivi de subtopic (sous-thèmes) dérivés du champ relPath du message. +Certains réseaux peuvent choisir d’utiliser des conventions thématiques différentes, externes à la sarracenia.

+
+
+

users <flag> (défaut: false)

+

Utiliser comme complément lorsque l’action declare est utilisée, pour demander à sr3 de déclarer des utilisateurs +sur le courtier, ainsi que les files d’attente et les échanges.

+
+
+

v2compatRenameDoublePost <flag> ( default: false)

+

la version 3 de Sarracenia propose une logique améliorée autour du renommage des fichiers, +en utilisant un seul message par opération de renommage. La version 2 nécessitait deux postes. +Lors de la publication, dans une situation de mise en miroir, pour la consommation par les clients +v2, cet indicateur doit être réglé.

+
+
+

varTimeOffset (default: 0)

+

For example:

+
varTimeOffset -7m
+
+
+

modifiera des substitutions de variables qui impliquent des substitutions de date/heure. +Dans un modèle comme ${YYYY}/${MM}/${DD} sera évalué comme étant le +date, évaluée sept minutes dans le passé.

+
+
+

vip - OPTIONS ACTIVE/PASSIVE

+

L’option vip indique qu’une configuration doit être active uniquement sur +un seul nœud dans un cluster à la fois, un singleton. C’est typiquement +requis pour un composant de poll, mais cela peut être utilisé avec un sender ou avec d’autres cas.

+

subscribe peut être utilisé sur un seul nœud de serveur ou plusieurs nœuds +pourrait partager les responsabilités. Un autre logiciel de haute disponibilité et configurée séparément +présente un vip (ip virtuelle) sur le serveur actif. Si jamais +le serveur tombe en panne, le vip est déplacé sur un autre serveur et le traitement +se produit en utilisant le nouveau serveur qui a maintenant le VIP actif. +Les deux serveurs exécuteraient une instance sr3:

+
- **vip          <list>          []**
+
+
+

Lorsqu’une seule instance sr3 est exécutée sur un serveur, ces options ne sont pas définies, +et l’abonnement fonctionnera en « mode autonome » (standalone mode).

+

Dans le cas des courtiers en cluster, les options doivent être définit en fonction du +vip qui change.

+

vip 153.14.126.3

+

Lorsqu’une instance sr3 ne trouve pas l’adresse IP, elle se met en veille pendant 5 secondes et tente à nouveau. +Si c’est le cas, elle consomme et traite un message d’annonce et revérifie pour le vip. +lorsque plus qu’un vip est spécifié, n´importe lequel des addresses IP dans la liste est suffisant.

+
+
+

wololo

+

Une option de ligne de commande pour écraser une configuration SR3 existante lors de la conversion +à partir de la v2.

+
+
+
+

SEE ALSO

+

sr3(1) - Sarracenia ligne de commande principale.

+

sr3_post(1) - émettre des messages d’annonce de fichiers (implémentation en Python.)

+

sr3_cpost(1) - émettre des messages d´annonce de fichiers (implémentation en C.)

+

sr3_cpump(1) - copie les messages d’annonce ( implémentation en C du composant shovel. )

+

Formats:

+

sr3_post(7) - Le formats des annonces.

+

Page d’Accueil:

+

https://metpx.github.io/sarracenia - Sarracenia : une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr3_post.1.html b/fr/Reference/sr3_post.1.html new file mode 100644 index 000000000..52e59bdeb --- /dev/null +++ b/fr/Reference/sr3_post.1.html @@ -0,0 +1,594 @@ + + + + + + + Sr3_Post — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Sr3_Post

+
+

Publie la Disponibilitée d’un fichier aux abonnés.

+
+
Manual section:
+

1

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPx Sarracenia Suite

+
+
+
+

SYNOPSIS

+

sr3_post|sr3_cpost [ OPTIONS ][ -pb|–post_broker broker ][ -pbu|–post_baseUrl url[,url]…* ] +[ -p|–path ] path1 path2…pathN ]

+

( aussi libsrshim.so )

+
+
+

DESCRIPTION

+

sr3_post affiche la disponibilité d’un fichier en créant une annonce. +Contrairement à la plupart des autres composants de Sarracenia qui agissent comme des démons, +sr3_post est une invocation unique qui publie et quitte. +Pour faire des fichiers +disponible pour les abonnés, sr3_post envoie les annonces +à un serveur AMQP, également appelé courtier (broker).

+

Cette page de manuel principalement sur l’implémentation en python, +il existe également une implémentation en C qui fonctionne presque identiquement. +Les différences sont:

+
+
    +
  • les plugins ne sont pas supportés dans l’implémentation en C.

  • +
  • L’implémentation en C utilise des expressions régulières POSIX, la grammaire python3 est légèrement différente.

  • +
  • +
    lorsque l’option sleep (utilisée uniquement dans l’implémentation C) est définie comme > 0,

    il transforme sr_cpost en démon qui fonctionne comme le composant watch +de sr3(1).

    +
    +
    +
  • +
+
+
+

Options obligatoires

+

L’option post_base_url url,url,… spécifie l’emplacement +où les abonnés téléchargeront le fichier. Il y a généralement un article (post) par fichier. +le format de l’argument des options de post_base_url

+
[ftp|http|sftp]://[user[:password]@]host[:port]/
+ou
+file:
+
+
+

Lorsque plusieurs URL sont données sous forme de liste séparée par des virgules à post_base_url, +les URL fournies sont utilisées dans le style round-robin pour fournir une forme grossière d’équilibrage de charge.

+

L’option [-p|–path path1 path2 .. pathN] spécifie le chemin des fichiers +Être annoncé. Il y a généralement un article par fichier. +Format de l’argument pour les option path

+
/absolute_path_to_the/filename
+ou
+relative_path_to_the/filename
+
+
+

L’option -pipe peut être spécifiée pour que sr3_post lise les noms de chemin des fichiers également à partir +de l’entrée standard.

+

Un exemple d’invocation de sr3_post

+
sr3_post --post_broker amqp://broker.com --post_baseUrl sftp://stanley@mysftpserver.com/ --path /data/shared/products/foo
+
+
+

Par défaut, sr3_post lit le fichier /data/shared/products/foo et calcule sa somme de contrôle (checksum). +Il crée ensuite un message de publication, se connecte à broker.com en tant qu’utilisateur “invité” (informations d’identification par défaut) +et envoie l’article au vhost par défaut ‘/’ et à l’échange par défaut. L’échange par défaut +est le préfixe xs_ suivi du nom d’utilisateur du courtier, donc par défaut ‘xs_guest’. +Un abonné peut télécharger le fichier /data/shared/products/foo en s’authentifiant en tant qu’utilisateur stanley +sur mysftpserver.com en utilisant le protocole sftp vers broker.com en supposant qu’il possède des informations d’identification appropriées. +La sortie de la commande est

+
[INFO] Published xs_guest v02.post.data.shared.products.foo '20150813161959.854 sftp://stanley@mysftpserver.com/ /data/shared/products/foo' sum=d,82edc8eb735fd99598a1fe04541f558d parts=1,4574,1,0,0
+
+
+

Dans MetPX-Sarracenia, chaque article est publié sous un certain sujet. +La ligne de la sortie d’exécution (log) commence par ‘[INFO]’, suivi du sujet de +l’article. Les rubriques dans AMQP sont des champs séparés par un point. Le sujet complet commence par +un topicPrefix (voir option), version V02, une action post, +suivi d’un sous-thème (voir option) ici la valeur par défaut, le chemin du fichier séparé par des points +data.shared.products.foo.

+

Le deuxième champ de la ligne de la sortie d’exécution est l’avis de message. Il se compose d’un horodatage (timestamp) +20150813161959.854, et l’URL source du fichier dans les 2 derniers champs.

+

Le reste des informations est stocké dans des en-têtes de message AMQP, constitués de paires clé=valeur. +L’en-tête sum=d,82edc8eb735fd99598a1fe04541f558d donne l’informations surl’empreinte du fichier (ou somme de contrôle). +Ici, d signifie somme de contrôle md5 effectuée sur les données, et 82edc8eb735fd99598a1fe04541f558d +est la valeur de la somme de contrôle. Le parts=1,4574,1,0,0 indiquent que le fichier est disponible en 1 partie de 4574 octets +(la taille du fichier.) Le 1,0,0 restant n’est pas utilisé pour les transferts de fichiers avec une seule partie.

+

Un autre exemple:

+
sr3_post --post_broker mqtt://broker.com --post_baseDir /data/web/public_data --postBaseUrl http://dd.weather.gc.ca/ --path bulletins/alphanumeric/SACN32_CWAO_123456
+
+
+

Par défaut, sr3_post lit le fichier /data/web/public_data/bulletins/alphanumeric/SACN32_CWAO_123456 +(en concaténant le post_base_dir et le chemin relatif de l’url source pour obtenir le chemin du fichier local) +et calcule sa somme de contrôle. Il crée ensuite un message d’article, se connecte à broker.com en tant qu’utilisateur “invité” +(informations d’identification par défaut) et envoie l’article au vhost par défaut ‘/’ et échange ‘xs_guest’, résultant +à la publication au broker MQTT sous le topic : xs_guest/v03/bulletins/alphanumeric/SACN32_CWAO_123456

+

Un abonné peut télécharger le fichier http://dd.weather.gc.ca/bulletins/alphanumeric/SACN32_CWAO_123456 en utilisant http +sans authentification sur dd.meteo.gc.ca.

+
+
+
+

ARGUMENTS ET OPTIONS

+

Veuillez vous référer à la page de manuel sr3_options(7) pour une description détaillée de +tous les paramètres et les méthodes pour les spécifier.

+
+

path path1 path2 … pathN

+
+

sr3_post évalue les chemins du système de fichiers à partir de l’option path +et éventuellement le baseDir si l’option est utilisée.

+

Si un chemin définit un fichier, ce fichier est annoncé.

+

Si un chemin définit un répertoire, alors tous les fichiers de ce répertoire sont +annoncés…

+
+
+
+

post_broker <broker>

+
+

le courtier auquel l’article est envoyé.

+
+
+
+

post_baseDir <path>

+
+

L’option base_dir fournit le chemin du répertoire qui, +lorsqu’ils sont combinés (ou trouvés) dans le chemin donné, +donne le chemin local absolu du fichier de données à afficher. +La partie racine du document du chemin local sera supprimée de l’annonce publiée. +Pour les URL sftp : il peut être approprié de spécifier un chemin relatif à un compte utilisateur. +Un exemple de cette utilisation serait : -dr ~user -post_base_url sftp:user@host +Pour les URL de fichiers: base_dir n’est généralement pas approprié. Pour afficher un chemin absolu, +omettez le paramètre -dr et spécifiez simplement le chemin complet comme argument.

+
+
+
+

post_exchange <exchange>

+
+

Sr_post publie sur un échange nommé xs_”broker_username” par défaut. +Utilisez l’option post_exchange pour remplacer cette valeur par défaut.

+
+
+
+

-h|–help

+
+

Afficher les options du programme.

+
+
+
+

blocksize <value>

+

Inutile pour le moment, sera rétabli après la version v3

+

Cette option contrôle la stratégie de partitionnement utilisée pour publier les fichiers. +La valeur doit être l’une des suivantes:

+
0 - calcule automatiquement une stratégie de partitionnement appropriée (par défaut)
+1 - toujours envoyer des fichiers entiers en une seule partie.
+<blocksize> - utilise une taille de partition fixe (taille d'exemple : 1M )
+
+
+

Les fichiers peuvent être annoncés en plusieurs parties. Chaque partie a une somme de contrôle distincte. +Les pièces et leurs sommes de contrôle sont stockées dans le cache. Les partitions peuvent traverser +le réseau séparément et en parallèle. Lorsque les fichiers changent, les transferts sont +optimisé en n’envoyant que les pièces qui ont changé.

+

La valeur de blocksize est un nombre entier pouvant être suivi de la lettre [B|K|M|G|T] signifiant: +pour Octets, Kilooctets, Mégaoctets, Gigaoctets, Teraoctets respectivement. Toutes ces références sont des puissances de 2. +Les fichiers plus gros que cette valeur seront annoncés avec des parties de taille blocksize.

+

L’algorithme d’autocomputation détermine une taille de bloc qui encourage un nombre raisonnable de pièces +pour des fichiers de différentes tailles. Comme la taille du fichier varie, le calcul automatique donnera différents +résultats. Cela entraînera le renvoi d’informations qui n’ont pas changé en tant que partitions d’un autre +size aura des sommes différentes et sera donc étiqueté comme différent.

+

Par défaut, sr_post calcule une taille de bloc raisonnable qui dépend de la taille du fichier. +L’utilisateur peut définir une blocksize fixe si c’est mieux pour ses produits ou s’il veut +tirer avantage du mécanisme de cache. Dans les cas où des fichiers volumineux sont ajoutés, par exemple, +il est logique de spécifier une taille de partition fixe afin que les blocs du cache soient les +mêmes blocs que ceux générés lorsque le fichier est plus volumineux, et ainsi éviter la retransmission. Alors utiliser +‘10M’ aurait du sens dans ce cas.

+

Dans les cas où un téléchargeur personnalisé est utilisé qui ne comprend pas le partitionnement, il est nécessaire +d’éviter que le fichier ne soit divisé en plusieurs parties. Il faudrait donc spécifier ‘1’ pour forcer l’envoi de tous les fichiers +comme une seule pièce.

+
+
+

post_baseUrl <url>

+

L’option url définit le protocole, les informations d’identification, l’hôte et le port +où le produit peut être récupéré.

+

L’annonce AMQP est composée des trois champs, l’heure de l’annonce, +cette valeur url et le chemin donné vers lequel a été retiré du base_dir +si nécessaire.

+

La concaténation des deux derniers champs de l’annonce définit +ce que les abonnés utiliseront pour télécharger le produit.

+
+
+

reset

+

Quand on a utilisé –suppress_duplicates|–cache, cette option vide le cache.

+
+
+

rename <path>

+

Avec l’option renommer, l’utilisateur peut suggérer un chemin de destination vers ses fichiers. Si le chemin donné +se termine par ‘/’, il suggère un chemin de répertoire… Si ce n’est pas le cas, l’option spécifie un changement de nom de fichier.

+

sr3_post, et sr3 watch utilisent un modèle basé sur un fichier basé sur un processus et un cache disque, +dont la conception est à filetage unique. La bibliothèque shim est généralement utilisée par de nombreux processus +à la fois, et aurait des problèmes de conflit de ressources et/ou de corruption avec le cache. +La bibliothèque shim a donc un cache purement basé sur la mémoire, réglable avec +les options de shim_ suivantes.

+
+
+

shim_defer_posting_to_exit EXPERIMENTAL

+
+

Repousse la publication du fichier jusqu’à la fin du processus. +Dans les cas où un même fichier est ouvert et ajouté à plusieurs reprises, ce +paramètre peut éviter les publications redondantes. (par défaut: Faux)

+
+
+
+

shim_post_minterval interval EXPERIMENTAL

+
+

Si un fichier est ouvert en écriture et fermé plusieurs fois dans l’intervalle, +il ne sera affiché qu’une seule fois. Lorsqu’un fichier est écrit plusieurs fois, en particulier +dans un script shell, cela fait de nombreux messages et le script shell affecte les performances. +les abonnés ne pourront en aucun cas faire des copies assez rapidement, donc +il y a peu d’avantages, par exemple, à 100 messages du même fichier dans la même seconde. +Il est sage de fixer une limite supérieure à la fréquence de publication d’un fichier donné. (par défaut : 5s) +Remarque: si un fichier est toujours ouvert ou a été fermé après sa publication précédente, alors +lors du traitement de la sortie du processus, il sera réenregistré, même si l’intervalle +n’est pas respecté, afin de fournir le message final le plus précis.

+
+
+
+

shim_skip_parent_open_files EXPERIMENTAL

+
+

L’option shim_skip_ppid_open_files signifie qu’un processus vérifie +si le processus parent a le même fichier ouvert, et n’affiche +pas si c’est le cas. (par défaut: Vrai)

+
+
+
+

sleep time

+
+

Cette option n’est disponible que dans l’implémentation c (sr_cpost)

+

Lorsque l’option est définie, elle transforme cpost en sr3 watch, sleep étant le temps d’attente entre +la génération des événements. Lorsque les fichiers sont écrits fréquemment, il est contre-productif de produire un message pour +chaque changement, car cela peut produire un flux continu de changements où les transferts ne peuvent pas être effectués assez rapidement +pour suivre. Dans de telles circonstances, on peut regrouper toutes les modifications apportées à un fichier +en sleep, et produisez un seul message.

+
+
REMARQUE::

dans sr_cpost, lorsqu’il est combiné avec force_polling (voir sr3 watch(1) ) l’intervalle de +sommeil ne doit pas être inférieur à environ cinq secondes, car il peut manquer la publication de certains fichiers.

+
+
+
+
+
+

subtopic <key>

+
+

Le sous-thème par défaut peut être remplacé par l’option subtopic.

+
+
+
+

nodupe_ttl on|off|999

+
+

Évitez de publier des doublons en comparant chaque fichier à ceux vus lors de l’inverval +suppress_duplicates. Lors de la publication de répertoires, ceci entraînera +sr_post a publier uniquement les fichiers (ou parties de fichiers) qui étaient nouveaux lorsqu’ils sont invoqués à nouveau.

+

Au fil du temps, le nombre de fichiers dans le cache peut devenir trop grand, et il est donc nettoyé des +anciennes entrées. La durée de vie par défaut d’une entrée de cache est de cinq minutes (300 secondes). Cette +durée de vie peut être remplacé par un intervalle de temps comme argument (le 999 ci-dessus).

+

Si la suppression des doublons est utilisée, il faut s’assurer qu’un blocksize fixe est +utilisé (défini sur une valeur autre que 0) car sinon la taille de bloc variera à mesure que les fichiers grandissent, +et beaucoup de transfert de données en double en résultera.

+
+
+
+

identity <method>[,<value>]

+

Toutes les publications de fichiers incluent une somme de contrôle. L’option sum spécifie comment la calculer. +C’est une chaîne séparée par des virgules. Les méthodes d’intégrité valides sont

+
cod,x - Calculer au téléchargement en appliquant x
+sha512 - faire SHA512 sur le contenu du fichier (par défaut)
+md5 - faire md5sum sur le contenu du fichier
+random - inventez une valeur aléatoire pour chaque publication.
+arbitrary - appliquer la valeur fixe littérale.
+
+
+
+

Note

+

Les sommes de contrôle sont stockées dans les attributs de fichier étendus (ou Alternate Data Streams sous Windows). +Ceci est nécessaire pour que la méthode arbitrary fonctionne, puisque nous n’avons aucun moyen de la calculer.

+
+
+
+

topicPrefix <key>

+
+

Pas habituellement utilisé +Par défaut, le topic est composé du topicPrefix par défaut : version V03 +suivi du sous-sujet par défaut: le chemin du fichier séparé par des points (le point étant le séparateur de sujet pour amqp). +Vous pouvez écraser le topicPrefix en définissant cette option.

+

Not usually used +By default, the topic is made of the default topicPrefix : version V03 +followed by the default subtopic: the file path separated with dots (dot being the topic separator for amqp). +You can overwrite the topicPrefix by setting this option.

+
+
+
+

header <name>=<value>

+
+

Ajoutez une en-tête <name> avec la valeur donnée aux annonces. Utilisé pour transmettre des chaînes en tant que métadonnées.

+
+
+
+
+

UTILISATION DE LA LIBRAIRIE SHIM

+

Plutôt qu’invoquer un sr3_post pour poster chaque fichier à publier, on peut avoir des processus automatiquement +publiez les fichiers qu’ils écrivent en leur faisant utiliser une bibliothèque de shim interceptant certains appels d’i/o de fichiers vers la libc +et le noyau. Pour activer la bibliothèque shim, dans l’environnement shell, ajoutez

+
export SR_POST_CONFIG=shimpost.conf
+export LD_PRELOAD="libsrshim.so.1"
+
+
+

shimpost.conf est un fichier de configuration sr_cpost dans +le répertoire ~/.config/sarra/post/. Un fichier de configuration sr_cpost est le même +qu’un sr_post, sauf que les plugins ne sont pas pris en charge. Avec la +bibliothèque shim en place, chaque fois qu’un fichier est écrit, les clauses accept/reject du +fichier shimpost.conf sont consultés, et s’il est accepté, le fichier est affiché +comme ce serait par sr_post. Si vous utilisez ssh, où l’on veut des fichiers +scp à publier, il faut inclure l’activation dans le .bashrc et y passer +la configuration à utiliser

+
expoert LC_SRSHIM=shimpost.conf
+
+
+

Puis dans le ~/.bashrc sur le serveur exécutant la commande à distance

+
if [ "$LC_SRSHIM" ]; then
+    export SR_POST_CONFIG=$LC_SRSHIM
+    export LD_PRELOAD="libsrshim.so.1"
+fi
+
+
+

SSH ne transmettra que les variables d’environnement qui commencent par LC_ (locale) afin d’obtenir +les variables passées avec un minimum d’effort, nous utilisons ce préfixe.

+
+

Trucs d’utilisation de shim

+

Cette méthode de notification nécessite une certaine configuration de l’environnement utilisateur. +L’environnement utilisateur a besoin du jeu de variables d’environnement LD_PRELOAD +avant le lancement du processus. Des complications qui restent telles que nous les avons +testées depuis deux ans depuis la première mise en œuvre de la bibliothèque de shim:

+
    +
  • si nous voulons remarquer les fichiers créés par des processus scp distants (qui créent des shells sans connexion) +alors le crochet d’environnement doit être dans .bashrc. et en utilisant un environnement +variable qui commence par LC_ pour que ssh transmette la valeur de configuration sans +avoir à modifier la configuration sshd dans les distributions Linux typiques. +( discussion complète : https://github.com/MetPX/sarrac/issues/66 )

  • +
  • code qui a certaines faiblesses, comme dans FORTRAN un manque d’IMPLICIT NONE +https://github.com/MetPX/sarracenia/issues/69 peut planter lorsque la bibliothèque shim +est introduit. La correction nécessaire dans ces cas a jusqu’à présent consisté à corriger +l’application, et non la bibliothèque. +( aussi : https://github.com/MetPX/sarrac/issues/12 )

  • +
  • code utilisant l’appel exec vers tcl/tk, considère par défaut tout +sortie vers le descripteur de fichier 2 (erreur standard) comme condition d’erreur. +ces messages peuvent être étiquetés comme priorité INFO ou WARNING, mais ils vont +faire en sorte que l’appelant tcl indique qu’une erreur fatale s’est produite. Ajouter +-ignorestderr aux invocations de exec évite de tels abandons injustifiés.

  • +
  • Les scripts shell complexes peuvent avoir un impact démesuré sur les performances. +Puisque scripts shell hautes performances est un oxymore, la meilleure solution, +du point de vue des performances consiste à réécrire les scripts dans un langage de script plus efficace +tel que python ( https://github.com/MetPX/sarrac/issues/15 )

  • +
  • Les bases de code qui déplacent les hiérarchies de fichiers volumineux (par exemple, mv tree_with_thousands_of_files new_tree ) +verra un coût beaucoup plus élevé pour cette opération, car elle est mise en œuvre comme +un renommage de chaque fichier dans l’arborescence, plutôt qu’une seule opération sur la racine. +Ceci est actuellement considéré comme nécessaire car la correspondance du modèle accept/reject +peut entraîner une arborescence très différente sur la destination, plutôt que simplement +même arbre en miroir. Voir Traitement de renommage ci-dessous pour plus de détails.

  • +
  • export SR_SHIMDEBUG=1 obtiendra plus de sortie que vous ne le souhaitez. utiliser avec précaution.

  • +
+
+
+

Traitement de renommage

+

Il est à noter que renommer le fichier n’est pas aussi simple dans le cas de la mise en miroir que dans le système opérateur +sous-jacent. Alors que l’opération est une opération atomique unique dans un système d’exploitation, lorsque +en utilisant les notifications, il existe des cas d’acceptation/rejet qui créent quatre effets possibles.

+ + + + + + + + + + + + + + + + + + +

old name est:

new name est:

Accepté

Rejeté

Accepté

renommer

copier

Rejeté

retirer

rien

+

Lorsqu’un fichier est déplacé, deux notifications sont créées:

+
    +
  • Une notification a le nouveau nom dans le relpath, tout en contenant un champ oldname +pointant vers l’ancien nom. Cela déclenchera des activités dans la moitié supérieure de +la table, soit un renommage, en utilisant le champ oldname, soit une copie s’il n’est pas présent +à destination.

  • +
  • Une deuxième notification avec l’ancien nom dans relpath sera acceptée +encore une fois, mais cette fois, il contiendra le champ newname et traite l’action de suppression.

  • +
+

Renommer un répertoire à la racine d’un grand arbre est une opération atomique efficace +sous Linux/Unix, la mise en miroir de cette opération nécessite la création d’une publication de renommage pour chaque fichier +dans l’arbre, et est donc beaucoup plus cher.

+
+
+
+

VARIABLES ENVIRONNEMENTALES

+

Dans l’implémentation C (sr_cpost), si la variable SR_CONFIG_EXAMPLES est définie, alors la directive add peut être utilisée +pour copier des exemples dans le répertoire de l’utilisateur à des fins d’utilisation et/ou de personnalisation.

+

Une entrée dans le ~/.config/sarra/default.conf (créé via sr_subscribe edit default.conf ) +pourrait être utilisé pour définir la variable

+
declare env SR_CONFIG_EXAMPLES=/usr/lib/python3/dist-packages/sarra/examples
+
+
+

la valeur doit être disponible à partir de la sortie d’une commande de liste à partir de +l’implémentation python.

+
+
+

Voir aussi

+

sr3(1) - Interface de ligne de commande principale de Sarracenia.

+

sr3_cpost(1) - publication des annonces de fichiers (implémentation c.)

+

sr3_cpump(1) - implémentation en c du composant shovel. (copier les messages)

+

Formats:

+

sr3_credentials(7) - Convertir les lignes du fichier journal au format .save pour recharger/renvoyer.

+

sr3_options(7) - les options de configuration

+

sr3_post(7) - le format des annonces

+

Page d’acceuil:

+

https://metpx.github.io/sarracenia - Sarracenia: une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Reference/sr_post.7.html b/fr/Reference/sr_post.7.html new file mode 100644 index 000000000..8096ff2a8 --- /dev/null +++ b/fr/Reference/sr_post.7.html @@ -0,0 +1,802 @@ + + + + + + + SR_post — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

SR_post

+
+

Format/Protocole de messages d’annonce Sarracenia

+
+
Manual section:
+

7

+
+
Date:
+

May 21, 2024

+
+
Version:
+

UNKNOWN

+
+
Manual group:
+

MetPX-Sarracenia

+
+
+
+

STATUS: Stable/Default

+

Les messages de Sarracenia version 2 sont la format précédente, utilisée pour des transferts +de téraoctets et de millions de fichiers par jour. La version 3 propose une prochaine +itération du format des messages d’annonces Sarracenia.

+

La plupart des champs et leur signification sont les mêmes dans la version 3 que dans la version 2. +Certains champs changent parce que le protocole est exposé a une revision plus approfondi qu’auparavant.

+

Le changement de protocole de charge utile vise à simplifier les implémentations futures +et à activer l’utilisation par des protocoles de messagerie autres que l’AMQP antérieur à la version 1.0. +Voir v03 Modifications pour plus de détails.

+

Pour générer des messages en format v03, utilisez le paramètre suivant

+
post_topicPrefix v03
+
+
+

Pour sélectionner les messages à consommer dans ce format:

+
topicPrefix v03
+
+
+
+
+

SYNOPSIS

+

Version 03 du format des annonces de modification d’un fichier pour sr_post.

+

Un message sr3_post se compose d’un sujet et du BODY

+
+

AMQP Topic:

+
+
::

<version> = « v03 » la version du protocole ou du format. +« post » = le type de message dans le protocole. +<dir> = un sous-répertoire menant au fichier (peut être de nombreux répertoires profonds)

+
+
+
+
+

BODY: { <en-tête> } (JSON encoding.)

+

Les en-têtes sont un tableau de paires nom:valeur:

+
OBLIGATOIRE:
+
+        "pubTime"       - "YYYYMMDDTHHMMSS.ss" - UTC date/horodatage.
+        "baseUrl"       - racine de l’URL à télécharger.
+        "relPath"       - Le chemin relatif peut être concaténé à <base_url>
+
+ un de:
+        "identity"     - Version WMO du champ de sum v02, en cours de développement.
+        {
+           "method" : "md5" | "sha512" | "cod" | "arbitrary" | "random" ,
+           "value"  : "base64 valeur de somme de contrôle encodée"
+        }
+ ou:
+        "fileOp"   - pour décrire des opérations de fichiers sans transfert de données.
+        {
+           "link" : "la valeur d´un lien symbolique"
+           "remove" : ""  - un valeur pour indique que le fichier a été détruit
+           "hlink" : "fichier a cibler pour un lien dure (non-symbolique)¨
+           "rename" : "pour de renommage de fichier. valeur indique l´ancien nom."
+           "directory": "" - indique que l'opẃereation (soit création ou suppresion) est sur un répertoire.
+        }
+ou:
+        rien. Si aucun des champs ci-haut sont inclus, la suppression des duplicats
+        se fiera sur d´autres champes, tel que la date de modification de fichier,
+        s´il y lieu, afin d´éviter des boucles. Il est fortement suggeré au fournisseurs
+        de données de fournir un algorithme de d´intégrité pour éviter la fragilisation
+        des services de réplication de données.
+
+dans certains cas, les deux champs peuvent être présent, tel qu´un renommage de fichier,
+ou ca se peut que la fichier a été modifié et renommé, par exemple.
+
+
+FACULTATIF:
+
+        pour compatibilité GeoJSON:
+        "type": "Feature"
+        "geometry": RFC 7946 (geoJSON) spécification géographique compatible.
+
+        "size"          - le nombre d’octets annoncés.
+        "blocks"        - si le fichier publié est partitionné, alors :
+        {
+            "method"    : "inplace" | "partitioned" , - Le fichier est-il déjà en parties ?
+            "size"      : "9999", -  taille des blocs.
+            "count"     : "9999", - nombres de blocs dans un fichier.
+            "remainder" : "9999", - taille du dernier bloc.
+            "number"    : "9999", - quel est ce bloc.
+        }
+        "rename"        - nom pour écrire au fichier localement.
+        "retrievePath"       - le chemin de récupération relatif peut être concaténé avec <base_url> pour remplacer relPath -
+                          utilisé pour les cas d’API.
+        "topic"         - copie du sujet de l’en-tête AMQP (généralement omis)
+        "source"        - l’entité d’origine du message d´annonce.
+        "from_cluster"  - le cluster d’origine d’un message d´annonce.
+        "to_clusters"   - une spécification de destination.
+        "atime"         - heure du dernier accès à un fichier (facultatif)
+        "mtime"         - heure de la dernière modification d’un fichier (facultatif)
+        "mode"          - bits d’autorisation (facultatif)
+
+        "content"       - pour les fichiers plus petits, le contenu peut être incorporé.
+        {
+            "encoding" : "utf-8" | "base64" | "iso-8859-1" ,
+            "value"    " "contenu de fichier encodé"
+        }
+        Noter que iso-8859-1 est là pour des raisons de transmission inaltéré de formats obsolètes.
+
+        "contentType" : "chaine mime-type"  - indique le format des données.
+
+        Pour le messages de thème "v03.report", les en-têtes additionnelles qui suivent seront présents:
+
+        "report" { "code": 999  - Code de réponse de style HTTP.
+                   "timeCompleted": "YYYYMMDDTHHMMSS.ss" - UTC date/horodatage.
+                   "message" :  - message de rapport d’état documenté dans `Report Messages`_
+                 }
+
+        Autres champs optionnels:
+
+
+        des paires supplémentaires nom:valeur définies par l’utilisateur sont autorisées.
+
+
+
+

Note

+

L’en-tête parts n’a pas encore été revu par d’autres. Nous avons commencé la discussion sur size, +mais il n’y a pas eu de conclusion.

+
+
+
+
+

DESCRIPTION

+

Les sources créent des messages d´annonce en format sr_post pour annoncer les modifications apportées aux fichiers. +Les abonnés lisent le message pour décider si un téléchargement du contenu annoncé est justifié. Cette page +de manuel décrit entièrement le format de ces messages d´annonce. Les messages d´annonce sont des charges utiles +pour un bus de messages AMQP (Advanced Message Queuing Protocol), mais le transport de données de fichiers +est séparé, utilisant des protocoles plus courants tels que SFTP, HTTP, HTTPS ou FTP (ou autre?). +Les fichiers sont transportés sou forme de flux d’octets purs, aucune métadonnée au-delà du contenu du fichier +n’est transporté (bits de permission, attributs étendus, etc….)

+

Avec cette méthode, les messages AMQP fournissent un « plan de contrôle » pour les transferts de données. +Alors que chaque message d´annonce est essentiellement point à point, les pompes de données peuvent +être reliées transitivement entre elles pour créer des réseaux arbitraires. Chaque publication est consommée +par le saut suivant de la chaîne. Chaque saut re-publie (crée un nouveau poste pour) les données des sauts ultérieurs. +Les avis se déplacent dans le même sens que les données. Si les consommateurs le permettent, les messages de +rapport circulent également dans le chemin de contrôle, mais dans la direction opposée, permettant aux sources +de connaître l’ensemble de leur disposition.

+

La couche minimale sur AMQP brut offre une fonctionnalité de transfert de fichiers plus complète :

+
+
Filtrage des sources (utilisation des échanges `AMQP TOPIC`_)

Les messages d´annonce utilisent les topic exchanges de l’AMQP, où les thèmes sont des hiérarchies +destiné à représenter des thèmes d’intérêt pour un consommateur. Un consommateur peut télécharger le +critères de sélection pour le courtier de sorte que seulement un petit sous-ensemble d’avis +sont transmis au client. Lorsqu’il y a beaucoup d’utilisateurs intéressés par seulement un +petit sous-ensembles de données, les économies de trafic sont importantes.

+
+
Fingerprint Winnowing (utilisation de l’en-tête identity)

Chaque produit a une empreinte digitale d’intégrité et une taille destinée à l’identifier de manière unique, +appelée fingerprint. Si deux fichiers ont la même empreinte digitale, ils sont considérés comme équivalents. +Dans les cas où plusieurs sources de données équivalentes sont disponibles, mais les consommateurs en aval +préféreraient recevoir des annonces uniques des fichiers, les processus intermédiaires peuvent choisir de +publier des notifications du premier avec une empreinte digitale donnée, et ignorez les suivantes. +Propager uniquement la première occurrence d’une référence reçue en aval, sur la base de +son empreinte digitale, est appelée: Fingerprint Winnowing.

+

Fingerprint Winnowing est la base d’une stratégie robuste de haute disponibilité : mettre en place plusieurs +sources pour les mêmes données, les consommateurs acceptent les annonces de chacune des sources, mais seulement +en transférant le premier qui est reçu en aval. En fonctionnement normal, une source peut être plus rapide +que les autres, et donc les fichiers des autres sources sont généralement « winnowed ». Quand une source +disparaît, les données des autres sources sont automatiquement sélectionnées, parce ce que les empreintes +digitales sont maintenant fresh et utilisés, jusqu’à ce qu’une source plus rapide soit disponible.

+

L’avantage de cette méthode pour une haute disponibilité est qu’aucune décision A/B n’est requise. +Le temps d’un switchover est nul. D’autres stratégies sont sujet à des retards considérables +en prenant la décision de passer au numérique, et les pathologies que l’on pourrait considérer comme des oscillations, +et/ou des deadlocks.

+

Fingerprint Winnowing permet également le mesh-like, ou un réseau any to any, où l’on interconnecte simplement +un nœud avec d’autres et les messages d´annonce se propagent. Leur chemin spécifique à travers le +le réseau n’est pas défini, mais chaque participant téléchargera chaque nouvelle référence à partir du premier +nœud qui le met à sa disposition. Garder les messages d´annonce petits et séparés des données +est optimal pour cet usage.

+
+
Partitionnement (utilisation de l´entête parts )

Dans n’importe quel réseau de pompage de données de stockage et de transmission de données qui transporte des fichiers +entiers, limite la taille maximale d’un fichier au minimum disponible sur n’importe quel nœud intermédiaire. +Pour éviter de définir la taille maximale d’un fichier, une norme de segmentation est spécifiée, permettant aux +nœuds intermédiaires de tenir uniquement des segments du fichier, et de les transmettre au fur et à mesure qu’ils +soient reçus, plutôt que d’être forcé à conserver le fichier entier.

+

Le partitionnement permet également à plusieurs flux de transférer des parties du fichier en parallèle. +Plusieurs flux peuvent fournir une optimisation efficace sur les liens longs.

+
+
+
+
+

THÈME (TOPIC)

+

Dans les échanges basé par thèmes dans AMQP, chaque message d´annonce a un en-tête de thème. AMQP définit le caractère ‘.’ +en tant que séparateur hiérarchique (comme ‘' dans un nom de chemin Windows, ou ‘/’ dans Linux), il existe également une +paire de caractères génériques définis par la norme : ‘*’ correspond à un seul thème, ‘#’ correspond au reste de +la chaîne de caractère du thème. Pour permettre des modifications dans le corps du message d´annonce à l’avenir, les +arborescences de thèmes commencent par le numéro de la version du protocole.

+

AMQP permet le filtrage des thèmes côté serveur à l’aide de wildcards. Les abonnés spécifient les sujets d’intérêt +(qui correspondent à des répertoires sur le serveur), leur permettant de réduire le +nombre de notifications envoyées du serveur au client.

+

La racine de l’arborescence des thèmes est le spécificateur de version : « v03 ». Ensuite il y a le spécificateur +de type de message. Ces deux champs définissent le protocole utilisé pour le reste du message d´annonce. +Le type de message d´annonce pour les messages d´annonce est « post ». Après avoir fixé le préfixe du thème, +les sous-thèmes restants sont les éléments de chemin d’accès du fichier sur le serveur Web. +Par exemple, si un fichier est placé sur http://www.example.com/a/b/c/d/foo.txt, +alors le thème complet du message d´annonce sera : v03.a.b.c.d +Les champs AMQP sont limités à 255 caractères et les caractères du champ sont +encodé en utf8, de sorte que la limite de longueur réelle peut être inférieure à cela.

+
+

Note

+

Sarracenia s’appuie sur des courtiers pour interpréter l’en-tête du thème. Les courtiers interprètent +des en-têtes spécifiques au protocole AMQP, et ne décode pas efficacement la charge utile pour extraire les en-têtes. +Par conséquent, l’en-tête du thème est stocké dans un en-tête AMQP, plutôt que dans la charge utile qui autorise le +filtrage côté serveur. Pour éviter d’envoyer deux fois les mêmes informations, cet en-tête est +omis de la charge utile JSON.

+

De nombreuses implémentations côté client, une fois le message d´annonce chargé, définiront l’en-tête topic +dans la structure en mémoire, il serait donc très imprudent de définir l’en-tête topic +dans une application même si elle n’est pas visible dans la charge utile sur fil.

+
+
+

Mappage vers MQTT

+

L’un des objectifs du format v03 est d’avoir un format de charge utile qui fonctionne avec plus que l’AMQP. +Message Queing Telemetry Transport (MQTT v3.11) est une norme iso ( https://www.iso.org/standard/69466.html +un protocole qui peut facilement prendre en charge le même modèle de messagerie publication/abonnement, avec quelques détails +different, donc un mappage est nécessaire.

+

Tout d’abord, le séparateur de thème dans MQTT est une barre oblique (/), au lieu du point (.) qui est utilisé dans AMQP.

+

Deuxièmement, avec AMQP, on peut établir des hiérarchies de thèmes différents en utilisant des topic-based exchanges. +MQTT n’a pas de concept similaire, il n’y a qu’une seule hiérarchie, donc lors du mappage, il faut placer le nom +de l’échange à la racine de l’hiérarchie des thèmes pour obtenir le même effet

+
AMQP:   Exchange: <exchange name>
+           topic: v03.<directory>...
+
+MQTT:   topic: <exchange name>/v03/<directory>...
+
+
+
+
+
+

LES EN-TÊTES FIXES

+

Le message d´annonce est un tableau encodé en JSON unique, avec un ensemble obligatoire de champs, tout en permettant +l’utilisation d’autres champs arbitraires. Les champs obligatoires doivent être présents dans chaque message:

+
+
    +
  • “pubTime” : “<horodatage>” : la date de publication de l’affichage qui a été émis. Format: YYYYMMDDTHHMMSS. <decimalseconds>

  • +
+

Remarque : L’horodatage est toujours dans le fuseau horaire UTC.

+
    +
  • “baseUrl” : “<base_url>” – l’URL de base utilisée pour récupérer les données.

  • +
  • “relPath” : “<relativepath>” – la partie variable de l’URL, généralement ajoutée à baseUrl.

  • +
+
+

L’URL que les consommateurs utiliseront pour télécharger les données. Exemple d’URL complet

+
sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRPDS_HiRes_000.gif
+
+
+

Champs supplémentaires :

+
+

from_cluster=<nom_du_cluster>

+

L’en-tête from_cluster définit le nom du cluster source où +les données ont été introduites dans le réseau. Cela est utilisé pour renvoyer les journaux +au cluster chaque fois que ses produits sont utilisés.

+
+ +
+

size and blocks

+
+
"size":<sz> ,
+
+"blocks" :
+{
+       "method": "inplace" or "partitioned",
+       "size": <bsz>,
+       "count": <blktot>,
+       "remainder": <brem>,
+       "number": <bno>
+}
+
+
+
+

Un en-tête indiquant la méthode et les paramètres de partitionnement appliqués au fichier. +Le partitionnement est utilisé pour envoyer un seul fichier en tant que collection de segments, plutôt qu’en une +seule entité. Le partitionnement est utilisé pour accélérer les transferts de grands ensembles de données en utilisant +plusieurs flux et/ou pour réduire l’utilisation du stockage pour les fichiers extrêmement volumineux.

+

Lors du transfert de fichiers partitionnés, chaque partition est annoncée et potentiellement transportée +indépendamment sur un réseau de pompage de données.

+
+

<méthode>

+
+

Indique quelle méthode de partitionnement, si il y en a une, a été utilisée dans la transmission.

+ + + + + + + + + + + + + + + +

Méthode

Déscription

p - partitioned

Le fichier est partitionné, des fichiers en pièce individuels +sont créés.

i - inplace

Le fichier est partitionné, mais les blocs sont lus à partir d’un +seul fichier, plutôt que des parties.

1 - <sizeonly>

Le fichier est dans une seule partie (pas de partitionnement). +dans v03, seul l’en-tête size sera présent. blocs est omis.

+
+
    +
  • analogue aux options rsync : –inplace, –partial,

  • +
+

<blocksize in bytes>: bsz

+
+

Nombre d’octets dans un bloc. Lorsque vous utilisez la méthode 1, la taille du bloc est la taille du fichier. +Les restants des champs sont seulement utiles pour les fichiers partitionnés.

+

<blocks in total>: blktot +le nombre total (en entier) de blocs dans le fichier (le dernier bloc peut être partiel)

+

<remainder>: brem +normalement 0, pour le dernier bloc, octets restants dans le fichier +à transférer.

+
+
+
– if (fzb=1 and brem=0)

then bsz=fsz in bytes in bytes. +– fichiers entièrement remplacé. +– c’est la même chose que le mode –whole-fil de rsync.

+
+
+
+

<block#>: bno +0 origine, le numéro de bloc couvert par cette publication.

+
+
+

rename=<relpath>

+

Chemin d’accès relatif du répertoire actif dans lequel placer le fichier.

+
+
+

fileOp { ‘rename’:<oldpath> … }

+

lorsqu’un fichier est renommé à la source, pour l’envoyer aux abonnés, il va y avoir deux posts: un message +est annoncé avec le nouveau nom comme base_url, et l’en-tête FileOp va inclure la valeur de l’ancien nom du fichier, +dans un sous-champs appellé ‘rename’.

+

Les liens dures (hard links) sont, par contre, traités comme un post ordinaire du fichier avec un ensemble d’en-tête fileOp. +Les changements de noms de liens symboliques et répertoires sont representés pas la présences de sous champs “directory” +(répertoire) et “link” (lien) dans le champs “fileOp” qui contient également un rename.

+
+
+

identity

+

Le champ d’intégrité donne une somme de contrôle qui est utile pour identifier le contenu +d’un fichier:

+
"identity" : { "method" : <méthode>, "value": <valeur> }
+
+
+

Le champ d’intégrité est une signature calculée pour permettre aux récepteurs de déterminer +s’ils ont déjà téléchargé le produit ailleurs.

+

<method> - champ de chaîne de caractère (string field) indiquant la méthode de somme de contrôle utilisée.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Méthode

Déscription

random

Pas de sommes de contrôle (copie inconditionnelle). Ignore la +lecture du fichier (plus rapide)

arbitrary

valeur arbitraire définie par l’application qui ne peut pas être +calculée

md5

Somme de contrôle de l’ensemble des données +(MD-5 selon IETF RFC 1321)

link

Lié : SHA512 somme de la valeur du lien

md5name

Somme de contrôle du nom du fichier (MD-5 selon IETF RFC 1321)

remove

Supprimé : SHA512 du nom du fichier.

sha512

Somme de contrôle de l’ensemble des données +(SHA512 selon IETF RFC 6234)

cod

Somme de contrôle du téléchargement, avec algorithme comme argument +Exemple : cod,sha512 signifie télécharger, appliquer la somme de +contrôle SHA512 et annoncer avec cette somme de contrôle calculée +lors de la propagation ultérieure.

<name>

Somme de contrôle avec un autre algorithme, nommé <name> +<name> doit être registered dans le réseau de pompage de données +Enregistré signifie que tous les abonnés en aval peuvent obtenir +l’algorithme pour valider la somme de contrôle.

+
*<value>* La valeur est calculée en appliquant la méthode donnée à la partition transférée.
+pour un algorithme ou aucune valeur n’a de sens, un entier aléatoire est généré pour prendre en charge
+l'équilibrage de charge basé sur la somme de contrôle.
+
+
+
+
+
+

Report Messages

+

Certains clients peuvent renvoyer la télémétrie à l’origine des données téléchargées à des fins de dépannage +et à des fins de statistiques. Ces messages d´annonce ont le thème v03.report et ont un en-tête report +qui est un object JSON avec quatre champs :

+
+

{ “elapsedTime”: <report_time>, “resultCode”: <report_code>, “host”: <report_host>, “user”: <report_user>* }

+
    +
  • <report_code> les codes de résultat décrits dans la section suivante

  • +
  • <report_time> l’heure à laquelle le rapport a été généré.

  • +
  • <report_host> nom d’hôte à partir duquel la récupération a été lancée.

  • +
  • <report_user> nom d’utilisateur du courtier à partir duquel la récupération a été lancée.

  • +
+
+

Les messages de rapport ne doivent jamais inclure l’en-tête content (aucun fichier incorporé dans les rapports).

+
+

Report_Code

+

Le code de rapport est un code d’état à trois chiffres, adopté à partir du protocole HTTP (w3.org/IETF RFC 2616) +encodé sous forme de texte. Conformément à la RFC, tout code renvoyé doit être interprété comme suit :

+
+
    +
  • 2xx indique une réussite.

  • +
  • 3xx indique qu’une action supplémentaire est nécessaire pour terminer l’opération.

  • +
  • 4xx indique qu’une erreur permanente sur le client a empêché une opération réussie.

  • +
  • 5xx indique qu’un problème sur le serveur a empêché une opération réussie.

  • +
+
+
+

Note

+

FIXME: besoin de valider si notre utilisation des codes d’erreur coïncide avec l’intention générale +exprimé ci-dessus… Un 3xx signifie-t-il que nous nous attendons à ce que le client fasse quelque chose? 5xx signifie-t-il +que la défaillance était du côté du courtier/serveur ?

+
+

Les codes d’erreur spécifiques renvoyés et leurs significations dépendent de l’implémentation. +Pour l’implémentation sarracenia, les codes suivants sont définis :

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Code

Texte correspondant et signification pour la mise en œuvre de sarracenia

201

Téléchargement réussi. (variantes: Downloaded, Inserted, Published, Copied, or Linked)

203

Informations non-autoritaire : transformées pendant le téléchargement.

205

Réinitialiser le contenu : tronqué. Le fichier est plus court que prévu (longueur modifiée +pendant le transfert). Cela ne se produit que lors des transferts en plusieurs parties.

205

Réinitialiser le contenu : somme de contrôle recalculée à la réception.

304

Non modifié (Somme de contrôle validée, inchangée, donc aucun téléchargement en suit.)

307

Insertion différée (écriture dans une partie du fichier temporaire pour le moment.)

417

Échec de l’attente : message d´annonce non valide (en-têtes corrompus)

496

failure: During send, other protocol failure.

497

failure: During send, other protocol failure.

499

Échec : Non copié. Problème de téléchargement SFTP/FTP/HTTP

503

Service indisponible. supprimer (la suppression de fichiers n’est pas prise en charge.)

503

Impossible de traiter : Service indisponible

503

Protocole de transport spécifié dans la publication n’est pas pris en charge

xxx

Les codes d’état de validation des messages d´annonce et des fichiers dépendent du script

+

FIXME: will 3 error codes that are the same cause confusion?

+
+
+

Autres champs de rapport

+

<report_message> une chaine de caractères.

+
+
+
+

En-têtes facultatives

+

pour le cas d’utilisation de la mise en miroir de fichiers, des en-têtes supplémentaires seront présents :

+
+

atime,mtime,mode

+
+

man 2 stat - les métadonnées du fichier standard linux/unix : +temps d’accès, temps de modification et autorisation (bits de mode) +les heures sont dans le même format que le champ pubTime. +la chaîne d’autorisation est composée de quatre caractères destinés à être interprétés comme suit : +autorisations octal linux/unix traditionnelles.

+
+

Les en-têtes qui sont inconnus à un courtier DOIVENT être transmis sans modification.

+

Sarracenia fournit un mécanisme permettant aux utilisateurs d’inclure d’autres en-têtes arbitraires dans les messages d´annonce, +pour amplifier les métadonnées pour une prise de décision plus détaillée sur le téléchargement de données. +Par exemple:

+
"PRINTER" : "name_of_corporate_printer",
+
+"GeograpicBoundingBox" :
+ {
+         "top_left" : { "lat": 40.73, "lon": -74.1 } ,
+         "bottom_right": { "lat": -40.01, "lon": -71.12 }
+ }
+
+
+

permettrait au client d’appliquer un filtrage/traitement côté client plus élaboré et plus précis. +L’implémentation intermédiaire peut ne rien savoir de l’en-tête, +mais ils ne devraient pas être dépouillés, car certains consommateurs peuvent les comprendre et les traiter.

+
+
+
+

EXEMPLE

+
AMQP TOPIC: v03.NRDPS.GIF
+MQTT TOPIC: exchange/v03/NRDPS/GIF/
+Body: { "pubTime": "201506011357.345", "baseUrl": "sftp://afsiext@cmcdataserver", "relPath": "/data/NRPDS/outputs/NRDPS_HiRes_000.gif",
+   "rename": "NRDPS/GIF/", "parts":"p,457,1,0,0", "identity" : { "method":"md5", "value":"<md5sum-base64>" }, "source": "ec_cmc" }
+
+       - v03 - version du protocole
+       - la version et le type ensemble determine le format des thèmes qui suivent et du corps du message d'annonce.
+
+       - blocksize est 457  (== taile du fichier)
+       - block count est 1
+       - restant est 0.
+       - block number est 0.
+       - d - somme de contrôle a été calculé à partir du corps du fichier.
+       - la source complète de l'URL spécifiée (ne se termine pas par '/')
+       - chemin relatif spécifié pour
+
+       tirer de:
+               sftp://afsiext@cmcdataserver/data/NRPDS/outputs/NRDPS_HiRes_000.gif
+
+       chemin de téléchargement relatif complet :
+               NRDPS/GIF/NRDPS_HiRes_000.gif
+
+               -- prends le nom du fichier de base_url.
+               -- peut être modifié par un processus de validation.
+
+
+
+
+

Un Autre Exemple

+

Le post résultant de la commande de sr3_watch suivante, a noter la création du fichier ‘foo’:

+
sr3_watch -pbu sftp://stanley@mysftpserver.com/ -path /data/shared/products/foo -pb amqp://broker.com
+
+
+

Ici, sr_watch vérifie si le fichier /data/shared/products/foo est modifié. +Lorsque cela se produit, sr_watch lit le fichier /data/shared/products/foo et calcule sa somme de contrôle. +Il crée ensuite un message d’annonce, se connecte à broker.com en tant qu’utilisateur « invité » +(informations d’identification par défaut) et envoie la publication aux vhosts ‘/’ par défaut et +à l’échange ‘sx_guest’ (l’échange par défaut).

+

Un abonné peut télécharger le fichier /data/shared/products/foo en se connectant en tant qu’utilisateur stanley +sur mysftpserver.com en utilisant le protocole sftp pour broker.com en supposant qu’il dispose des +informations d’identification appropriées.

+

La sortie de la commande est la suivante

+
AMQP Topic: v03.20150813.data.shared.products
+MQTT Topic: <exchange>/v03/20150813/data/shared/products
+Body: { "pubTime":"20150813T161959.854", "baseUrl":"sftp://stanley@mysftpserver.com/",
+        "relPath": "/data/shared/products/foo", "parts":"1,256,1,0,0",
+        "sum": "d,25d231ec0ae3c569ba27ab7a74dd72ce", "source":"guest" }
+
+
+

Les posts sont publiés sur les échanges de thèmes AMQP, ce qui signifie que chaque message d’annonce a un en-tête de thème. +Le corps se compose d’un temps 20150813T161959.854, suivi des deux parties de +l’URL de récupération. Les en-têtes ont d’abord les parts, une taille en octets 256, +le nombre de blocs de cette taille 1, les octets restants 0, le +bloc actuel 0, un indicateur d signifiant que la somme de contrôle md5 est +effectuée sur les données, et la somme de contrôle 25d231ec0ae3c569ba27ab7a74dd72ce.

+
+

Possibilités d’optimisation

+

L’objectif d’optimisation est la lisibilité et la facilité de mise en œuvre, beaucoup plus +que l’efficacité ou la performance. Il existe de nombreuses optimisations pour réduire les +frais généraux de plusieur aspects, ce qui augmente la complexité de l’implémentation. +exemples: gzip la charge utile permettrait d’économiser peut-être 50% de taille, +regroupant également des en-têtes fixes (l’en-tête ‘body’ peut contenir +tous les champs fixes: « pubtime, baseurl, relpath, sum, parts », et un autre +champ ‘meta’ pourrait contenir: atime, mtime, mode donc il y aurait moins de +champs nommés et ca économiserais peut-être 40 octets de surcharge par avis. Mais +tous les changements augmentent la complexité, et ca rends les messages d’annonce plus difficile à analyser.

+
+
+
+

Standards

+
+
    +
  • Sarracenia s’appuie sur AMQP pre 1.0 +vu que la norme 1.0 a éliminé les concepts : courtier, échange, fil d’attente et +reliure. L’ensemble de fonctionnalités 1.0 est inférieur au minimum nécessaire pour prendre en charge +L’architecte publication-abonnement de Sarracenia

  • +
  • MQTT fait référence à MQTT v5.0 +et MQTT v3.1.1, +MQTT v5 a une extension importante: les abonnements partagés (fortement utilisés dans Sarracenia.) +donc v5 est fortement recommandé. La prise en charge de la version 3.1 est uniquement pour des raisons de support héritées.

  • +
  • JSON est défini par IETF RFC 7159. +La norme JSON inclut l’utilisation obligatoire de l’ensemble de caractères UNICODE (ISO 10646) +L’ensemble de caractères par défaut JSON est UTF-8, mais autorise plusieurs caractères +(UTF-8, UTF-16, UTF-32), mais interdit également la présence de marques d’ordre d’octets (byte order markings, BOM.)

  • +
  • comme Sarracenia v02, UTF-8 est obligatoire. Sarracenia restreint le format JSON +en exigeant un codage UTF-8 (IETF RFC 3629) qui n’a pas besoin/n’utilise pas de BOM. +Aucun autre codage n’est autorisé.

  • +
  • Le codage d’URL, conformément à la RFC 1738 de l’IETF, est utilisé pour échapper aux caractères dangereux +quand approprié.

  • +
+
+
+
+

VOIR AUSSI

+

sr3(1) - rracenia ligne de commande principale.

+

sr3_post(1) - poste des annoncements de fichiers (implémentation en Python.)

+

sr3_cpost(1) - poste des annoncements de fichiers (implémentation en C.)

+

sr3_cpump(1) - copie des messages d’annonce (implantation en C du composant “shovel”.)

+

Formats:

+

sr3_credentials(7) - Convertissez les lignes du fichier journal au format .save pour le rechargement/le renvoi.

+

sr3_options(7) - les options de configurations

+

Home Page:

+

https://metpx.github.io/sarracenia - Sarracenia : une boîte à outils de gestion du partage de données pub/sub en temps réel

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/1_CLI_introduction.html b/fr/Tutoriel/1_CLI_introduction.html new file mode 100644 index 000000000..f7cf96944 --- /dev/null +++ b/fr/Tutoriel/1_CLI_introduction.html @@ -0,0 +1,678 @@ + + + + + + + Téléchargement en utilisant la console — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Téléchargement en utilisant la console

+

Ce bloc-notes jupyter présente l’utilisation de Sarracenia version 3 à partir de la ligne de commande (principalement avec Linux, mais devrait être similaire avec Windows et Mac, la principale différence étant des conventions différentes pour l’emplacement de stockage des préférences et des sorties d’éxécution. C’est probablement la façon la plus simple de travailler avec Sarracenia. Vous configurez un flux pour télécharger des +fichiers dans un répertoire, et vous pouvez lire le répertoire pour y traiter les fichiers.

+
+
[ ]:
+
+
+
+import sarracenia
+!mkdir -p ~/.config/sr3/subscribe
+!mkdir -p ~/.cache/sr3/log
+
+
+
+
+

Prérequis

+

Ce code qui précède n’est qu’un moyen d’obtenir des bloc-notes jupyter pour installer metpx-sr3 sur un serveur. Créer des répertoires au cas où les gens utiliseraient l’accès à l’API sans exécuter les choses via l’API. Le prérequis de base est d’avoir installé metpx-sr3 d’une manière ou d’une autre, soit en tant que package .deb, soit en utilisant pip (ou pip3) disponible pour l’environnement utilisé par jupyter.

+

Le reste de ce bloc-notes suppose que metpx-sr3 est installé.

+
+
+

SR3

+

L’interface de ligne de commande s’appelle sr3 (abréviation de Sarracenia version 3). On définit les flux à exécuter à l’aide de fichiers de configuration dans un format simple : format keyword value (mot clé, valeur). Voici des exemples de configurations pour vous aider à démarrer:

+
+
[3]:
+
+
+
+!sr3 list examples
+
+
+
+
+
+
+
+
+Sample Configurations: (from: /home/peter/Sarracenia/sr3/sarracenia/examples )
+cpump/cno_trouble_f00.inc        poll/airnow.conf
+poll/aws-nexrad.conf             poll/mail.conf
+poll/nasa-mls-nrt.conf           poll/noaa.conf
+poll/soapshc.conf                poll/usgs.conf
+post/WMO_mesh_post.conf          sarra/wmo_mesh.conf
+sender/ec2collab.conf            sender/pitcher_push.conf
+shovel/no_trouble_f00.inc        subscribe/aws-nexrad.conf
+subscribe/dd_2mqtt.conf          subscribe/dd_all.conf
+subscribe/dd_amis.conf           subscribe/dd_aqhi.conf
+subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf
+subscribe/dd_cmml.conf           subscribe/dd_gdps.conf
+subscribe/dd_radar.conf          subscribe/dd_rdps.conf
+subscribe/dd_swob.conf           subscribe/ddc_cap-xml.conf
+subscribe/ddc_normal.conf        subscribe/downloademail.conf
+subscribe/ec_ninjo-a.conf        subscribe/hpfxWIS2DownloadAll.conf
+subscribe/hpfx_amis.conf         subscribe/local_sub.conf
+subscribe/ping.conf              subscribe/pitcher_pull.conf
+subscribe/sci2ec.conf            subscribe/subnoaa.conf
+subscribe/subsoapshc.conf        subscribe/subusgs.conf
+sender/ec2collab.conf            sender/pitcher_push.conf
+watch/master.conf                watch/pitcher_client.conf
+watch/pitcher_server.conf        watch/sci2ec.conf
+
+
+

Il existe différents types de flux : les exemples sont classés selon le type de flux (poll, post, sarra, sender, shovel…) Un subscribe (abonnement) est utilisé par les clients pour télécharger à partir d’une pompe de données. Choisissons-en un.

+
+
[4]:
+
+
+
+!sr3 add subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+add: 2022-03-19 13:17:47,786 2724188 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

Les fichiers qui sont actifs pour vous sont placés dans ~/.config/sr3//config_name. Vous pouvez y naviguer et les modifier avec un éditeur si vous le souhaitez. Vous pouvez également le faire avec sr3 edit subscribe/hpfx_amis.conf

+
# this is a feed of wmo bulletin (a set called AMIS in the old times)
+
+broker amqps://hpfx.collab.science.gc.ca/
+exchange xpublic
+
+# instances: number of downloading processes to run at once.  defaults to 1. Not enough for this case
+instances 5
+
+# expire, in operational use, should be longer than longest expected interruption
+expire 10m
+
+topic_prefix v02.post
+subtopic *.WXO-DD.bulletins.alphanumeric.#
+mirror false
+directory /tmp/amis/
+accept .*
+
+
+

ajouté messageCountMax, donc il ne s’exécute pas éternellement.

+
+
[6]:
+
+
+
+!mkdir /tmp/amis
+!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

Le répertoire racine où les fichiers doivent être placés doit exister avant de commencer. les commandes ci-dessus sont à configurer sur une machine Linux, vous pourriez avoir besoin d’autre chose sur un Mac ou Windows.

+

Vous pouvez ensuite exécuter un flux de manière interactive avec l’action foreground, et il se terminera rapidement, comme ceci:

+
+
[7]:
+
+
+
+!sr3 foreground subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2022-03-19 13:18:34,230 2724487 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10
+.2022-03-19 13:18:34,442 [INFO] 2724489 sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10
+2022-03-19 13:18:34,444 [INFO] 2724489 sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10
+2022-03-19 13:18:34,444 [INFO] 2724489 sarracenia.flow loadCallbacks plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.log.Log']
+2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flowcb.log __init__ subscribe initialized with: {'on_housekeeping', 'after_accept', 'after_work'}
+2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run options:
+_Config__admin="...st/ None True True False False None None",
+_Config__broker="...ca/ None True True False False None None",
+_Config__post_broker=None, accel_threshold=0, acceptSizeWrong=False,
+acceptUnmatched=False, action='foreground', attempts=3, auto_delete=False,
+baseDir=None, baseUrl_relPath=False, batch=10,
+bindings="... ['*.WXO-DD.bulletins.alphanumeric.#'])]", bufsize=1048576,
+bytes_per_second=None, bytes_ps=0,
+cfg_run_dir="...me/peter/.cache/sr3/subscribe/hpfx_amis'",
+component='subscribe', config='hpfx_amis.conf',
+configurations=['subscribe/hpfx_amis.conf'], currentDir=None,
+dangerWillRobinson=False, debug=False, declared_exchanges=[],
+declared_users="...': 'source', 'eggmeister': 'subscriber'}", delete=False,
+destfn_script=None, directory='/tmp/hpfx_amis/', discard=False,
+documentRoot=None, download=True, durable=True,
+env_declared="...OKER', 'MQP', 'SFTPUSER', 'TESTDOCROOT']", exchange='xpublic',
+exchangeDeclare=True, expire=600.0, feeder=amqp://tfeed@localhost,
+fileEvents={'delete', 'link', 'modify', 'create'}, filename='WHATFN',
+fixed_headers={}, flatten='/', hostdir='fractal', hostname='fractal',
+housekeeping=300, imports=[], inflight=None, inline=False,
+inline_encoding='guess', inline_max=4096, inline_only=False, instances=5,
+identity_arbitrary_value=None, identity_method='sha512',
+logEvents="...ekeeping', 'after_accept', 'after_work'}",
+logFormat="...me)s] %(name)s %(funcName)s %(message)s'", logLevel='info',
+logStdout=False, log_flowcb_needed=False, lr_backupCount=5, lr_interval=1,
+lr_when='midnight', masks="...pile('.*'), True, False, 0, False, '/')]",
+messageAgeMax=0, messageCountMax=10, messageDebugDump=False, messageRateMax=0,
+messageRateMin=0,
+message_strategy="...ubborn': True, 'failure_duration': '5m'}", message_ttl=0,
+mirror=False, no=0, fileAgeMax=0, nodupe_ttl=0, overwrite=True,
+permCopy=True, permDefault=0, permDirDefault=509, permLog=384,
+pid_filename="...e/hpfx_amis//subscribe_hpfx_amis_00.pid'", plugins_early=[],
+plugins_late=['sarracenia.flowcb.log.Log'], post_baseDir=None,
+post_baseUrl=None, post_documentRoot=None, post_exchanges=[],
+post_topicPrefix=['v02', 'post'], prefetch=25, pstrip=False, queueBind=True,
+queueDeclare=True, queueName="...s_subscribe.hpfx_amis.81537164.67226020'",
+queue_filename="...mis/subscribe.hpfx_amis.anonymous.qname'", randid='2067',
+randomize=False, realpathPost=False, rename=None, report_back=False,
+reset=False, retry_path="...hpfx_amis//subscribe_hpfx_amis_00.retry'",
+retry_ttl=600.0, settings={}, sleep=0.1, statehost=False, strip=0, subtopic=[],
+timeCopy=True, timeout=300, timezone='UTC', tls_rigour='normal',
+topicPrefix=['v02', 'post'], undeclared=[], users=False, v2plugin_options=[],
+v2plugins={}, vhost='/', vip=None
+2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.log.Log']
+2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run pid: 2724489 subscribe/hpfx_amis.conf instance: 0
+2022-03-19 13:18:35,043 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.94 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___39822
+2022-03-19 13:18:35,221 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___39822
+2022-03-19 13:18:36,276 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.36 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRWA20_KWAL_191718___44601
+2022-03-19 13:18:36,398 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRWA20_KWAL_191718___44601
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.65 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___38142
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 4.06 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRMT60_KWAL_191718___7676
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 4.05 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___30876
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.55 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SX/CWEG/17/SXCN03_CWEG_191700___24546
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.55 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRND30_KWAL_191718___32815
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 1.64 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SX/KWAL/17/SXCN40_KWAL_191718___41131
+2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 0.94 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___22785
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___38142
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMT60_KWAL_191718___7676
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___30876
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN03_CWEG_191700___24546
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRND30_KWAL_191718___32815
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_191718___41131
+2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___22785
+2022-03-19 13:18:41,611 [INFO] 2724489 sarracenia.flowcb.gather.message on_stop closing
+2022-03-19 13:18:41,611 [INFO] 2724489 sarracenia.flow close flow/close completed cleanly pid: 2724489 subscribe/hpfx_amis.conf instance: 0
+
+
+
+

comme vous pouvez le voir, il a téléchargé cinq fichiers dans /tmp/amis. L’action foreground est destinée à aider au débogage, plutôt qu’aux opérations réelles.

+
+
[8]:
+
+
+
+!sr3 status
+
+
+
+
+
+
+
+
+2022-03-19 13:18:52,445 2724517 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10
+status:
+Component/Config                         State        Run  Miss   Exp Retry
+----------------                         -----        ---  ----   --- -----
+subscribe/hpfx_amis                      stopped        0     0     0     0
+      total running configs:   0 ( processes: 0 missing: 0 stray: 0 )
+
+
+

Il y a 1 configuration dans votre liste. Vous pouvez en avoir des centaines. Les colonnes de droite indiquent le nombre d’instances dont vous disposez pour chaque configuration. Dans l’exemple ci-dessus, instances est défini sur 5, donc on s’attendrait à voir 5 instances en cours d’exécution lors de l’exécution. Vous pouvez démarrer une configuration spécifique avec _sr3 start subscribe/_ ou démarrer toutes les instances actives avec : *sr3 start

+
+
[ ]:
+
+
+
+!sr3 log subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2022-03-19 13:18:58,254 2724529 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10
+2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOVX45_KWAL_191509___38991
+2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191509___52945
+2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_191509___11643
+2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191509___30237
+2022-03-19 11:10:07,738 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 12.03 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SO/KWNB/15/SOVD83_KWNB_191200_RRX__37893
+2022-03-19 11:10:07,898 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOVD83_KWNB_191200_RRX__37893
+2022-03-19 11:10:17,165 [INFO] root stop_signal signal 15 received
+2022-03-19 11:10:18,562 [INFO] sarracenia.flow run clean stop from run loop
+2022-03-19 11:10:18,577 [INFO] sarracenia.flowcb.gather.message on_stop closing
+2022-03-19 11:10:18,577 [INFO] sarracenia.flow close flow/close completed cleanly pid: 2231352 subscribe/hpfx_amis instance: 1
+
+
+

Lors de l’exécution en arrière-plan, la sortie doit aller dans un fichier journal (sortie d’exécution). Comme nous n’avons exécuté ce fichier de configuration qu’au premier plan, demander à voir le journal imprime une erreur indiquant que le journal est manquant. Cela vous indique que les journaux se trouvent dans le répertoire ~/.cache/sr3/log. Les journaux peuvent être surveillés en temps réel avec des outils traditionnels tels que tail -f ou grep.

+

sr3 stop fait ce que vous pensez.

+

Les processus peuvent planter. Dans la sortie sr3 status ci-dessus, si le nombre de processus dans la colonne Run est inférieur à celui dans la colonne Exp (for Expected), cela signifie que certaines instances ont planté. Vous pouvez le réparer (démarrez simplement les instances manquantes) avec :

+

sr3 sanity - démarre les instances manquantes, tue également les parasites s’il en trouve.

+

Voilà, une introduction à l’exécution des configurations dans Sarracenia à partir de la ligne de commande.

+
+
+

Conclusion

+

Si tout ce que vous voulez faire est d’obtenir des données à partir d’une pompe de données en temps réel, utiliser l’interface de ligne de commande pour contrôler certains processus qui s’exécutent tout le temps, afin qu’ils vident les fichiers dans un certain répertoire est la méthode la plus simple.

+

Ce n’est pas très efficace cependant. Lorsque vous avez un grand nombre de fichiers sur lesquels travailler et que vous souhaitez un traitement à grande vitesse, il est préférable, dans le sens d’une charge CPU et d’E/S (I/O) réduite et en termes de vitesse de traitement d’avoir votre propre application informée de l’arrivée des fichiers, plutôt que de scanner un répertoire.

+

La façon la plus simple de le faire est d’ajouter des rappels à vos flux. Nous couvrirons cela ensuite.

+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/1_CLI_introduction.ipynb b/fr/Tutoriel/1_CLI_introduction.ipynb new file mode 100644 index 000000000..dfe11e6df --- /dev/null +++ b/fr/Tutoriel/1_CLI_introduction.ipynb @@ -0,0 +1,389 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "chubby-tenant", + "metadata": {}, + "source": [ + "# Téléchargement en utilisant la console\n", + "\n", + "Ce [bloc-notes jupyter](https://jupyter.org) présente l'utilisation de [Sarracenia version 3](https://metpx.github.io/sarracenia) à partir de la ligne de commande (principalement avec Linux, mais devrait être similaire avec Windows et Mac, la principale différence étant des conventions différentes pour l'emplacement de stockage des préférences et des sorties d'éxécution. C'est probablement la façon la plus simple de travailler avec Sarracenia. Vous configurez un flux pour télécharger des fichiers dans un répertoire, et vous pouvez lire le répertoire pour y traiter les fichiers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "neither-shannon", + "metadata": {}, + "outputs": [], + "source": [ + "import sarracenia\n", + "!mkdir -p ~/.config/sr3/subscribe\n", + "!mkdir -p ~/.cache/sr3/log" + ] + }, + { + "cell_type": "markdown", + "id": "varying-armor", + "metadata": {}, + "source": [ + "\n", + "## Prérequis\n", + "\n", + "Ce code qui précède n'est qu'un moyen d'obtenir des bloc-notes jupyter pour installer metpx-sr3 sur un serveur.\n", + "Créer des répertoires au cas où les gens utiliseraient l'accès à l'API sans exécuter les choses via l'API. Le prérequis de base est d'avoir installé metpx-sr3 d'une manière ou d'une autre, soit en tant que package .deb, soit en utilisant pip (ou pip3) disponible pour l'environnement utilisé par jupyter.\n", + "\n", + "Le reste de ce bloc-notes suppose que [metpx-sr3](https://metpx.github.io/sarracenia) est installé." + ] + }, + { + "cell_type": "markdown", + "id": "absolute-integral", + "metadata": {}, + "source": [ + "## SR3\n", + "\n", + "L'interface de ligne de commande s'appelle [sr3](../Reference/sr3.1.html) (abréviation de Sarracenia version 3). On définit les\n", + "flux à exécuter à l'aide de fichiers de configuration dans un format simple : format _keyword_ _value_ (mot clé, valeur).\n", + "Voici des exemples de configurations pour vous aider à démarrer:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "drawn-opposition", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sample Configurations: (from: /home/peter/Sarracenia/sr3/sarracenia/examples )\r\n", + "cpump/cno_trouble_f00.inc poll/airnow.conf \r\n", + "poll/aws-nexrad.conf poll/mail.conf \r\n", + "poll/nasa-mls-nrt.conf poll/noaa.conf \r\n", + "poll/soapshc.conf poll/usgs.conf \r\n", + "post/WMO_mesh_post.conf sarra/wmo_mesh.conf \r\n", + "sender/ec2collab.conf sender/pitcher_push.conf \r\n", + "shovel/no_trouble_f00.inc subscribe/aws-nexrad.conf \r\n", + "subscribe/dd_2mqtt.conf subscribe/dd_all.conf \r\n", + "subscribe/dd_amis.conf subscribe/dd_aqhi.conf \r\n", + "subscribe/dd_cacn_bulletins.conf subscribe/dd_citypage.conf \r\n", + "subscribe/dd_cmml.conf subscribe/dd_gdps.conf \r\n", + "subscribe/dd_radar.conf subscribe/dd_rdps.conf \r\n", + "subscribe/dd_swob.conf subscribe/ddc_cap-xml.conf \r\n", + "subscribe/ddc_normal.conf subscribe/downloademail.conf \r\n", + "subscribe/ec_ninjo-a.conf subscribe/hpfxWIS2DownloadAll.conf \r\n", + "subscribe/hpfx_amis.conf subscribe/local_sub.conf \r\n", + "subscribe/ping.conf subscribe/pitcher_pull.conf \r\n", + "subscribe/sci2ec.conf subscribe/subnoaa.conf \r\n", + "subscribe/subsoapshc.conf subscribe/subusgs.conf \r\n", + "sender/ec2collab.conf sender/pitcher_push.conf \r\n", + "watch/master.conf watch/pitcher_client.conf \r\n", + "watch/pitcher_server.conf watch/sci2ec.conf \r\n" + ] + } + ], + "source": [ + "!sr3 list examples" + ] + }, + { + "cell_type": "markdown", + "id": "affecting-marking", + "metadata": {}, + "source": [ + "Il existe différents types de flux : les exemples sont classés selon le type de flux (poll, post, sarra, sender, shovel...)\n", + "Un _subscribe_ (abonnement) est utilisé par les clients pour télécharger à partir d'une pompe de données. Choisissons-en un." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "egyptian-suicide", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "add: 2022-03-19 13:17:47,786 2724188 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf \r\n", + "\r\n" + ] + } + ], + "source": [ + "!sr3 add subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "overall-instruction", + "metadata": {}, + "source": [ + "Les fichiers qui sont actifs pour vous sont placés dans ~/.config/sr3//config_name. Vous pouvez y naviguer\n", + "et les modifier avec un éditeur si vous le souhaitez. Vous pouvez également le faire avec _sr3 edit subscribe/hpfx_amis.conf_\n", + "\n", + " # this is a feed of wmo bulletin (a set called AMIS in the old times)\n", + "\n", + " broker amqps://hpfx.collab.science.gc.ca/\n", + " exchange xpublic\n", + "\n", + " # instances: number of downloading processes to run at once. defaults to 1. Not enough for this case\n", + " instances 5\n", + " \n", + " # expire, in operational use, should be longer than longest expected interruption\n", + " expire 10m\n", + "\n", + " topic_prefix v02.post\n", + " subtopic *.WXO-DD.bulletins.alphanumeric.#\n", + " mirror false\n", + " directory /tmp/amis/\n", + " accept .*\n", + "\n", + "ajouté messageCountMax, donc il ne s'exécute pas éternellement." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "primary-score", + "metadata": {}, + "outputs": [], + "source": [ + "!mkdir /tmp/amis\n", + "!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "ancient-scholarship", + "metadata": {}, + "source": [ + "Le répertoire racine où les fichiers doivent être placés doit exister avant de commencer.\n", + "les commandes ci-dessus sont à configurer sur une machine Linux, vous pourriez avoir besoin d'autre chose sur un Mac ou Windows.\n", + "\n", + "Vous pouvez ensuite exécuter un flux de manière interactive avec l'action _foreground_, et il se terminera rapidement, comme ceci:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "nominated-nerve", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-03-19 13:18:34,230 2724487 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10\n", + ".2022-03-19 13:18:34,442 [INFO] 2724489 sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10\n", + "2022-03-19 13:18:34,444 [INFO] 2724489 sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10\n", + "2022-03-19 13:18:34,444 [INFO] 2724489 sarracenia.flow loadCallbacks plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.log.Log']\n", + "2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flowcb.log __init__ subscribe initialized with: {'on_housekeeping', 'after_accept', 'after_work'}\n", + "2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run options:\n", + "_Config__admin=\"...st/ None True True False False None None\",\n", + "_Config__broker=\"...ca/ None True True False False None None\",\n", + "_Config__post_broker=None, accel_threshold=0, acceptSizeWrong=False,\n", + "acceptUnmatched=False, action='foreground', attempts=3, auto_delete=False,\n", + "baseDir=None, baseUrl_relPath=False, batch=10,\n", + "bindings=\"... ['*.WXO-DD.bulletins.alphanumeric.#'])]\", bufsize=1048576,\n", + "bytes_per_second=None, bytes_ps=0,\n", + "cfg_run_dir=\"...me/peter/.cache/sr3/subscribe/hpfx_amis'\",\n", + "component='subscribe', config='hpfx_amis.conf',\n", + "configurations=['subscribe/hpfx_amis.conf'], currentDir=None,\n", + "dangerWillRobinson=False, debug=False, declared_exchanges=[],\n", + "declared_users=\"...': 'source', 'eggmeister': 'subscriber'}\", delete=False,\n", + "destfn_script=None, directory='/tmp/hpfx_amis/', discard=False,\n", + "documentRoot=None, download=True, durable=True,\n", + "env_declared=\"...OKER', 'MQP', 'SFTPUSER', 'TESTDOCROOT']\", exchange='xpublic',\n", + "exchangeDeclare=True, expire=600.0, feeder=amqp://tfeed@localhost,\n", + "fileEvents={'delete', 'link', 'modify', 'create'}, filename='WHATFN',\n", + "fixed_headers={}, flatten='/', hostdir='fractal', hostname='fractal',\n", + "housekeeping=300, imports=[], inflight=None, inline=False,\n", + "inline_encoding='guess', inline_max=4096, inline_only=False, instances=5,\n", + "identity_arbitrary_value=None, identity_method='sha512',\n", + "logEvents=\"...ekeeping', 'after_accept', 'after_work'}\",\n", + "logFormat=\"...me)s] %(name)s %(funcName)s %(message)s'\", logLevel='info',\n", + "logStdout=False, log_flowcb_needed=False, lr_backupCount=5, lr_interval=1,\n", + "lr_when='midnight', masks=\"...pile('.*'), True, False, 0, False, '/')]\",\n", + "messageAgeMax=0, messageCountMax=10, messageDebugDump=False, messageRateMax=0,\n", + "messageRateMin=0,\n", + "message_strategy=\"...ubborn': True, 'failure_duration': '5m'}\", message_ttl=0,\n", + "mirror=False, no=0, fileAgeMax=0, nodupe_ttl=0, overwrite=True,\n", + "permCopy=True, permDefault=0, permDirDefault=509, permLog=384,\n", + "pid_filename=\"...e/hpfx_amis//subscribe_hpfx_amis_00.pid'\", plugins_early=[],\n", + "plugins_late=['sarracenia.flowcb.log.Log'], post_baseDir=None,\n", + "post_baseUrl=None, post_documentRoot=None, post_exchanges=[],\n", + "post_topicPrefix=['v02', 'post'], prefetch=25, pstrip=False, queueBind=True,\n", + "queueDeclare=True, queueName=\"...s_subscribe.hpfx_amis.81537164.67226020'\",\n", + "queue_filename=\"...mis/subscribe.hpfx_amis.anonymous.qname'\", randid='2067',\n", + "randomize=False, realpathPost=False, rename=None, report_back=False,\n", + "reset=False, retry_path=\"...hpfx_amis//subscribe_hpfx_amis_00.retry'\",\n", + "retry_ttl=600.0, settings={}, sleep=0.1, statehost=False, strip=0, subtopic=[],\n", + "timeCopy=True, timeout=300, timezone='UTC', tls_rigour='normal',\n", + "topicPrefix=['v02', 'post'], undeclared=[], users=False, v2plugin_options=[],\n", + "v2plugins={}, vhost='/', vip=None\n", + "2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.log.Log']\n", + "2022-03-19 13:18:34,803 [INFO] 2724489 sarracenia.flow run pid: 2724489 subscribe/hpfx_amis.conf instance: 0\n", + "2022-03-19 13:18:35,043 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.94 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___39822 \n", + "2022-03-19 13:18:35,221 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___39822 \n", + "2022-03-19 13:18:36,276 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.36 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRWA20_KWAL_191718___44601 \n", + "2022-03-19 13:18:36,398 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRWA20_KWAL_191718___44601 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.65 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___38142 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 4.06 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRMT60_KWAL_191718___7676 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 4.05 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___30876 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.55 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SX/CWEG/17/SXCN03_CWEG_191700___24546 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 2.55 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRND30_KWAL_191718___32815 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 1.64 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SX/KWAL/17/SXCN40_KWAL_191718___41131 \n", + "2022-03-19 13:18:40,766 [INFO] 2724489 sarracenia.flowcb.log after_accept accepted: (lag: 0.94 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SR/KWAL/17/SRCN40_KWAL_191718___22785 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___38142 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRMT60_KWAL_191718___7676 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___30876 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN03_CWEG_191700___24546 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRND30_KWAL_191718___32815 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_191718___41131 \n", + "2022-03-19 13:18:41,592 [INFO] 2724489 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191718___22785 \n", + "2022-03-19 13:18:41,611 [INFO] 2724489 sarracenia.flowcb.gather.message on_stop closing\n", + "2022-03-19 13:18:41,611 [INFO] 2724489 sarracenia.flow close flow/close completed cleanly pid: 2724489 subscribe/hpfx_amis.conf instance: 0\n", + "\n" + ] + } + ], + "source": [ + "!sr3 foreground subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "foreign-european", + "metadata": {}, + "source": [ + "comme vous pouvez le voir, il a téléchargé cinq fichiers dans /tmp/amis.\n", + "L'action _foreground_ est destinée à aider au débogage, plutôt qu'aux opérations réelles." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "split-writing", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-03-19 13:18:52,445 2724517 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10\r\n", + "status: \r\n", + "Component/Config State Run Miss Exp Retry\r\n", + "---------------- ----- --- ---- --- -----\r\n", + "subscribe/hpfx_amis stopped 0 0 0 0\r\n", + " total running configs: 0 ( processes: 0 missing: 0 stray: 0 )\r\n" + ] + } + ], + "source": [ + "!sr3 status" + ] + }, + { + "cell_type": "markdown", + "id": "rocky-unemployment", + "metadata": {}, + "source": [ + "Il y a 1 configuration dans votre liste. Vous pouvez en avoir des centaines. Les colonnes de droite indiquent le nombre d'instances dont vous disposez pour chaque configuration. Dans l'exemple ci-dessus, _instances_ est défini sur 5, donc on s'attendrait à voir 5 instances en cours d'exécution lors de l'exécution. Vous pouvez démarrer une configuration spécifique avec _sr3 start subscribe/*_ ou démarrer toutes les instances actives avec : _sr3 start_" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "neural-laugh", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-03-19 13:18:58,254 2724529 [INFO] sarracenia.config fill_missing_options overriding batch for consistency with messageCountMax: 10\n", + "2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOVX45_KWAL_191509___38991 \n", + "2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191509___52945 \n", + "2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SXCN40_KWAL_191509___11643 \n", + "2022-03-19 11:10:07,282 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SRCN40_KWAL_191509___30237 \n", + "2022-03-19 11:10:07,738 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 12.03 ) https://hpfx.collab.science.gc.ca /20220319/WXO-DD/bulletins/alphanumeric/20220319/SO/KWNB/15/SOVD83_KWNB_191200_RRX__37893 \n", + "2022-03-19 11:10:07,898 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/SOVD83_KWNB_191200_RRX__37893 \n", + "2022-03-19 11:10:17,165 [INFO] root stop_signal signal 15 received\n", + "2022-03-19 11:10:18,562 [INFO] sarracenia.flow run clean stop from run loop\n", + "2022-03-19 11:10:18,577 [INFO] sarracenia.flowcb.gather.message on_stop closing\n", + "2022-03-19 11:10:18,577 [INFO] sarracenia.flow close flow/close completed cleanly pid: 2231352 subscribe/hpfx_amis instance: 1\n" + ] + } + ], + "source": [ + "!sr3 log subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "leading-matthew", + "metadata": {}, + "source": [ + "Lors de l'exécution en arrière-plan, la sortie doit aller dans un fichier journal (sortie d'exécution). Comme nous n'avons exécuté ce fichier de configuration qu'au premier plan, demander à voir le journal imprime une erreur indiquant que le journal est manquant. Cela vous indique que les journaux se trouvent dans le répertoire _~/.cache/sr3/log_. Les journaux peuvent être surveillés en temps réel avec des outils traditionnels tels que _tail -f_ ou _grep_.\n", + "\n", + "_sr3 stop_ fait ce que vous pensez.\n", + "\n", + "Les processus peuvent planter. Dans la sortie _sr3 status_ ci-dessus, si le nombre de processus dans la colonne Run est inférieur à celui dans la colonne Exp (for Expected), cela signifie que certaines instances ont planté. Vous pouvez le réparer (démarrez simplement les instances manquantes) avec :\n", + "\n", + "_sr3 sanity_ - démarre les instances manquantes, tue également les parasites s'il en trouve.\n", + "\n", + "Voilà, une introduction à l'exécution des configurations dans Sarracenia à partir de la ligne de commande.\n", + "\n", + "\n", + "## Conclusion\n", + "\n", + "Si tout ce que vous voulez faire est d'obtenir des données à partir d'une pompe de données en temps réel, utiliser l'interface de ligne de commande pour contrôler certains processus qui s'exécutent tout le temps, afin qu'ils vident les fichiers dans un certain répertoire est la méthode la plus simple.\n", + "\n", + "Ce n'est pas très efficace cependant. Lorsque vous avez un grand nombre de fichiers sur lesquels travailler et que vous souhaitez un traitement à grande vitesse, il est préférable, dans le sens d'une charge CPU et d'E/S (I/O) réduite et en termes de vitesse de traitement\n", + "d'avoir votre propre application informée de l'arrivée des fichiers, plutôt que de scanner un répertoire.\n", + "\n", + "La façon la plus simple de le faire est d'ajouter des rappels à vos flux. Nous couvrirons cela ensuite." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "artistic-purple", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fr/Tutoriel/2_CLI_with_flowcb_demo.html b/fr/Tutoriel/2_CLI_with_flowcb_demo.html new file mode 100644 index 000000000..702f4b6b0 --- /dev/null +++ b/fr/Tutoriel/2_CLI_with_flowcb_demo.html @@ -0,0 +1,889 @@ + + + + + + + Personnalisez la gestion des fichiers avec les rappels. — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Personnalisez la gestion des fichiers avec les rappels.

+

Tous les composants de Sarracenia implémentent l’algorithme Flow, avec différents rappels, dans le langage de programmation Python. La classe principale de Sarracenia (Python) est sarracenia.flow et la grande partie de la fonctionnalité de base est mis en œuvre à l’aide de la classe créée pour ajouter un traitement personnalisé à un flux, le classe flowcb (rappel de flux).

+

Pour une discussion détaillée de l’algorithme de flux lui-même, jetez un oeil dans le manuel Concepts. Pour tout flux, on peut ajouter un traitement personnalisé à divers moments pendant le traitement par sous-classement la classe sarracenia.flowcb.

+

En bref, l’algorithme comporte les étapes suivantes :

+
    +
  • init(self, options) – lors de l’import, initialisation de python traditionnelle

  • +
  • on_start – lorsqu’une instance est démarrée.

  • +
  • boucle pour toujours

    +
      +
    • gather – collecte les messages à traiter appelés : worklist.incoming

    • +
    • poll – une autre façon de collecter des messages, uniquement dans le composant poll.

    • +
    • filter – applique les correspondances d’expressions régulières acceptées/rejetées à la liste de messages. déplace les messages pour les fichiers à ne pas télécharger de worklist.incoming vers worklist.reject

      +
        +
      • after_accept point d’entrée de rappel. traiter worklist.incoming, en en rejetant potentiellement d’autres.

      • +
      +
    • +
    • ack – les messages worklist.rejected sont reconnus à la source en amont lorsque le traitement est terminé.

    • +
    • work – effectue un transfert ou une transformation sur un fichier.

    • +
    • ack – les messages worklist.ok pour les fichiers transférés avec succès sont reconnus à la source en amont.

      +
        +
      • Point d’entrée du rappel after_work

      • +
      +
    • +
    • ack – les messages worklist.failed pour les fichiers qui n’ont pas été transférés avec succès sont reconnus.

    • +
    • post – publier le résultat du travail effectué pour l’étape suivante.

    • +
    • occasionnellement… **on_housekeeping – faire des nettoyages périodiques…

    • +
    +
  • +
  • on_stop – arrêt du traitement.

  • +
+

pour plus de détails sur les points d’entrée disponibles de flowcb, consultez le code source:

+ +

Regardons l’utilisation de la classe dans une configuration :

+
+
[1]:
+
+
+
+!sr3 remove subscribe/hpfx_amis.conf
+!sr3 add subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2023-05-28 16:35:16,492 1919860 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10
+2023-05-28 16:35:16,493 1919860 [INFO] root remove removing subscribe/hpfx_amis
+
+add: 2023-05-28 16:35:17,637 1919863 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+
+
[2]:
+
+
+
+!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+

fait en sorte que le flux s’arrête après la consommation de 10 messages.

+
+
[3]:
+
+
+
+!sr3 list fcb
+
+
+
+
+
+
+
+
+2023-05-28 16:35:26,803 1919870 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10
+Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia )
+flowcb/accept/dateappend.py      flowcb/accept/delete.py
+flowcb/accept/downloadbaseurl.py flowcb/accept/hourtree.py
+flowcb/accept/httptohttps.py     flowcb/accept/longflow.py
+flowcb/accept/pathreplace.py     flowcb/accept/posthourtree.py
+flowcb/accept/postoverride.py    flowcb/accept/printlag.py
+flowcb/accept/rename4jicc.py     flowcb/accept/renamedmf.py
+flowcb/accept/renamewhatfn.py    flowcb/accept/save.py
+flowcb/accept/speedo.py          flowcb/accept/sundewpxroute.py
+flowcb/accept/testretry.py       flowcb/accept/toclusters.py
+flowcb/accept/tohttp.py          flowcb/accept/tolocal.py
+flowcb/accept/tolocalfile.py     flowcb/accept/wmotypesuffix.py
+flowcb/amserver.py               flowcb/clamav.py
+flowcb/destfn/replace.py         flowcb/destfn/sample.py
+flowcb/download/mail_ingest.py   flowcb/filter/deleteflowfiles.py
+flowcb/filter/fdelay.py          flowcb/filter/pclean_f90.py
+flowcb/filter/pclean_f92.py      flowcb/filter/wmo2msc.py
+flowcb/gather/file.py            flowcb/gather/message.py
+flowcb/housekeeping/resources.py flowcb/log.py
+flowcb/mdelaylatest.py           flowcb/nodupe/data.py
+flowcb/nodupe/name.py            flowcb/pclean.py
+flowcb/poll/airnow.py            flowcb/poll/mail.py
+flowcb/poll/nasa_mls_nrt.py      flowcb/poll/nexrad.py
+flowcb/poll/noaa_hydrometric.py  flowcb/poll/s3bucket.py
+flowcb/poll/usgs.py              flowcb/post/message.py
+flowcb/retry.py                  flowcb/rootchown.py
+flowcb/run.py                    flowcb/rxqueue_gzip.py
+flowcb/sample.py                 flowcb/send/am.py
+flowcb/send/email.py             flowcb/shiftdir2baseurl.py
+flowcb/trace_on_stop.py          flowcb/v2wrapper.py
+flowcb/wistree.py                flowcb/work/age.py
+flowcb/work/check.py             flowcb/work/citypage_check.py
+flowcb/work/delete.py            flowcb/work/rxpipe.py
+flowcb/work/send_egc_les.py
+
+
+

L’ajout de cette ligne à la configuration signifie que la sous-classe wistree flowcb (source ci-dessus) sera ajoutée au flux et modifier le traitement en faisant appeler ses routines… la principale étant after_accept

+
+
[4]:
+
+
+
+!echo callback accept.posthourtree  >>~/.config/sr3/subscribe/hpfx_amis.conf
+
+
+
+
+
[5]:
+
+
+
+!sr3 foreground subscribe/hpfx_amis.conf
+
+
+
+
+
+
+
+
+2023-05-28 16:35:34,235 1919873 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10
+.2023-05-28 16:35:34,349 [INFO] 1919874 sarracenia.config finalize overriding batch for consistency with messageCountMax: 10
+2023-05-28 16:35:34,355 [INFO] 1919874 sarracenia.config finalize overriding batch for consistency with messageCountMax: 10
+2023-05-28 16:35:34,355 [INFO] 1919874 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']
+2023-05-28 16:35:34,670 [INFO] 1919874 sarracenia.moth.amqp __getSetup queue declared q_anonymous_subscribe.hpfx_amis.33557703.14415188 (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2023-05-28 16:35:34,670 [INFO] 1919874 sarracenia.moth.amqp __getSetup binding q_anonymous_subscribe.hpfx_amis.33557703.14415188 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)
+2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flowcb.log __init__ subscribe initialized with: {'on_housekeeping', 'post', 'after_post', 'after_work', 'after_accept'}
+2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']
+2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flow run pid: 1919874 subscribe/hpfx_amis.conf instance: 0
+2023-05-28 16:35:34,764 [INFO] 1919874 sarracenia.flow run now active on vip None
+2023-05-28 16:35:35,009 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:35,009 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/CA/CWAO/20/CACN45_CWAO_281300__OBQ_20965
+2023-05-28 16:35:35,154 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/CACN45_CWAO_281300__OBQ_20965
+2023-05-28 16:35:35,677 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:35,677 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.56 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___1729
+2023-05-28 16:35:35,796 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___1729
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.55 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRWA20_KWAL_282035___43515
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.53 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SN/KWNB/20/SNVD15_KWNB_282000_RRO__50372
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.53 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SX/KWAL/20/SXAK50_KWAL_282035___51354
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 1.81 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___32251
+2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 1.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___62598
+2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRWA20_KWAL_282035___43515
+2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD15_KWNB_282000_RRO__50372
+2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SXAK50_KWAL_282035___51354
+2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___32251
+2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___62598
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS41_KKCI_282045___14468
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___20765
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS46_KKCI_282045___65023
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS44_KKCI_282045___40622
+2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.25 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___41115
+2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS41_KKCI_282045___14468
+2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___20765
+2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS46_KKCI_282045___65023
+2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS44_KKCI_282045___40622
+2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___41115
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow please_stop ok, telling 5 callbacks about it.
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow run starting last pass (without gather) through loop for cleanup.
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow please_stop ok, telling 5 callbacks about it.
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow run on_housekeeping pid: 1919874 subscribe/hpfx_amis.conf instance: 0
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.gather.message on_housekeeping messages: good: 12 bad: 0 bytes: 1.6 KiB average: 139 Bytes
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.retry on_housekeeping on_housekeeping
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping No retry in list
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000117
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping No retry in list
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000081
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.18 system=0.01
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 54.8 MiB, accumulating count (12 or 12/100 so far) before self-setting threshold
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats version: 3.00.40, started: 2 seconds ago, last_housekeeping:  2.9 seconds ago
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats messages received: 12, accepted: 12, rejected: 0   rate accepted: 100.0% or 4.1 m/s
+2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats files transferred: 12 bytes: 4.5 KiB rate: 1.5 KiB/sec
+2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flowcb.log stats lag: average: 1.91, maximum: 3.26
+2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flowcb.log on_housekeeping housekeeping
+2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flow run clean stop from run loop
+2023-05-28 16:35:37,669 [INFO] 1919874 sarracenia.flowcb.gather.message on_stop closing
+2023-05-28 16:35:37,669 [INFO] 1919874 sarracenia.flow close flow/close completed cleanly pid: 1919874 subscribe/hpfx_amis.conf instance: 0
+
+
+
+

Sans le plugin, le téléchargement mettrait tous les fichiers directement dans le répertoire de réception. avec l’ajout du retour wistree, il place le fichier dans /tmp/hpfx_amis. Avec le changement, il le place dans l’arborescence des répertoires WIS et ajoute un suffixe selon le type de fichier.

+
+

Entrées de fichier de configuration et rappels

+

flowcb.log

+

Pour ajouter un retour à un flux, une ligne est ajoutée au fichier de configuration du flux:

+
flowcb sarracenia.flowcb.log.Log
+
+
+

Si vous suivez la convention et que le nom de la classe est une version en majuscules (Log) du nom de fichier (log), alors un raccourci est disponible:

+
callback log
+
+
+

Quoi qu’il en soit, cela entraînera l’importation de la classe par Sarracenia, puis chercher des points d’entrée dans la classe à appeler aux moments opportuns.

+

Le constructeur de classe accepte un objet de classe sarracenia.config.Config, appelé options, qui stocke tous les paramètres à utiliser par le flux en cours d’exécution. Les options sont utilisées pour remplacer le comportement par défaut des flux et des rappels. L’argument de flowcb est une classe python standard qui doit se trouver dans le chemin python normal pour python import, et le dernier élément est le nom de la classe dans le fichier qui doit être instancié en tant qu’instance +flowcb.

+

un paramètre pour un rappel est déclaré comme suit :

+
set sarracenia.flowcb.filter.log.Log.logLevel debug
+
+
+

(le préfixe du paramètre correspond à la hiérarchie des types dans flowCallback)

+

lorsque le constructeur du rappel est appelé, son argument options contiendra :

+
options.logLevel = 'debug'
+
+
+

Si aucun remplacement spécifique au module n’est présent, le paramètre le plus global est utilisé.

+

Ainsi, l’utilisation des rappels peut être faite sans beaucoup de connaissances en python, juste la possibilité de créer des fichiers de configuration.

+

Au-delà de ce point, nous trouvons des conseils pour les personnes qui souhaitent écrire leurs propres retours en Python. Les rappels sont en Python ordinaire, avec quelques plis:

+
+
+

Écrire Vos Propres Rappels

+

Un rappel de flux est une classe python construite avec des routines nommées pour indiquer quand le programmeur veut qu’elles soient appelées. Pour ce faire, créez une routine qui sous-classe sarracenia.flowcb.FlowCB afin que la classe ait normalement:

+

from sarracenia.flowcb import FlowCB

+

parmi les importations dans le haut du fichier. Dans la partie principale du fichier, il y aura les classes de rappel personnalisées:

+

class Myclass(FlowCB):

+

déclarée comme sous-classe en tant que FlowCB. Les principales routines de la classe sont des points d’entrée qui seront appelés au moment où leur nom l’indique. S’il manque à une classe un point d’entrée donné, elle ne sera tout simplement pas appelée. La classe init() est utilisée pour initialiser les choses pour la classe de rappel :

+
def __init__(self, options):
+
+    self.o = options
+
+    logging.basicConfig(format=self.o.logFormat,
+                        level=getattr(logging, self.o.logLevel.upper()))
+    logger.setLevel(getattr(logging, self.o.logLevel.upper()))
+
+    self.o.add_option( 'myoption', 'str', 'usuallyThis')
+
+
+

Les lignes de configuration du logging dans init permettent de définir un niveau de logging spécifique pour cette classe flowCallback. Une fois le passe-partout de logging terminé, la routine add_option pour définir les paramètres de la classe. Les utilisateurs peuvent les inclure dans les fichiers de configuration, tout comme les options intégrées:

+
myoption IsReallyNeeded
+
+
+

Le résultat d’un tel réglage est que self.o.myoption = ‘IsReallyNeeded’. Si aucune valeur n’est définie dans la configuration, self.o.myoption sera par défaut ‘usualThis’ Il existe différents kinds (types) d’options, où le type déclaré modifie l’analyse:

+
'count'    type de nombre entier.
+'duration' un nombre à virgule flottante indiquant une quantité de secondes (0.001 est 1 milliseconde)
+           modifié par un suffixe d'unité ( m-minute, h-hour (heure), w-week(semaine) )
+'flag'     option booléenne (Vrai/Faux).
+'list'     une liste de valeurs de chaîne, chaque occurrence successive étant enchaînée au total.
+           toutes les options du plugin v2 sont déclarées de type liste.
+'taille'   taille entière. Suffixes k, m et g pour les multiplicateurs kilo, méga et giga (base 2).
+'str'      une valeur de chaîne arbitraire, comme tous les types ci-dessus, chaque
+           occurrence suivante remplace la précédente.
+
+
+
+
+

Listes De Travail

+

Autre qu’ options, l’autre argument principal des routines de rappel after_accept et after_work est la liste de travail. La liste de travail est donnée aux points d’entrée se produisant pendant le traitement des messages et est un certain nombre de listes de travail de messages:

+
worklist.incoming --> messages to process (either new or retries.)
+worklist.ok       --> successfully processed
+worklist.rejected --> messages to not be further processed.
+worklist.failed   --> messages for which processing failed.
+                      failed messages will be retried.
+worklist.directories_ok --> list of directories created during processing.
+
+
+

Initialement, tous les messages sont placés dans worklists.incoming. Si un plugin décide :

+
    +
  • a message is not relevant, moved it to the rejected worklist.

  • +
  • a no further processing of the message is needed, move it to ok worklist.

  • +
  • an operation failed and it should be retried later, move to failed worklist.

  • +
+

Ne supprimez pas de toutes les listes, déplacez uniquement les messages entre les listes de travail. Il est nécessaire de mettre les messages rejetés dans la liste de travail appropriée afin qu’ils soient reconnus comme reçus. Les messages ne peuvent être supprimés qu’après la prise en charge de l’accusé de réception.

+
+
+

Sortie d’Exécution

+

Python a une excellente journalisation intégrée et doit une fois utiliser le module de manière normale et pythonique, avec:

+

import logging

+

Après toutes les importations dans votre fichier source python, définissez un enregistreur pour le fichier source:

+

logger = logging.getLogger(_name_)

+

Comme c’est normal avec le module de journalisation Python, les messages peuvent ensuite être publiés dans le journal:

+

logger.debug(‘got here’)

+

Chaque message du journal sera précédé de la classe et de la routine émettant le message de journal, ainsi que de la date/heure.

+
+
+

Exemple de sous-classe Flowcb

+

Avec les informations ci-dessus sur la gestion des options, les listes de travail et la journalisation, nous sommes prêts à comprendre le module wistree que nous venons d’utiliser. Cette classe wistree.py accepte les fichiers dont les noms commencent par AHL et renomme l’arborescence de répertoires dans un standard différent, celui en évolution pour le WMO WIS 2.0 (pour plus d’informations sur ce module: https://github.com/wmo-im/GTStoWIS2)

+
+
[6]:
+
+
+
+  from sarracenia.flowcb import FlowCB
+  import logging
+  import GTStoWIS2
+
+  logger = logging.getLogger(__name__)
+
+
+  class Wistree(FlowCB):
+
+    def __init__(self, options):
+
+        if hasattr(options, 'logLevel'):
+            logger.setLevel(getattr(logging, options.logLevel.upper()))
+        else:
+            logger.setLevel(logging.INFO)
+        self.topic_builder=GTStoWIS2.GTStoWIS2()
+        self.o = options
+
+
+    def after_accept(self, worklist):
+
+        new_incoming=[]
+
+        for msg in worklist.incoming:
+
+            # fix file name suffix.
+            type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )
+            tpfx=msg['subtopic']
+
+            # input has relpath=/YYYYMMDDTHHMM/... + pubTime
+            # need to move the date from relPath to BaseDir, adding the T hour from pubTime.
+            try:
+                new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]
+                t='.'.join(tpfx[0:2])+'.'+new_baseSubDir
+                new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir
+                new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])
+                new_dir = new_baseDir + os.sep + new_relDir
+
+                if msg['new_file'][-len(type_suffix):] != type_suffix:
+                    new_file = msg['new_file']+type_suffix
+                else:
+                    new_file = msg['new_file']
+
+                msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, new_file )
+            except Exception as ex:
+                logger.error( "skipped" , exc_info=True )
+                worklist.failed.append(msg)
+                continue
+
+            msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )
+            new_incoming.append(msg)
+
+        worklist.incoming=new_incoming
+
+
+
+
+
+
+
+
+
+
+---------------------------------------------------------------------------
+ModuleNotFoundError                       Traceback (most recent call last)
+Cell In[6], line 3
+      1 from sarracenia.flowcb import FlowCB
+      2 import logging
+----> 3 import GTStoWIS2
+      5 logger = logging.getLogger(__name__)
+      8 class Wistree(FlowCB):
+
+ModuleNotFoundError: No module named 'GTStoWIS2'
+
+
+
+
+

Plugins qui changent la façon dont un fichier est téléchargé

+

La routine after_accept est l’une des deux plus couramment utilisées. Il est utilisé pour modifier le traitement avant le téléchargement ou l’envoi d’un fichier. Pour traiter le fichier après son téléchargement, le point d’entrée after_work est utilisé pour traiter la liste worklist.ok (fichiers qui ont été téléchargés avec succès).

+

La routine after_accept a une boucle externe qui parcourt toute la liste des messages entrants. Il construit une nouvelle liste de messages entrants à partir de ceux qu’il accepte, tout en ajoutant tous les messages rejetés à worklist.failed. La liste est juste une liste de messages, où chaque message est un dictionnaire python avec tous les champs stockés dans un message au format v03. Dans le message, il y a, par exemple, les champs baseURL et relPath :

+
    +
  • baseURL - la baseURL de la ressource à partir de laquelle un fichier serait obtenu.

  • +
  • relPath - le chemin relatif à ajouter à la baseURL pour obtenir l’URL de téléchargement complète.

  • +
+

Cela se produit avant que le transfert (téléchargement ou envoi, ou traitement) du fichier ait eu lieu, on peut donc changer le comportement en modifiant les champs du message. Normalement, les chemins de téléchargement (appelés new_dir et new_file) refléteront l’intention de copier l’arborescence source d’origine. donc si vous avez a/b/c.txt sur l’arborescence des sources et que vous téléchargez dans le répertoire mine sur le système local, le new_dir serait mine/a/b et new_file serait +c.txt.

+
+
+

Plugins qui Traitent un Fichier après son Téléchargement

+

Un cas d’utilisation courant est pour les plugins avec un point d’entrée after_work pour lire le fichier après son téléchargement et le transformer en un produit dérivé avec un nom différent. Ainsi, le nouveau fichier est créé comme dans la section précédente. Le message pour le fichier téléchargé doit encore être déplacé sur une liste pour s’assurer qu’il est reconnu par le courtier. Un tel point d’entrée ressemblerait à ceci:

+
+
[7]:
+
+
+
+
+    def after_work(self, worklist):
+
+        new_ok=[]
+        for m in worklist.ok:
+             success=do_something()
+             if success:
+                   new_ok.append(m)
+             # since it is already acknowledged, we can just drop it from ok.
+
+
+        worklist.ok = new_ok
+        # the messages on worklist.ok will get posted in the next algorithm phase.
+
+
+
+
+
+

Plugins qui renomment les fichiers

+

Le plugin ci-dessus modifie la disposition des fichiers à télécharger, en fonction de la classe GTStoWIS, qui prescrit une arborescence de répertoires différente en sortie. Il y a beaucoup de champs à mettre à jour lors de la modification du placement des fichiers, il est donc préférable d’utiliser:

+

msg.updatePaths( self.o, new_dir, new_file )

+

pour mettre à jour correctement tous les champs nécessaires dans le message. Il mettra à jour ‘new_baseURL’, ‘new_relPath’, ‘new_subtopic’ à utiliser lors de la publication.

+

La partie try/except de la routine traite du cas où, si un fichier arrive avec un nom à partir duquel une arborescence de rubriques ne peut pas être construite, une exception python peut se produire et le message est ajouté à la liste de travail ayant échoué et ne sera pas être traitées par des plugins ultérieurs.

+
+
+

Plugins qui Créent de Nouveaux Fichiers

+

La routine ci-dessus est parfaite lorsqu’un fichier vient d’être renommé. Si un plugin a besoin de créer de nouveaux fichiers vaguement dérivés du fichier d’entrée, alors vous voulez créer de nouveaux messages pour ces fichiers à partir de rien:

+
import sarracenia
+
+m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )
+
+
+

La routine msg_fromFileData utilisera self.o pour appliquer les paramètres de publication appropriés. Aucune connaissance des formats de message ou de la construction de champs n’est nécessaire. Si le fichier n’est pas local, comme lors de l’écriture d’un rappel d’interrogation, un routage alternatif peut être utilisé:

+
m = sarracenia.Message.fromFileInfo(sample_fileName, self.o, fake_stat_info )
+
+
+

le faux enregistrement de statistiques (selon la page de manuel stat(2) ou python os.stat() ) peut être construit à partir d’autres champs, en commençant par:

+
import paramiko
+
+fake_stat = paramiko.SFTPAttributes()
+fake_stat.st_mtime = ... something else... perhaps an http header?
+fake_stat.st_size = ... again will vary by context.
+
+
+

Dans tous les cas, une fois que vous avez le message, il peut être ajouté à la liste entrante.

+
+
+

Other Examples

+

Le sous-classement de Sarracenia.flowcb est utilisé en interne pour faire beaucoup de travail de base. C’est une bonne idée de regarder le code source de Sarracenia lui-même. Par exemple:

+
    +
  • sarracenia.flowcb jetez un oeil au fichier init.py là qui fournit ces informations sur un format plus programmatiquement succinct.

  • +
  • sarracenia.flowcb.gather.file est une classe qui implémente: la publication de fichiers et la surveillance de répertoires, dans le sens d’un rappel qui implémente le point d’entrée gather, en lisant un système de fichiers et en construisant un liste des messages à traiter.

  • +
  • sarracenia.flowcb.gather.message est une classe qui implémente la réception de messages à partir de flux de protocole de file d’attente de messages.

  • +
  • sarracenia.flowcb.gather.nodupe Ce module supprime les doublons du message flux basés sur les sommes de contrôle d’intégrité.

  • +
  • sarracenia.flowcb.post.message est une classe qui implémente la publication messages vers flux de protocole de file d’attente de messages

  • +
  • sarracenia.flowcb.retry lorsque le transfert d’un fichier échoue. Sarracenia doit conserver le message pertinent dans un fichier d’état pour un moment ultérieur où il pourra être réessayé.

  • +
+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/2_CLI_with_flowcb_demo.ipynb b/fr/Tutoriel/2_CLI_with_flowcb_demo.ipynb new file mode 100644 index 000000000..cc269c004 --- /dev/null +++ b/fr/Tutoriel/2_CLI_with_flowcb_demo.ipynb @@ -0,0 +1,677 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "acoustic-deviation", + "metadata": {}, + "source": [ + "# Personnalisez la gestion des fichiers avec les rappels.\n", + "\n", + "Tous les composants de Sarracenia implémentent *l'algorithme Flow*, avec différents\n", + "rappels, dans le langage de programmation Python. La classe principale de Sarracenia (Python)\n", + "est [sarracenia.flow](../Reference/code.html#module-sarracenia.flow) et la grande partie de la fonctionnalité de base est\n", + "mis en œuvre à l'aide de la classe créée pour ajouter un traitement personnalisé à un flux, le\n", + "classe flowcb (rappel de flux).\n", + "\n", + "Pour une discussion détaillée de l'algorithme de flux lui-même, jetez un oeil\n", + "dans le manuel [Concepts](../Explanation/Concepts.html). Pour tout flux, on peut\n", + "ajouter un traitement personnalisé à divers moments pendant le traitement par sous-classement\n", + "la classe [sarracenia.flowcb](../Reference/flowcb.html).\n", + "\n", + "En bref, l'algorithme comporte les étapes suivantes :\n", + "\n", + "* **__init__(self, options)** -- lors de l'import, initialisation de python traditionnelle\n", + "* **on_start** -- lorsqu'une instance est démarrée.\n", + "* boucle pour toujours\n", + " * **gather** -- collecte les messages à traiter appelés : worklist.incoming\n", + " * **poll** -- une autre façon de collecter des messages, uniquement dans le composant poll.\n", + " * **filter** -- applique les correspondances d'expressions régulières acceptées/rejetées à la liste de messages.\n", + " déplace les messages pour les fichiers à ne pas télécharger de worklist.incoming vers worklist.reject\n", + " * *after_accept* point d'entrée de rappel. traiter worklist.incoming, en en rejetant potentiellement d'autres.\n", + " * **ack** -- les messages worklist.rejected sont reconnus à la source en amont lorsque le traitement est terminé.\n", + " * **work** -- effectue un transfert ou une transformation sur un fichier.\n", + " * **ack** -- les messages worklist.ok pour les fichiers transférés avec succès sont reconnus à la source en amont.\n", + " * Point d'entrée du rappel *after_work*\n", + " * **ack** -- les messages worklist.failed pour les fichiers qui n'ont pas été transférés avec succès sont reconnus.\n", + " * **post** -- publier le résultat du travail effectué pour l'étape suivante.\n", + " * occasionnellement... **on_housekeeping -- faire des nettoyages périodiques...\n", + "* **on_stop** -- arrêt du traitement.\n", + "\n", + "pour plus de détails sur les points d'entrée disponibles de flowcb, consultez le code source:\n", + "\n", + "* [flowcb](../Reference/flowcb.html)\n" + ] + }, + { + "cell_type": "markdown", + "id": "external-mention", + "metadata": {}, + "source": [ + "Regardons l'utilisation de la classe dans une configuration :" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "coordinated-cocktail", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-05-28 16:35:16,492 1919860 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10\n", + "2023-05-28 16:35:16,493 1919860 [INFO] root remove removing subscribe/hpfx_amis\n", + "\n", + "add: 2023-05-28 16:35:17,637 1919863 [INFO] sarracenia.sr add copying: /home/peter/Sarracenia/sr3/sarracenia/examples/subscribe/hpfx_amis.conf to /home/peter/.config/sr3/subscribe/hpfx_amis.conf \n", + "\n" + ] + } + ], + "source": [ + "!sr3 remove subscribe/hpfx_amis.conf\n", + "!sr3 add subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "tired-north", + "metadata": {}, + "outputs": [], + "source": [ + "!echo messageCountMax 10 >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "psychological-ratio", + "metadata": {}, + "source": [ + "fait en sorte que le flux s'arrête après la consommation de 10 messages." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "greater-nevada", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-05-28 16:35:26,803 1919870 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10\r\n", + "Provided callback classes: ( /home/peter/Sarracenia/sr3/sarracenia ) \r\n", + "flowcb/accept/dateappend.py flowcb/accept/delete.py \r\n", + "flowcb/accept/downloadbaseurl.py flowcb/accept/hourtree.py \r\n", + "flowcb/accept/httptohttps.py flowcb/accept/longflow.py \r\n", + "flowcb/accept/pathreplace.py flowcb/accept/posthourtree.py \r\n", + "flowcb/accept/postoverride.py flowcb/accept/printlag.py \r\n", + "flowcb/accept/rename4jicc.py flowcb/accept/renamedmf.py \r\n", + "flowcb/accept/renamewhatfn.py flowcb/accept/save.py \r\n", + "flowcb/accept/speedo.py flowcb/accept/sundewpxroute.py \r\n", + "flowcb/accept/testretry.py flowcb/accept/toclusters.py \r\n", + "flowcb/accept/tohttp.py flowcb/accept/tolocal.py \r\n", + "flowcb/accept/tolocalfile.py flowcb/accept/wmotypesuffix.py \r\n", + "flowcb/amserver.py flowcb/clamav.py \r\n", + "flowcb/destfn/replace.py flowcb/destfn/sample.py \r\n", + "flowcb/download/mail_ingest.py flowcb/filter/deleteflowfiles.py \r\n", + "flowcb/filter/fdelay.py flowcb/filter/pclean_f90.py \r\n", + "flowcb/filter/pclean_f92.py flowcb/filter/wmo2msc.py \r\n", + "flowcb/gather/file.py flowcb/gather/message.py \r\n", + "flowcb/housekeeping/resources.py flowcb/log.py \r\n", + "flowcb/mdelaylatest.py flowcb/nodupe/data.py \r\n", + "flowcb/nodupe/name.py flowcb/pclean.py \r\n", + "flowcb/poll/airnow.py flowcb/poll/mail.py \r\n", + "flowcb/poll/nasa_mls_nrt.py flowcb/poll/nexrad.py \r\n", + "flowcb/poll/noaa_hydrometric.py flowcb/poll/s3bucket.py \r\n", + "flowcb/poll/usgs.py flowcb/post/message.py \r\n", + "flowcb/retry.py flowcb/rootchown.py \r\n", + "flowcb/run.py flowcb/rxqueue_gzip.py \r\n", + "flowcb/sample.py flowcb/send/am.py \r\n", + "flowcb/send/email.py flowcb/shiftdir2baseurl.py \r\n", + "flowcb/trace_on_stop.py flowcb/v2wrapper.py \r\n", + "flowcb/wistree.py flowcb/work/age.py \r\n", + "flowcb/work/check.py flowcb/work/citypage_check.py \r\n", + "flowcb/work/delete.py flowcb/work/rxpipe.py \r\n", + "flowcb/work/send_egc_les.py \r\n" + ] + } + ], + "source": [ + "!sr3 list fcb" + ] + }, + { + "cell_type": "markdown", + "id": "efficient-picture", + "metadata": {}, + "source": [ + "L'ajout de cette ligne à la configuration signifie que la sous-classe wistree flowcb (source ci-dessus) sera ajoutée au\n", + "flux et modifier le traitement en faisant appeler ses routines... la principale étant *after_accept*" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "external-commercial", + "metadata": {}, + "outputs": [], + "source": [ + "!echo callback accept.posthourtree >>~/.config/sr3/subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "insured-fetish", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-05-28 16:35:34,235 1919873 [INFO] sarracenia.config finalize overriding batch for consistency with messageCountMax: 10\n", + ".2023-05-28 16:35:34,349 [INFO] 1919874 sarracenia.config finalize overriding batch for consistency with messageCountMax: 10\n", + "2023-05-28 16:35:34,355 [INFO] 1919874 sarracenia.config finalize overriding batch for consistency with messageCountMax: 10\n", + "2023-05-28 16:35:34,355 [INFO] 1919874 sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']\n", + "2023-05-28 16:35:34,670 [INFO] 1919874 sarracenia.moth.amqp __getSetup queue declared q_anonymous_subscribe.hpfx_amis.33557703.14415188 (as: amqps://anonymous@hpfx.collab.science.gc.ca/) \n", + "2023-05-28 16:35:34,670 [INFO] 1919874 sarracenia.moth.amqp __getSetup binding q_anonymous_subscribe.hpfx_amis.33557703.14415188 with v02.post.*.WXO-DD.bulletins.alphanumeric.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca/)\n", + "2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flowcb.log __init__ subscribe initialized with: {'on_housekeeping', 'post', 'after_post', 'after_work', 'after_accept'}\n", + "2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'accept.posthourtree', 'log']\n", + "2023-05-28 16:35:34,723 [INFO] 1919874 sarracenia.flow run pid: 1919874 subscribe/hpfx_amis.conf instance: 0\n", + "2023-05-28 16:35:34,764 [INFO] 1919874 sarracenia.flow run now active on vip None\n", + "2023-05-28 16:35:35,009 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:35,009 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/CA/CWAO/20/CACN45_CWAO_281300__OBQ_20965 \n", + "2023-05-28 16:35:35,154 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/CACN45_CWAO_281300__OBQ_20965 \n", + "2023-05-28 16:35:35,677 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:35,677 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.56 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___1729 \n", + "2023-05-28 16:35:35,796 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___1729 \n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.55 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRWA20_KWAL_282035___43515 \n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.53 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SN/KWNB/20/SNVD15_KWNB_282000_RRO__50372 \n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 0.53 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SX/KWAL/20/SXAK50_KWAL_282035___51354 \n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 1.81 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___32251 \n", + "2023-05-28 16:35:36,314 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 1.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___62598 \n", + "2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRWA20_KWAL_282035___43515 \n", + "2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SNVD15_KWNB_282000_RRO__50372 \n", + "2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SXAK50_KWAL_282035___51354 \n", + "2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___32251 \n", + "2023-05-28 16:35:36,951 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___62598 \n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.accept.posthourtree after_accept post_hour_tree: new_dir: /tmp/hpfx_amis/20\n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS41_KKCI_282045___14468 \n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___20765 \n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS46_KKCI_282045___65023 \n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.26 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/WA/KKCI/20/WAUS44_KKCI_282045___40622 \n", + "2023-05-28 16:35:37,133 [INFO] 1919874 sarracenia.flowcb.log after_accept accepted: (lag: 3.25 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/bulletins/alphanumeric/20230528/SR/KWAL/20/SRCN40_KWAL_282035___41115 \n", + "2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS41_KKCI_282045___14468 \n", + "2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___20765 \n", + "2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS46_KKCI_282045___65023 \n", + "2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/WAUS44_KKCI_282045___40622 \n", + "2023-05-28 16:35:37,636 [INFO] 1919874 sarracenia.flowcb.log after_work downloaded ok: /tmp/hpfx_amis/20/SRCN40_KWAL_282035___41115 \n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow please_stop ok, telling 5 callbacks about it.\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow run starting last pass (without gather) through loop for cleanup.\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow please_stop ok, telling 5 callbacks about it.\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flow run on_housekeeping pid: 1919874 subscribe/hpfx_amis.conf instance: 0\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.gather.message on_housekeeping messages: good: 12 bad: 0 bytes: 1.6 KiB average: 139 Bytes\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.retry on_housekeeping on_housekeeping\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000117\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping No retry in list\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000081\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.18 system=0.01\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 54.8 MiB, accumulating count (12 or 12/100 so far) before self-setting threshold\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats version: 3.00.40, started: 2 seconds ago, last_housekeeping: 2.9 seconds ago \n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats messages received: 12, accepted: 12, rejected: 0 rate accepted: 100.0% or 4.1 m/s\n", + "2023-05-28 16:35:37,637 [INFO] 1919874 sarracenia.flowcb.log stats files transferred: 12 bytes: 4.5 KiB rate: 1.5 KiB/sec\n", + "2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flowcb.log stats lag: average: 1.91, maximum: 3.26 \n", + "2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flowcb.log on_housekeeping housekeeping\n", + "2023-05-28 16:35:37,638 [INFO] 1919874 sarracenia.flow run clean stop from run loop\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2023-05-28 16:35:37,669 [INFO] 1919874 sarracenia.flowcb.gather.message on_stop closing\r\n", + "2023-05-28 16:35:37,669 [INFO] 1919874 sarracenia.flow close flow/close completed cleanly pid: 1919874 subscribe/hpfx_amis.conf instance: 0\r\n", + "\r\n" + ] + } + ], + "source": [ + "!sr3 foreground subscribe/hpfx_amis.conf" + ] + }, + { + "cell_type": "markdown", + "id": "stretch-directive", + "metadata": {}, + "source": [ + "Sans le plugin, le téléchargement mettrait tous les fichiers directement dans le répertoire de réception. avec l'ajout du retour wistree, il place le fichier dans /tmp/hpfx_amis. Avec le changement, il le place dans l'arborescence des répertoires WIS et ajoute un suffixe selon le type de fichier." + ] + }, + { + "cell_type": "markdown", + "id": "funny-isolation", + "metadata": {}, + "source": [ + "## Entrées de fichier de configuration et rappels\n", + "\n", + "\n", + "[flowcb.log](../Reference/flowcb.html#module-sarracenia.flowcb.log)\n", + "\n", + "Pour ajouter un retour à un flux, une ligne est ajoutée au fichier de configuration du flux:\n", + "\n", + " flowcb sarracenia.flowcb.log.Log\n", + "\n", + "Si vous suivez la convention et que le nom de la classe est une version en \n", + "majuscules (Log) du nom de fichier (log), alors un raccourci est disponible:\n", + "\n", + " callback log \n", + "\n", + "Quoi qu'il en soit, cela entraînera l'importation de la classe par Sarracenia, puis\n", + "chercher des points d'entrée dans la classe à appeler aux moments opportuns.\n", + "\n", + "Le constructeur de classe accepte un objet de classe sarracenia.config.Config, \n", + "appelé options, qui stocke tous les paramètres à utiliser par le flux en cours d'exécution. \n", + "Les options sont utilisées pour remplacer le comportement par défaut des flux et des rappels. \n", + "L'argument de flowcb est une classe python standard qui doit se trouver dans le chemin \n", + "python normal pour python *import*, et le dernier élément est le nom de la classe dans le fichier \n", + "qui doit être instancié en tant qu'instance flowcb.\n", + "\n", + "un paramètre pour un rappel est déclaré comme suit :\n", + "\n", + " set sarracenia.flowcb.filter.log.Log.logLevel debug\n", + "\n", + "(le préfixe du paramètre correspond à la hiérarchie des types dans flowCallback)\n", + "\n", + "lorsque le constructeur du rappel est appelé, \n", + "son argument options contiendra :\n", + "\n", + " options.logLevel = 'debug'\n", + "\n", + "Si aucun remplacement spécifique au module n'est présent, \n", + "le paramètre le plus global est utilisé.\n", + "\n", + "Ainsi, l'utilisation des rappels peut être faite sans beaucoup de connaissances en python, \n", + "juste la possibilité de créer des fichiers de configuration.\n", + "\n", + "Au-delà de ce point, nous trouvons des conseils pour les personnes qui souhaitent écrire \n", + "leurs propres retours en Python. Les rappels sont en Python ordinaire, avec quelques plis:" + ] + }, + { + "cell_type": "markdown", + "id": "shared-album", + "metadata": {}, + "source": [ + "## Écrire Vos Propres Rappels\n", + "\n", + "Un rappel de flux est une classe python construite avec des routines nommées \n", + "pour indiquer quand le programmeur veut qu'elles soient appelées. Pour ce faire, \n", + "créez une routine qui sous-classe *sarracenia.flowcb.FlowCB* \n", + "afin que la classe ait normalement:\n", + "\n", + " from sarracenia.flowcb import FlowCB\n", + "\n", + "parmi les importations dans le haut du fichier. \n", + "Dans la partie principale du fichier, il y aura \n", + "les classes de rappel personnalisées:\n", + "\n", + " class Myclass(FlowCB):\n", + "\n", + "déclarée comme sous-classe en tant que FlowCB. Les principales routines de la classe \n", + "sont des points d'entrée qui seront appelés au moment où leur nom l'indique. \n", + "S'il manque à une classe un point d'entrée donné, elle ne sera tout simplement pas appelée. \n", + "La classe __init__() est utilisée pour initialiser les choses pour la classe de rappel :\n", + "\n", + " def __init__(self, options):\n", + "\n", + " self.o = options\n", + "\n", + " logging.basicConfig(format=self.o.logFormat,\n", + " level=getattr(logging, self.o.logLevel.upper()))\n", + " logger.setLevel(getattr(logging, self.o.logLevel.upper()))\n", + "\n", + " self.o.add_option( 'myoption', 'str', 'usuallyThis')\n", + "\n", + "Les lignes de configuration du logging dans __init__ permettent de définir \n", + "un niveau de logging spécifique pour cette classe flowCallback. Une fois le \n", + "passe-partout de logging terminé, la routine add_option pour définir les paramètres de la classe. \n", + "Les utilisateurs peuvent les inclure dans les fichiers de configuration, tout comme les options intégrées:\n", + "\n", + " myoption IsReallyNeeded\n", + "\n", + "Le résultat d'un tel réglage est que *self.o.myoption = 'IsReallyNeeded'*. \n", + "Si aucune valeur n'est définie dans la configuration, *self.o.myoption* sera par défaut *'usualThis'* \n", + "Il existe différents *kinds* (types) d'options, où le type déclaré modifie l'analyse:\n", + "\n", + " 'count' type de nombre entier.\n", + " 'duration' un nombre à virgule flottante indiquant une quantité de secondes (0.001 est 1 milliseconde)\n", + " modifié par un suffixe d'unité ( m-minute, h-hour (heure), w-week(semaine) )\n", + " 'flag' option booléenne (Vrai/Faux).\n", + " 'list' une liste de valeurs de chaîne, chaque occurrence successive étant enchaînée au total.\n", + " toutes les options du plugin v2 sont déclarées de type liste.\n", + " 'taille' taille entière. Suffixes k, m et g pour les multiplicateurs kilo, méga et giga (base 2).\n", + " 'str' une valeur de chaîne arbitraire, comme tous les types ci-dessus, chaque\n", + " occurrence suivante remplace la précédente.\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "determined-medicare", + "metadata": {}, + "source": [ + "## Listes De Travail\n", + "\n", + "Autre qu' *options*, l'autre argument principal des routines de rappel after_accept et after_work est la liste de travail. \n", + "La liste de travail est donnée aux points d'entrée se produisant pendant le traitement des messages \n", + "et est un certain nombre de listes de travail de messages:\n", + "\n", + " worklist.incoming --> messages to process (either new or retries.)\n", + " worklist.ok --> successfully processed\n", + " worklist.rejected --> messages to not be further processed.\n", + " worklist.failed --> messages for which processing failed.\n", + " failed messages will be retried.\n", + " worklist.directories_ok --> list of directories created during processing.\n", + "\n", + "Initialement, tous les messages sont placés dans worklists.incoming. \n", + "Si un plugin décide :\n", + "\n", + "- a message is not relevant, moved it to the rejected worklist.\n", + "- a no further processing of the message is needed, move it to ok worklist.\n", + "- an operation failed and it should be retried later, move to failed worklist.\n", + "\n", + "Ne supprimez pas de toutes les listes, déplacez uniquement les messages entre les \n", + "listes de travail. Il est nécessaire de mettre les messages rejetés dans la liste de travail \n", + "appropriée afin qu'ils soient reconnus comme reçus. Les messages ne peuvent\n", + "être supprimés qu'après la prise en charge de l'accusé de réception." + ] + }, + { + "cell_type": "markdown", + "id": "aa6cfe2b", + "metadata": {}, + "source": [ + "## Sortie d'Exécution\n", + "\n", + "Python a une excellente journalisation intégrée et doit une fois utiliser le module \n", + "de manière normale et pythonique, avec:\n", + "\n", + " import logging\n", + "\n", + "Après toutes les importations dans votre fichier source python, définissez \n", + "un enregistreur pour le fichier source:\n", + "\n", + " logger = logging.getLogger(\\__name\\__)\n", + "\n", + "Comme c'est normal avec le module de journalisation Python, les messages \n", + "peuvent ensuite être publiés dans le journal:\n", + "\n", + " logger.debug('got here')\n", + "\n", + "Chaque message du journal sera précédé de la classe et de la routine émettant\n", + "le message de journal, ainsi que de la date/heure." + ] + }, + { + "cell_type": "markdown", + "id": "million-smoke", + "metadata": {}, + "source": [ + "## Exemple de sous-classe Flowcb\n", + "\n", + "Avec les informations ci-dessus sur la gestion des options, les listes de travail et la journalisation, \n", + "nous sommes prêts à comprendre le module wistree que nous venons d'utiliser.\n", + "Cette classe wistree.py accepte les fichiers dont les noms commencent par AHL et renomme l'arborescence \n", + "de répertoires dans un standard différent, celui en évolution pour le WMO WIS 2.0 (pour plus d'informations sur ce module: \n", + "https://github.com/wmo-im/GTStoWIS2)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "related-consensus", + "metadata": {}, + "outputs": [ + { + "ename": "ModuleNotFoundError", + "evalue": "No module named 'GTStoWIS2'", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mModuleNotFoundError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[6], line 3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01msarracenia\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mflowcb\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m FlowCB\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mlogging\u001b[39;00m\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mGTStoWIS2\u001b[39;00m\n\u001b[1;32m 5\u001b[0m logger \u001b[38;5;241m=\u001b[39m logging\u001b[38;5;241m.\u001b[39mgetLogger(\u001b[38;5;18m__name__\u001b[39m)\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m \u001b[38;5;21;01mWistree\u001b[39;00m(FlowCB):\n", + "\u001b[0;31mModuleNotFoundError\u001b[0m: No module named 'GTStoWIS2'" + ] + } + ], + "source": [ + " from sarracenia.flowcb import FlowCB\n", + " import logging\n", + " import GTStoWIS2\n", + "\n", + " logger = logging.getLogger(__name__)\n", + "\n", + "\n", + " class Wistree(FlowCB):\n", + "\n", + " def __init__(self, options):\n", + "\n", + " if hasattr(options, 'logLevel'):\n", + " logger.setLevel(getattr(logging, options.logLevel.upper()))\n", + " else:\n", + " logger.setLevel(logging.INFO)\n", + " self.topic_builder=GTStoWIS2.GTStoWIS2()\n", + " self.o = options\n", + "\n", + "\n", + " def after_accept(self, worklist):\n", + "\n", + " new_incoming=[]\n", + "\n", + " for msg in worklist.incoming:\n", + "\n", + " # fix file name suffix.\n", + " type_suffix = self.topic_builder.mapAHLtoExtension( msg['new_file'][0:2] )\n", + " tpfx=msg['subtopic']\n", + " \n", + " # input has relpath=/YYYYMMDDTHHMM/... + pubTime\n", + " # need to move the date from relPath to BaseDir, adding the T hour from pubTime.\n", + " try:\n", + " new_baseSubDir=tpfx[0]+msg['pubTime'][8:11]\n", + " t='.'.join(tpfx[0:2])+'.'+new_baseSubDir\n", + " new_baseDir = msg['new_dir'] + os.sep + new_baseSubDir\n", + " new_relDir = 'WIS' + os.sep + self.topic_builder.mapAHLtoTopic(msg['new_file'])\n", + " new_dir = new_baseDir + os.sep + new_relDir\n", + " \n", + " if msg['new_file'][-len(type_suffix):] != type_suffix: \n", + " new_file = msg['new_file']+type_suffix\n", + " else:\n", + " new_file = msg['new_file']\n", + " \n", + " msg.updatePaths( self.o, new_baseDir + os.sep + new_relDir, new_file )\n", + " except Exception as ex:\n", + " logger.error( \"skipped\" , exc_info=True )\n", + " worklist.failed.append(msg)\n", + " continue\n", + " \n", + " msg['_deleteOnPost'] |= set( [ 'from_cluster', 'sum', 'to_clusters' ] )\n", + " new_incoming.append(msg)\n", + "\n", + " worklist.incoming=new_incoming \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "offshore-student", + "metadata": {}, + "source": [ + "\n", + "## Plugins qui changent la façon dont un fichier est téléchargé\n", + "\n", + "\n", + "La routine *after_accept* est l'une des deux plus couramment utilisées. Il est utilisé pour modifier le traitement avant le téléchargement ou l'envoi d'un fichier. Pour traiter le fichier après son téléchargement, le point d'entrée *after_work* est utilisé pour traiter la liste worklist.ok (fichiers qui ont été téléchargés avec succès).\n", + "\n", + "La routine after_accept a une boucle externe qui parcourt toute la liste des messages entrants. Il construit une nouvelle liste de messages entrants à partir de ceux qu'il accepte, tout en ajoutant tous les messages rejetés à *worklist.failed.* La liste est juste une liste de messages, où chaque message est un dictionnaire python avec tous les champs stockés dans un message au format v03. Dans le message, il y a, par exemple, les champs *baseURL* et *relPath* :\n", + "\n", + "* baseURL - la baseURL de la ressource à partir de laquelle un fichier serait obtenu.\n", + "* relPath - le chemin relatif à ajouter à la baseURL pour obtenir l'URL de téléchargement complète.\n", + "\n", + "Cela se produit avant que le transfert (téléchargement ou envoi, ou traitement) du fichier ait eu lieu, on peut donc changer le comportement en modifiant les champs du message. Normalement, les chemins de téléchargement (appelés new_dir et new_file) refléteront l'intention de copier l'arborescence source d'origine. donc si vous avez *a/b/c.txt* sur l'arborescence des sources et que vous téléchargez dans le répertoire *mine* sur le système local, le new_dir serait *mine/a/b* et new_file serait *c.txt*.\n", + "\n", + "\n", + "## Plugins qui Traitent un Fichier après son Téléchargement\n", + "\n", + "Un cas d'utilisation courant est pour les plugins avec un point d'entrée *after_work* pour lire le fichier après son téléchargement et le transformer en un produit dérivé avec un nom différent. Ainsi, le nouveau fichier est créé comme dans la section précédente. Le message pour le fichier téléchargé doit encore être déplacé sur une liste pour s'assurer qu'il est reconnu par le courtier. Un tel point d'entrée ressemblerait à ceci:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "disciplinary-dublin", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + " def after_work(self, worklist):\n", + "\n", + " new_ok=[]\n", + " for m in worklist.ok:\n", + " success=do_something()\n", + " if success:\n", + " new_ok.append(m)\n", + " # since it is already acknowledged, we can just drop it from ok.\n", + " \n", + " \n", + " worklist.ok = new_ok\n", + " # the messages on worklist.ok will get posted in the next algorithm phase." + ] + }, + { + "cell_type": "markdown", + "id": "coastal-moses", + "metadata": {}, + "source": [ + "\n", + "## Plugins qui renomment les fichiers\n", + "\n", + "\n", + "Le plugin ci-dessus modifie la disposition des fichiers à télécharger, en fonction de la classe [GTStoWIS](https://github.com/wmo-im/GTStoWIS), qui prescrit une arborescence de répertoires différente en sortie. Il y a beaucoup de champs à mettre à jour lors de la modification du placement des fichiers, il est donc préférable d'utiliser:\n", + "\n", + " msg.updatePaths( self.o, new_dir, new_file )\n", + "\n", + "pour mettre à jour correctement tous les champs nécessaires dans le message. Il mettra à jour 'new_baseURL', 'new_relPath', 'new_subtopic' à utiliser lors de la publication.\n", + "\n", + "La partie try/except de la routine traite du cas où, si un fichier arrive avec un nom à partir duquel une arborescence de rubriques ne peut pas être construite, une exception python peut se produire et le message est ajouté à la liste de travail ayant échoué et ne sera pas être traitées par des plugins ultérieurs.\n", + "\n", + "## Plugins qui Créent de Nouveaux Fichiers\n", + "\n", + "\n", + "La routine ci-dessus est parfaite lorsqu'un fichier vient d'être renommé. Si un plugin a besoin de créer de nouveaux fichiers vaguement dérivés du fichier d'entrée, alors vous voulez créer de nouveaux messages pour ces fichiers à partir de rien:\n", + "\n", + " import sarracenia\n", + "\n", + " m = sarracenia.Message.fromFileData(sample_fileName, self.o, os.stat(sample_fileName) )\n", + "\n", + "La routine msg_fromFileData utilisera self.o pour appliquer les paramètres de publication appropriés. \n", + "Aucune connaissance des formats de message ou de la construction de champs n'est nécessaire. Si le fichier n'est pas local, \n", + "comme lors de l'écriture d'un rappel d'interrogation, un routage alternatif peut être utilisé: \n", + "\n", + " m = sarracenia.Message.fromFileInfo(sample_fileName, self.o, fake_stat_info )\n", + "\n", + "le faux enregistrement de statistiques (selon la page de manuel stat(2) ou python os.stat() ) peut être construit à partir d'autres champs, en commençant par:\n", + "\n", + " import paramiko\n", + "\n", + " fake_stat = paramiko.SFTPAttributes()\n", + " fake_stat.st_mtime = ... something else... perhaps an http header?\n", + " fake_stat.st_size = ... again will vary by context.\n", + " \n", + "Dans tous les cas, une fois que vous avez le message, il peut être ajouté à la liste entrante.\n" + ] + }, + { + "cell_type": "markdown", + "id": "inclusive-scope", + "metadata": {}, + "source": [ + "## Other Examples\n", + "\n", + "\n", + "Le sous-classement de [Sarracenia.flowcb](../Reference/flowcb.html) est utilisé en interne pour faire beaucoup de travail de base. C'est une bonne idée de regarder le code source de Sarracenia lui-même. Par exemple:\n", + "\n", + "* [sarracenia.flowcb](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/__init__.py) jetez un oeil au fichier __init__.py là qui fournit ces informations sur un format plus programmatiquement succinct.\n", + "\n", + "* [sarracenia.flowcb.gather.file](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/gather/file.py)\n", + " est une classe qui implémente:\n", + " la publication de fichiers et la surveillance de répertoires, dans le sens d'un rappel qui\n", + " implémente le point d'entrée *gather*, en lisant un système de fichiers et en construisant un\n", + " liste des messages à traiter.\n", + "\n", + "* [sarracenia.flowcb.gather.message](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/gather/message.py)\n", + "est une classe qui implémente la réception de messages à partir de flux de protocole de file d'attente de messages.\n", + "\n", + "* [sarracenia.flowcb.gather.nodupe](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/nodupe)\n", + "Ce module supprime les doublons du message\n", + " flux basés sur les sommes de contrôle d'intégrité.\n", + "\n", + "* [sarracenia.flowcb.post.message](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/post/message.py)\n", + "est une classe qui implémente la publication\n", + " messages vers flux de protocole de file d'attente de messages\n", + "\n", + "* [sarracenia.flowcb.retry](https://github.com/MetPX/Sarracenia/blob/development/sarracenia/flowcb/retry.py)\n", + "lorsque le transfert d'un fichier échoue. Sarracenia doit conserver le message pertinent dans un fichier d'état pour\n", + " un moment ultérieur où il pourra être réessayé.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "expensive-yellow", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fr/Tutoriel/3_api_flow_demo.html b/fr/Tutoriel/3_api_flow_demo.html new file mode 100644 index 000000000..c4e3991f6 --- /dev/null +++ b/fr/Tutoriel/3_api_flow_demo.html @@ -0,0 +1,596 @@ + + + + + + + Exemple d’API de flux — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Exemple d’API de flux

+

La classe sarracenia.flow fournit un filtrage d’acceptation/rejet intégré pour les messages, prend en charge le téléchargement intégré dans plusieurs protocoles, réessaye en cas d’échec et permet la création de rappels pour personnaliser le traitement.

+

Vous devez fournir une configuration comme argument lors de l’instanciation d’un abonné. La sarracenia.config.no_file_config() renvoie une configuration vide sans consulter l’arborescence des fichiers de configuration sr3.

+

Après avoir apporté les modifications nécessaires à la configuration, l’abonné est alors initié et exécuté.

+
+
[1]:
+
+
+
+!mkdir /tmp/flow_demo
+
+
+
+
+
+
+
+
+mkdir: cannot create directory ‘/tmp/flow_demo’: File exists
+
+
+

Créer un répertoire pour les fichiers que vous allez télécharger. La racine de l’arborescence de répertoires doit exister.

+
+
[2]:
+
+
+
+import re
+import sarracenia.config
+from sarracenia.flow.subscribe import Subscribe
+import sarracenia.flowcb
+import sarracenia.credentials
+
+cfg = sarracenia.config.no_file_config()
+
+cfg.broker = 'amqps://anonymous:anonymous@hpfx.collab.science.gc.ca'
+cfg.topicPrefix = [ 'v02', 'post']
+cfg.component = 'subscribe'
+cfg.config = 'flow_demo'
+cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]
+cfg.queueName='q_anonymous.subscriber_test2'
+cfg.download=True
+cfg.batch=1
+cfg.messageCountMax=5
+
+# set the instance number for the flow class.
+cfg.no=0
+
+cfg.finalize()
+
+# accept/reject patterns:
+pattern=".*"
+#   to_match, write_to_dir, DESTFN, regex_to_match, accept=True,mirror,strip, pstrip,flatten
+cfg.masks= [ ( pattern, "/tmp/flow_demo", None, re.compile(pattern), True, False, False, False, '/' ) ]
+
+
+
+
+
+
+
+

démareurs.

+

les paramètres du courtier, des liaisons et du nom de la file d’attente sont expliqués dans le bloc-notes de moth.

+
+
+

cfg.download

+

Si vous souhaitez que le flux télécharge les fichiers correspondant aux messages. Si vrai, il téléchargera les fichiers.

+
+
+

cfg.batch

+

Les messages sont traités par lots. Le nombre de messages à récupérer par appel à newMessages() est limité par le paramètre batch. Nous le définissons ici sur 1 afin que vous puissiez voir chaque fichier téléchargé immédiatement lorsque le message correspondant est téléchargé. vous pouvez laisser ce champ vide et la valeur par défaut est 25. Les paramètres sont une question de goût et de cas d’utilisation.

+
+
+

cfg.messageCountMax

+

Normalement, nous laissons ce paramètre à sa valeur par défaut (0) qui n’a aucun effet sur le traitement. à des fins de démonstration, nous limitons le nombre de messages que l’abonné traitera avec ce paramètre. après la réception de messageCountMax messages, arrêtez le traitement.

+
+
+

cfg.masks

+

Les masques sont une forme compilée de directives d’acceptation/rejet. un relPath est comparé à la regex dans le masque. Si l’expression régulière correspond et que accept est True, le message est accepté pour un traitement ultérieur. Si l’expression régulière correspond, mais accept vaut False, le traitement du message est arrêté (le message est rejeté.)

+

les masques sont un tuple. la signification peut être recherchée dans la page de manuel sr3(1).

+
    +
  • pattern_string, la chaîne d’expression régulière d’entrée, à compiler par les routines.

  • +
  • directory, où mettre les fichiers téléchargés (racine de l’arborescence, lors de la mise en miroir)

  • +
  • fn, transformation du filename à faire. NONE est utilisé 99% des cas d’utilisation.

  • +
  • regex, version regex compilée de pattern_string

  • +
  • accept(True/False), si le modèle correspond, acceptez le message pour un traitement ultérieur.

  • +
  • mirror(True/False), lors du téléchargement, créez une arborescence complète pour refléter la source, ou videz-la simplement dans le répertoire

  • +
  • strip(True/False), modifier le relpath en supprimant les entrées de la gauche.

  • +
  • pstrip(True/False), entrées de bande basées sur le modèle

  • +
  • flatten(char … ‘/’ signifie ne pas aplatir.) )

  • +
+
+
+

cfg.no, cfg.pid_filename

+

Ces paramètres sont nécessaires car ils seraient normalement définis par la classe sarracenia.instance qui est normalement utilisée pour lancer des flux. Ils permettent de configurer des chemins d’exécution pour retry_queues et des fichiers d’état, afin de mémoriser les paramètres si nécessaire entre les exécutions.

+
+
[3]:
+
+
+
+subscriber = sarracenia.flow.subscribe.Subscribe( cfg )
+
+subscriber.run()
+
+
+
+
+
+
+
+
+2023-05-28 16:52:19,861 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2023-05-28 16:52:19,940 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'AMQPLAIN', b'PLAIN'], locales: ['en_US']
+2023-05-28 16:52:19,984 [DEBUG] amqp __init__ using channel_id: 1
+2023-05-28 16:52:20,002 [DEBUG] amqp _on_open_ok Channel open
+2023-05-28 16:52:20,048 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous.subscriber_test2 (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-28 16:52:20,048 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous.subscriber_test2 with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-28 16:52:20,072 [DEBUG] sarracenia.moth.amqp __getSetup getSetup ... Done!
+2023-05-28 16:52:20,073 [DEBUG] sarracenia.flowcb.retry __init__ sr_retry __init__
+2023-05-28 16:52:20,074 [DEBUG] sarracenia.config add_option retry_driver declared as type:<class 'str'> value:disk
+2023-05-28 16:52:20,101 [DEBUG] sarracenia.diskqueue __init__  work_retry_00 __init__
+2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryMax declared as type:<class 'int'> value:0
+2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryBaseLineFile declared as type:<class 'int'> value:100
+2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryMultiplier declared as type:<class 'float'> value:3
+2023-05-28 16:52:20,104 [DEBUG] sarracenia.config add_option logEvents declared as type:<class 'set'> value:{'after_work', 'after_accept', 'on_housekeeping'}
+2023-05-28 16:52:20,104 [DEBUG] sarracenia.config add_option logMessageDump declared as type:<class 'bool'> value:False
+2023-05-28 16:52:20,105 [INFO] sarracenia.flowcb.log __init__ subscribe initialized with: {'after_work', 'after_accept', 'on_housekeeping'}
+2023-05-28 16:52:20,105 [DEBUG] sarracenia.config check_undeclared_options missing defaults: {'post_exchangeSuffix', 'exchangeSplit', 'identity', 'pollUrl', 'post_exchangeSplit', 'MemoryMax', 'notify_only', 'cluster', 'blocksize', 'exchange_suffix', 'report_exchange', 'realpathFilter', 'action', 'logMessageDump', 'retry_driver', 'source', 'nodupe_basis', 'MemoryMultiplier', 'reconnect', 'force_polling', 'header', 'inplace', 'post_exchange', 'save', 'post_on_start', 'follow_symlinks', 'count', 'MemoryBaseLineFile', 'feeder', 'sendTo', 'restore'}
+2023-05-28 16:52:20,105 [INFO] sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']
+2023-05-28 16:52:20,105 [INFO] sarracenia.flow run pid: 1921103 subscribe/flow_demo instance: 0
+2023-05-28 16:52:20,128 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202430', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_ef8614a54e610cd50588f448a9632244:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202430', 'source': 'WXO-DD', 'mtime': '20230528T202432.81', 'atime': '20230528T202432.81', 'pubTime': '20230528T202432.81', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CWRZ/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CWRZ'], 'identity': {'method': 'md5', 'value': '30K1LtKs+91neD6625tbcg=='}, 'size': 7665, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}
+2023-05-28 16:52:20,129 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1667.32 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CWRZ/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml
+2023-05-28 16:52:20,129 [INFO] sarracenia.flow run now active on vip None
+2023-05-28 16:52:20,130 [DEBUG] sarracenia.config add_option accelWgetCommand declared as type:<class 'str'> value:/usr/bin/wget %s -o - -O %d
+2023-05-28 16:52:20,213 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml
+2023-05-28 16:52:20,233 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_b82b2479d8c115cf0eb4c82bfcc59981:DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'source': 'WXO-DD', 'mtime': '20230528T202437.541', 'atime': '20230528T202437.541', 'pubTime': '20230528T202437.541', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ban/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', 'partners', 'on-firewx', '20230528', 'ban'], 'identity': {'method': 'md5', 'value': 'QGDX+gsirC8l8hnDfEHa1w=='}, 'size': 5203, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}
+2023-05-28 16:52:20,233 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1662.69 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ban/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml
+2023-05-28 16:52:20,311 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml
+2023-05-28 16:52:20,336 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_b96cfe8f3a477e2c4a39af99a97b6429:DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'source': 'WXO-DD', 'mtime': '20230528T202437.541', 'atime': '20230528T202437.541', 'pubTime': '20230528T202437.541', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ple/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', 'partners', 'on-firewx', '20230528', 'ple'], 'identity': {'method': 'md5', 'value': 'eL80Iw/3MaCqipWJOT7LeQ=='}, 'size': 5091, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}
+2023-05-28 16:52:20,336 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1662.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ple/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml
+2023-05-28 16:52:20,433 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml
+2023-05-28 16:52:20,456 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_0e2c0d07f3648f9956db0a2b1523e6d7:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'source': 'WXO-DD', 'mtime': '20230528T202443.46', 'atime': '20230528T202443.46', 'pubTime': '20230528T202443.46', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CXHI/2023-05-28-2024-CXHI-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CXHI'], 'identity': {'method': 'md5', 'value': 'bGYYmVHKuo3JSRDOjiC7NA=='}, 'size': 9353, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}
+2023-05-28 16:52:20,457 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1657.00 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CXHI/2023-05-28-2024-CXHI-AUTO-minute-swob.xml
+2023-05-28 16:52:20,534 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2024-CXHI-AUTO-minute-swob.xml
+2023-05-28 16:52:20,558 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_1198e5a492e9a42cd6aadbbe92bcb788:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'source': 'WXO-DD', 'mtime': '20230528T202443.47', 'atime': '20230528T202443.47', 'pubTime': '20230528T202443.47', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CWBM/2023-05-28-2024-CWBM-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CWBM'], 'identity': {'method': 'md5', 'value': 'sIqUJmWCsX5BVfplkZA75Q=='}, 'size': 9354, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}
+2023-05-28 16:52:20,559 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1657.09 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CWBM/2023-05-28-2024-CWBM-AUTO-minute-swob.xml
+2023-05-28 16:52:20,652 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2024-CWBM-AUTO-minute-swob.xml
+2023-05-28 16:52:20,653 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2023-05-28 16:52:20,653 [INFO] sarracenia.flow run starting last pass (without gather) through loop for cleanup.
+2023-05-28 16:52:20,654 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.
+2023-05-28 16:52:20,654 [INFO] sarracenia.flow run on_housekeeping pid: 1921103 subscribe/flow_demo instance: 0
+2023-05-28 16:52:20,655 [INFO] sarracenia.flowcb.gather.message on_housekeeping messages: good: 5 bad: 0 bytes: 783 Bytes average: 156 Bytes
+2023-05-28 16:52:20,655 [INFO] sarracenia.flowcb.retry on_housekeeping on_housekeeping
+2023-05-28 16:52:20,655 [INFO] sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping
+2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping No retry in list
+2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000612
+2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping
+2023-05-28 16:52:20,657 [INFO] sarracenia.diskqueue on_housekeeping No retry in list
+2023-05-28 16:52:20,657 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000450
+2023-05-28 16:52:20,657 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.47 system=0.07
+2023-05-28 16:52:20,657 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 759.0 MiB, accumulating count (5 or 5/100 so far) before self-setting threshold
+2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats version: 3.00.40, started: now, last_housekeeping:  0.6 seconds ago
+2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats messages received: 5, accepted: 5, rejected: 0   rate accepted: 100.0% or 9.0 m/s
+2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats files transferred: 5 bytes: 35.8 KiB rate: 64.8 KiB/sec
+2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats lag: average: 1661.38, maximum: 1667.32
+2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log on_housekeeping housekeeping
+2023-05-28 16:52:20,659 [INFO] sarracenia.flow run clean stop from run loop
+2023-05-28 16:52:20,679 [DEBUG] amqp collect Closed channel #1
+2023-05-28 16:52:20,680 [INFO] sarracenia.flowcb.gather.message on_stop closing
+2023-05-28 16:52:20,680 [INFO] sarracenia.flow close flow/close completed cleanly pid: 1921103 subscribe/flow_demo instance: 0
+
+
+
+
+

Conclusion:

+

Avec la classe sarracenia.flow, une méthode de fonctionnement asynchrone est prise en charge, elle peut être personnalisée à l’aide de la classe flowcb (rappel de flux) pour introduire un traitement spécifique à des moments spécifiques. C’est comme l’invocation d’une seule instance à partir de la ligne de commande, sauf que toute la configuration est effectuée dans python en définissant des champs cfg, plutôt qu’en utilisant le langage de configuration.

+

Qu’est-ce qui est perdu par rapport à l’utilisation de l’outil de ligne de commande :

+
    +
  • possibilité d’utiliser le langage de configuration (légèrement plus simple que d’attribuer des valeurs à l’objet cfg)

  • +
  • exécution facile de plusieurs instances,

  • +
  • surveillance coordonnée des instances (redémarrages en cas d’échec, et nombre programmable d’abonnés démarrés par configuration.)

  • +
  • gestion des fichiers journaux.

  • +
+

L’outil de ligne de commande fournit ces fonctionnalités supplémentaires.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/3_api_flow_demo.ipynb b/fr/Tutoriel/3_api_flow_demo.ipynb new file mode 100644 index 000000000..d61fede25 --- /dev/null +++ b/fr/Tutoriel/3_api_flow_demo.ipynb @@ -0,0 +1,260 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "informative-conservation", + "metadata": {}, + "source": [ + "# Exemple d'API de flux\n", + "\n", + "La [classe sarracenia.flow](../Reference/code.html#module-sarracenia.flow) fournit un filtrage d'acceptation/rejet intégré pour les messages, prend en charge le téléchargement intégré dans plusieurs protocoles, réessaye en cas d'échec et permet la création de rappels pour personnaliser le traitement.\n", + "\n", + "Vous devez fournir une configuration comme argument lors de l'instanciation d'un abonné. \n", + "La _sarracenia.config.no_file_config()_ renvoie une configuration vide sans consulter \n", + "l'arborescence des fichiers de configuration sr3.\n", + "\n", + "Après avoir apporté les modifications nécessaires à la configuration, l'abonné est alors initié et exécuté." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "weekly-terminology", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "mkdir: cannot create directory ‘/tmp/flow_demo’: File exists\r\n" + ] + } + ], + "source": [ + "!mkdir /tmp/flow_demo" + ] + }, + { + "cell_type": "markdown", + "id": "exterior-folks", + "metadata": {}, + "source": [ + "Créer un répertoire pour les fichiers que vous allez télécharger. La racine de l'arborescence de répertoires doit exister." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "aggregate-election", + "metadata": {}, + "outputs": [], + "source": [ + "import re\n", + "import sarracenia.config\n", + "from sarracenia.flow.subscribe import Subscribe\n", + "import sarracenia.flowcb\n", + "import sarracenia.credentials\n", + "\n", + "cfg = sarracenia.config.no_file_config()\n", + "\n", + "cfg.broker = 'amqps://anonymous:anonymous@hpfx.collab.science.gc.ca'\n", + "cfg.topicPrefix = [ 'v02', 'post']\n", + "cfg.component = 'subscribe'\n", + "cfg.config = 'flow_demo'\n", + "cfg.bindings = [ ('xpublic', ['v02', 'post'], ['*', 'WXO-DD', 'observations', 'swob-ml', '#' ]) ]\n", + "cfg.queueName='q_anonymous.subscriber_test2'\n", + "cfg.download=True\n", + "cfg.batch=1\n", + "cfg.messageCountMax=5\n", + "\n", + "# set the instance number for the flow class.\n", + "cfg.no=0\n", + "\n", + "cfg.finalize()\n", + "\n", + "# accept/reject patterns:\n", + "pattern=\".*\"\n", + "# to_match, write_to_dir, DESTFN, regex_to_match, accept=True,mirror,strip, pstrip,flatten\n", + "cfg.masks= [ ( pattern, \"/tmp/flow_demo\", None, re.compile(pattern), True, False, False, False, '/' ) ]\n", + "\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "legitimate-necessity", + "metadata": {}, + "source": [ + "\n", + "## démareurs.\n", + "les paramètres du courtier, des liaisons et du nom de la file d'attente sont expliqués dans le bloc-notes de moth.\n", + "\n", + "## cfg.download\n", + "\n", + "Si vous souhaitez que le flux télécharge les fichiers correspondant aux messages. \n", + "Si vrai, il téléchargera les fichiers.\n", + "\n", + "## cfg.batch\n", + "\n", + "Les messages sont traités par lots. Le nombre de messages à récupérer par appel à newMessages() \n", + "est limité par le paramètre _batch_. Nous le définissons ici sur 1 afin que vous puissiez voir chaque fichier téléchargé immédiatement lorsque le message correspondant est téléchargé. vous pouvez laisser ce champ vide et la valeur par défaut est 25. Les paramètres sont une question de goût et de cas d'utilisation.\n", + "\n", + "## cfg.messageCountMax\n", + "\n", + "Normalement, nous laissons ce paramètre à sa valeur par défaut (0) qui n'a aucun effet sur le traitement. \n", + "à des fins de démonstration, nous limitons le nombre de messages que l'abonné traitera avec ce paramètre. \n", + "après la réception de _messageCountMax_ messages, arrêtez le traitement.\n", + "\n", + "## cfg.masks\n", + "\n", + "Les masques sont une forme compilée de directives d'acceptation/rejet. un relPath est comparé à la regex dans le masque. \n", + "Si l'expression régulière correspond et que accept est True, le message est accepté pour un traitement ultérieur. \n", + "Si l'expression régulière correspond, mais accept vaut False, le traitement du message est arrêté (le message est rejeté.)\n", + "\n", + "les masques sont un tuple. la signification peut être recherchée dans la page de manuel sr3(1).\n", + "\n", + "* pattern_string, la chaîne d'expression régulière d'entrée, à compiler par les routines.\n", + "* directory, où mettre les fichiers téléchargés (racine de l'arborescence, lors de la mise en miroir)\n", + "* fn, transformation du filename à faire. NONE est utilisé 99% des cas d'utilisation.\n", + "* regex, version regex compilée de pattern_string\n", + "* accept(True/False), si le modèle correspond, acceptez le message pour un traitement ultérieur.\n", + "* mirror(True/False), lors du téléchargement, créez une arborescence complète pour refléter la source, ou videz-la simplement dans le répertoire\n", + "* strip(True/False), modifier le relpath en supprimant les entrées de la gauche.\n", + "* pstrip(True/False), entrées de bande basées sur le modèle\n", + "* flatten(char ... '/' signifie ne pas aplatir.) )\n", + "\n", + "## cfg.no, cfg.pid_filename\n", + "\n", + "Ces paramètres sont nécessaires car ils seraient normalement définis par la classe sarracenia.instance \n", + "qui est normalement utilisée pour lancer des flux. Ils permettent de configurer des chemins d'exécution pour retry_queues et des fichiers d'état, \n", + "afin de mémoriser les paramètres si nécessaire entre les exécutions.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "musical-discrimination", + "metadata": { + "scrolled": false + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-28 16:52:19,861 [INFO] sarracenia.flow loadCallbacks flowCallback plugins to load: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2023-05-28 16:52:19,940 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@hpfx2.collab.science.gc.ca', 'copyright': 'Copyright (c) 2007-2022 VMware, Inc. or its affiliates.', 'information': 'Licensed under the MPL 2.0. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 24.2.1', 'product': 'RabbitMQ', 'version': '3.9.13'}, mechanisms: [b'AMQPLAIN', b'PLAIN'], locales: ['en_US']\n", + "2023-05-28 16:52:19,984 [DEBUG] amqp __init__ using channel_id: 1\n", + "2023-05-28 16:52:20,002 [DEBUG] amqp _on_open_ok Channel open\n", + "2023-05-28 16:52:20,048 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous.subscriber_test2 (as: amqps://anonymous@hpfx.collab.science.gc.ca) \n", + "2023-05-28 16:52:20,048 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous.subscriber_test2 with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)\n", + "2023-05-28 16:52:20,072 [DEBUG] sarracenia.moth.amqp __getSetup getSetup ... Done!\n", + "2023-05-28 16:52:20,073 [DEBUG] sarracenia.flowcb.retry __init__ sr_retry __init__\n", + "2023-05-28 16:52:20,074 [DEBUG] sarracenia.config add_option retry_driver declared as type: value:disk\n", + "2023-05-28 16:52:20,101 [DEBUG] sarracenia.diskqueue __init__ work_retry_00 __init__\n", + "2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryMax declared as type: value:0\n", + "2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryBaseLineFile declared as type: value:100\n", + "2023-05-28 16:52:20,103 [DEBUG] sarracenia.config add_option MemoryMultiplier declared as type: value:3\n", + "2023-05-28 16:52:20,104 [DEBUG] sarracenia.config add_option logEvents declared as type: value:{'after_work', 'after_accept', 'on_housekeeping'}\n", + "2023-05-28 16:52:20,104 [DEBUG] sarracenia.config add_option logMessageDump declared as type: value:False\n", + "2023-05-28 16:52:20,105 [INFO] sarracenia.flowcb.log __init__ subscribe initialized with: {'after_work', 'after_accept', 'on_housekeeping'}\n", + "2023-05-28 16:52:20,105 [DEBUG] sarracenia.config check_undeclared_options missing defaults: {'post_exchangeSuffix', 'exchangeSplit', 'identity', 'pollUrl', 'post_exchangeSplit', 'MemoryMax', 'notify_only', 'cluster', 'blocksize', 'exchange_suffix', 'report_exchange', 'realpathFilter', 'action', 'logMessageDump', 'retry_driver', 'source', 'nodupe_basis', 'MemoryMultiplier', 'reconnect', 'force_polling', 'header', 'inplace', 'post_exchange', 'save', 'post_on_start', 'follow_symlinks', 'count', 'MemoryBaseLineFile', 'feeder', 'sendTo', 'restore'}\n", + "2023-05-28 16:52:20,105 [INFO] sarracenia.flow run callbacks loaded: ['sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'log']\n", + "2023-05-28 16:52:20,105 [INFO] sarracenia.flow run pid: 1921103 subscribe/flow_demo instance: 0\n", + "2023-05-28 16:52:20,128 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202430', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_ef8614a54e610cd50588f448a9632244:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202430', 'source': 'WXO-DD', 'mtime': '20230528T202432.81', 'atime': '20230528T202432.81', 'pubTime': '20230528T202432.81', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CWRZ/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CWRZ'], 'identity': {'method': 'md5', 'value': '30K1LtKs+91neD6625tbcg=='}, 'size': 7665, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}\n", + "2023-05-28 16:52:20,129 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1667.32 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CWRZ/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,129 [INFO] sarracenia.flow run now active on vip None\n", + "2023-05-28 16:52:20,130 [DEBUG] sarracenia.config add_option accelWgetCommand declared as type: value:/usr/bin/wget %s -o - -O %d\n", + "2023-05-28 16:52:20,213 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-CWRZ-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,233 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_b82b2479d8c115cf0eb4c82bfcc59981:DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'source': 'WXO-DD', 'mtime': '20230528T202437.541', 'atime': '20230528T202437.541', 'pubTime': '20230528T202437.541', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ban/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', 'partners', 'on-firewx', '20230528', 'ban'], 'identity': {'method': 'md5', 'value': 'QGDX+gsirC8l8hnDfEHa1w=='}, 'size': 5203, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}\n", + "2023-05-28 16:52:20,233 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1662.69 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ban/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml \n", + "2023-05-28 16:52:20,311 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-on-mnrf-affes-ban-ban-AUTO-swob.xml \n", + "2023-05-28 16:52:20,336 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_b96cfe8f3a477e2c4a39af99a97b6429:DMS:CMC:SWOB_FORESTRY:XML::20230528202436', 'source': 'WXO-DD', 'mtime': '20230528T202437.541', 'atime': '20230528T202437.541', 'pubTime': '20230528T202437.541', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ple/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', 'partners', 'on-firewx', '20230528', 'ple'], 'identity': {'method': 'md5', 'value': 'eL80Iw/3MaCqipWJOT7LeQ=='}, 'size': 5091, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}\n", + "2023-05-28 16:52:20,336 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1662.80 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/partners/on-firewx/20230528/ple/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml \n", + "2023-05-28 16:52:20,433 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2023-on-mnrf-affes-ple-ple-AUTO-swob.xml \n", + "2023-05-28 16:52:20,456 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_0e2c0d07f3648f9956db0a2b1523e6d7:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'source': 'WXO-DD', 'mtime': '20230528T202443.46', 'atime': '20230528T202443.46', 'pubTime': '20230528T202443.46', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CXHI/2023-05-28-2024-CXHI-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CXHI'], 'identity': {'method': 'md5', 'value': 'bGYYmVHKuo3JSRDOjiC7NA=='}, 'size': 9353, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-28 16:52:20,457 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1657.00 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CXHI/2023-05-28-2024-CXHI-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,534 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2024-CXHI-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,558 [DEBUG] sarracenia.moth.amqp getNewMessage new msg: {'_format': 'v02', '_deleteOnPost': {'_format', 'exchange', 'ack_id', 'local_offset', 'subtopic'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_1198e5a492e9a42cd6aadbbe92bcb788:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528202442', 'source': 'WXO-DD', 'mtime': '20230528T202443.47', 'atime': '20230528T202443.47', 'pubTime': '20230528T202443.47', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CWBM/2023-05-28-2024-CWBM-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CWBM'], 'identity': {'method': 'md5', 'value': 'sIqUJmWCsX5BVfplkZA75Q=='}, 'size': 9354, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}\n", + "2023-05-28 16:52:20,559 [INFO] sarracenia.flowcb.log after_accept accepted: (lag: 1657.09 ) https://hpfx.collab.science.gc.ca /20230528/WXO-DD/observations/swob-ml/20230528/CWBM/2023-05-28-2024-CWBM-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,652 [INFO] sarracenia.flowcb.log after_work downloaded ok: /tmp/flow_demo/2023-05-28-2024-CWBM-AUTO-minute-swob.xml \n", + "2023-05-28 16:52:20,653 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2023-05-28 16:52:20,653 [INFO] sarracenia.flow run starting last pass (without gather) through loop for cleanup.\n", + "2023-05-28 16:52:20,654 [INFO] sarracenia.flow please_stop ok, telling 4 callbacks about it.\n", + "2023-05-28 16:52:20,654 [INFO] sarracenia.flow run on_housekeeping pid: 1921103 subscribe/flow_demo instance: 0\n", + "2023-05-28 16:52:20,655 [INFO] sarracenia.flowcb.gather.message on_housekeeping messages: good: 5 bad: 0 bytes: 783 Bytes average: 156 Bytes\n", + "2023-05-28 16:52:20,655 [INFO] sarracenia.flowcb.retry on_housekeeping on_housekeeping\n", + "2023-05-28 16:52:20,655 [INFO] sarracenia.diskqueue on_housekeeping work_retry_00 on_housekeeping\n", + "2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping No retry in list\n", + "2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000612\n", + "2023-05-28 16:52:20,656 [INFO] sarracenia.diskqueue on_housekeeping post_retry_000 on_housekeeping\n", + "2023-05-28 16:52:20,657 [INFO] sarracenia.diskqueue on_housekeeping No retry in list\n", + "2023-05-28 16:52:20,657 [INFO] sarracenia.diskqueue on_housekeeping on_housekeeping elapse 0.000450\n", + "2023-05-28 16:52:20,657 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current Memory cpu_times: user=0.47 system=0.07\n", + "2023-05-28 16:52:20,657 [INFO] sarracenia.flowcb.housekeeping.resources on_housekeeping Current mem usage: 759.0 MiB, accumulating count (5 or 5/100 so far) before self-setting threshold\n", + "2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats version: 3.00.40, started: now, last_housekeeping: 0.6 seconds ago \n", + "2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats messages received: 5, accepted: 5, rejected: 0 rate accepted: 100.0% or 9.0 m/s\n", + "2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats files transferred: 5 bytes: 35.8 KiB rate: 64.8 KiB/sec\n", + "2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log stats lag: average: 1661.38, maximum: 1667.32 \n", + "2023-05-28 16:52:20,658 [INFO] sarracenia.flowcb.log on_housekeeping housekeeping\n", + "2023-05-28 16:52:20,659 [INFO] sarracenia.flow run clean stop from run loop\n", + "2023-05-28 16:52:20,679 [DEBUG] amqp collect Closed channel #1\n", + "2023-05-28 16:52:20,680 [INFO] sarracenia.flowcb.gather.message on_stop closing\n", + "2023-05-28 16:52:20,680 [INFO] sarracenia.flow close flow/close completed cleanly pid: 1921103 subscribe/flow_demo instance: 0\n" + ] + } + ], + "source": [ + "subscriber = sarracenia.flow.subscribe.Subscribe( cfg )\n", + "\n", + "subscriber.run()" + ] + }, + { + "cell_type": "markdown", + "id": "passive-biotechnology", + "metadata": {}, + "source": [ + "## Conclusion:\n", + "\n", + "Avec la classe sarracenia.flow, une méthode de fonctionnement asynchrone est prise en charge, elle peut être personnalisée à l'aide de la classe flowcb (rappel de flux) pour introduire un traitement spécifique à des moments spécifiques. C'est comme l'invocation d'une seule instance à partir de la ligne de commande, sauf que toute la configuration est effectuée dans python en définissant des champs cfg, plutôt qu'en utilisant le langage de configuration.\n", + "\n", + "Qu'est-ce qui est perdu par rapport à l'utilisation de l'outil de ligne de commande :\n", + "\n", + "* possibilité d'utiliser le langage de configuration (légèrement plus simple que d'attribuer des valeurs à l'objet cfg) \n", + "* exécution facile de plusieurs instances, \n", + "* surveillance coordonnée des instances (redémarrages en cas d'échec, et nombre programmable d'abonnés démarrés par configuration.) \n", + "* gestion des fichiers journaux.\n", + "\n", + "L'outil de ligne de commande fournit ces fonctionnalités supplémentaires." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fr/Tutoriel/4_api_moth_sub_demo.html b/fr/Tutoriel/4_api_moth_sub_demo.html new file mode 100644 index 000000000..37720fb71 --- /dev/null +++ b/fr/Tutoriel/4_api_moth_sub_demo.html @@ -0,0 +1,720 @@ + + + + + + + Un premier exemple utilisant l’API Sarracenia Moth — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Un premier exemple utilisant l’API Sarracenia Moth

+

Sarracenia est un package conçu pour annoncer la disponibilité de nouvelles données, généralement sous forme de fichiers. Nous plaçons les fichiers sur des serveurs standard, les rendons disponibles via le Web ou sftp, et informons les utilisateurs qu’ils sont arrivés à l’aide de messages.

+

Sarracenia utilise des protocoles de transmission de messages standard existants, comme rabbitmq/AMQP pour transporter les messages, et dans les cercles de transmission de messages, car le serveur qui distribue les messages est appelé un courtier (broker).

+

Nous appelons la combinaison d’un courtier de messages et d’un serveur de fichiers (qui peut être un serveur unique ou un grand cluster) une pompe de données (data pump).

+

En supposant que vous avez installé le paquet metpx-sr3, soit en tant que paquet debian, ou via pip, les annonces d’accès à sens unique à utiliser avec la classe sarracenia.moth (Messages Organisés par les en-têtes de sujet), qui permet à un programme python de se connecter à un serveur Sarracenia, et commencer à recevoir des messages qui annoncent des ressources.

+

La fabrique pour construire les objets sarracenia.moth prend deux arguments :

+
    +
  • courtier : un objet (Credential) contenant une url pointant vers le serveur de messagerie qui annonce des produits, et d’autres options associées.

  • +
  • options : un dictionnaire d’autres paramètres que la classe pourrait utiliser.

  • +
+

L’exemple ci-dessous construit un appel à un courtier auquel tout le monde peut accéder et demander 10 annonces.

+

Vous pouvez l’exécuter, puis nous pourrons discuter de quelques paramètres :

+
+
[9]:
+
+
+
+import sarracenia.moth
+import sarracenia.moth.amqp
+import sarracenia.credentials
+
+import time
+import socket
+
+
+options = sarracenia.moth.default_options
+options.update(sarracenia.moth.amqp.default_options)
+options['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')
+options['topicPrefix'] = [ 'v02', 'post' ]
+options['bindings'] = [('xpublic', ['v02', 'post'], ['#'])]
+options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_QuelquechoseDUtile'
+
+print('options: %s' % options)
+
+
+
+
+
+
+
+
+
+
+options: {'acceptUnmatched': True, 'batch': 25, 'bindings': [('xpublic', ['v02', 'post'], ['#'])], 'broker': <sarracenia.credentials.Credential object at 0x7f602ee7d780>, 'dry_run': False, 'exchange': None, 'expire': None, 'inline': False, 'inlineEncoding': 'guess', 'inlineByteMax': 4096, 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', 'logLevel': 'info', 'messageDebugDump': False, 'message_strategy': {'reset': True, 'stubborn': True, 'failure_duration': '5m'}, 'message_ttl': 0, 'topicPrefix': ['v02', 'post'], 'tlsRigour': 'normal', 'auto_delete': False, 'durable': True, 'exchangeDeclare': True, 'prefetch': 25, 'queueName': 'q_anonymous_fractal_QuelquechoseDUtile', 'queueBind': True, 'queueDeclare': True, 'reset': False, 'subtopic': [], 'vhost': '/', 'queue_name': 'q_anonymous_fractal_QuelquechoseDUtile'}
+
+
+

Le paramètre courtier(broker) est un objet contenant une URL conventionnelle et d’autres options, indiquant le protocole de messagerie à utiliser pour accéder au serveur en amont. Lorsque vous vous connectez à un courtier, vous devez lui indiquer les messages qui vous intéressent. Dans Moth, tous les courtiers auxquels nous accédons doivent utiliser des hiérarchies de sujets. Vous pouvez les voir si vous avez exécuté avec succès l’exemple ci-dessus, il devrait y avoir dans les impressions +de message un élément “sujet”(topic) dans les dictionnaires. En voici un exemple :

+

v02.post.20210213.WXO-DD.observations.swob-ml.20210213.CTZR

+

Celle-ci se divise en deux parties :

+
    +
  • topic_prefix: v02.post

  • +
  • le reste de l’arborescence des rubriques est le reflet du chemin vers le produit annoncé, par rapport à un répertoire de base.

  • +
+

Dans AMQP, il y a le concept des “échanges” qui sont en quelque sorte comparables aux chaînes de télévision… ce sont des regroupements d’annonces. donc pour se connecter à un courtier AMQP, il faut spécifier:

+
    +
  • exchange: Sarracenia promulgue xpublic comme défaut conventionnel.

  • +
  • topic_prefix: décidez quelle version des messages vous souhaitez obtenir. Ce serveur produit des v02.

  • +
  • subtopic: à quel sous-ensemble de messages topic_prefix voulons-nous nous abonner.

  • +
+
+

Liaisons

+

L’option de liaisons définit les trois valeurs ci-dessus. dans l’exemple, les liaisons sont :

+
    +
  • topic_prefix: v02.post (obtenir des messages v02.)

  • +
  • exchange: xpublic (celui par défaut.)

  • +
  • subtopic: # ( un joker AMQP signifiant tout. )

  • +
+

on se connecte au courtier

+

amqp://hpfx.collab.science.gc.ca, sur l’échange xpublic, et nous serons intéressés par tous les messages correspondant à la spécification de sujet v02.post.#… (c’est-à-dire tous les messages v02 disponibles .)

+
+

sous-thème

+

Le sous-thème ici ( # ) correspond à tout ce qui est produit sur le serveur. Plus le sous-thème est large, plus il y a de messages à envoyer et plus le traitement est important. Il est préférable de le rendre plus étroit. En prenant l’exemple ci-dessus, si nous sommes intéressés par swob, un sous-thème comme:

+
    +
  • *.WXO-DD.observations.swob-ml.#

  • +
+

correspondrait à tous les swobs similaires à celui ci-dessus, mais évitez de vous envoyer des messages pour des non-swobs.

+
+
+
+

queue_name

+

Par convention, dans les courtiers administrés par Sarracenia, les utilisateurs ne peuvent créer que des files d’attente commençant par q_ suivi de leur nom d’utilisateur. nous nous sommes connectés en tant qu’anonymes, et donc q_anonymous doit être utilisé. Après cela, le reste peut être ce que vous voulez, mais il y a quelques considérations :

+
    +
  • Si vous souhaitez démarrer plusieurs processus Python pour partager un flux de données, ils spécifient tous le même nom de file d’attente et ils partageront le flux de messages. Il s’adapte bien à quelques dizaines de téléchargeurs coopérants, mais ne s’adapte pas à l’infini, ne vous attendez pas à plus d’environ 99 processus pour pouvoir partager efficacement une charge à partir d’une seule file d’attente. Pour évoluer au-delà de cela avec AMQP, plusieurs sélections sont préférables.

  • +
  • si vous allez demander de l’aide aux administrateurs de la pompe de données … vous devrez leur fournir le nom de la file d’attente, et ils devront peut-être pouvoir le choisir parmi des centaines ou des milliers qui se trouvent sur le serveur.

  • +
+
+
+

Messages

+

Différents protocoles de messagerie ont différentes structures et conventions de stockage. la classe MoTH renvoie les messages sous forme de dictionnaires python, quel que soit le protocole utilisé pour les obtenir ou, en cas de transfert, pour les transmettre. On peut ajouter des champs pour une utilisation programmatique aux messages simplement en ajoutant des éléments au dictionnaire. S’ils sont uniquement destinés à un usage interne, ajoutez le nom de l’élément du dictionnaire à la clé +spéciale ‘_deleteOnPost’, afin que l’élément du dictionnaire soit supprimé lors du transfert du message.

+
+
+

Ack

+

Les messages sont marqués en transit par le courtier, et si vous ne les reconnaissez pas, la pompe de données les conservera et les réexpédiera éventuellement. conserver les messages en attente en mémoire ralentira également le traitement de tous les messages. Il faut accuser réception des messages dès que possible, mais pas si tôt que vous perdrez des données si le programme est interrompu. Dans l’exemple, nous reconnaissons après avoir fait notre travail d’impression du message.

+
+
[10]:
+
+
+
+h = sarracenia.moth.Moth.subFactory(options)
+
+count=0
+bon=0 # compteur des messages bien reçus
+
+while count < 5:
+    m = h.getNewMessage()  #get only one Message
+    if m is not None:
+        print("message %d: %s" % (count,m) )
+        content = m.getContent()
+        print("le premier 50 octets du fichier annoncé: %s" % content[0:50])
+        bon += 1
+        h.ack(m)
+    time.sleep(0.1)
+    count += 1
+
+h.cleanup() # remove server-side queue defined by Factory.
+h.close()
+print( f"{bon} messages bien reçus")
+
+
+
+
+
+
+
+
+
+2023-05-28 15:01:16,250 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_QuelquechoseDUtile (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-28 15:01:16,251 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_QuelquechoseDUtile with v02.post.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+
+
+
+
+
+
+
+message 0: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_5eebe93b78f7f20d6c58dff7079f17f8:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190113.733', 'atime': '20230528T190113.733', 'pubTime': '20230528T190113.733', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_HGT_ISBL_1000_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'DcEZ6+fx637myOUf83VyDQ=='}, 'size': 236654, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}
+le premier 50 octets du fichier annoncé: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x03\x9cn\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1c\x0c\x00\x00\x01\x02\x00\x00\x00A\x03\x00\x00\x12q1\x00\x00\x00'
+message 1: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_abed0a37b3c8b8511cc78b6f8c5c6a82:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190114.13', 'atime': '20230528T190114.13', 'pubTime': '20230528T190114.13', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_SPFH_ISBL_0150_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'oMQDWV/QlF9aLLGOu+Tumw=='}, 'size': 330883, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}
+le premier 50 octets du fichier annoncé: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x05\x0c\x83\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1c\x0c\x00\x00\x01\x02\x00\x00\x00A\x03\x00\x00\x12q1\x00\x00\x00'
+message 2: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_7af6caa9fd11fae11919525225783fad:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190113.823', 'atime': '20230528T190113.823', 'pubTime': '20230528T190113.823', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_DEPR_ISBL_0850_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'zSw+zw6P1XlQayy+CjoLAg=='}, 'size': 194315, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}
+le premier 50 octets du fichier annoncé: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x02\xf7\x0b\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1c\x0c\x00\x00\x01\x02\x00\x00\x00A\x03\x00\x00\x12q1\x00\x00\x00'
+message 3: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190112', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_e02209b0564eb4b19dd746af9eb5ee9c:CMC:HRDPS:GRIB2:BIN::20230528190112', 'source': 'WXO-DD', 'mtime': '20230528T190114.89', 'atime': '20230528T190114.89', 'pubTime': '20230528T190114.89', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_WDIR_TGL_40_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'GpPL5qQEOn0ALfuOzQrIHw=='}, 'size': 529466, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}
+le premier 50 octets du fichier annoncé: b'GRIB\x00\x00\x00\x02\x00\x00\x00\x00\x00\x08\x14:\x00\x00\x00\x15\x01\x006\x00\x00\x04\x00\x01\x07\xe7\x05\x1c\x0c\x00\x00\x01\x02\x00\x00\x00A\x03\x00\x00\x12q1\x00\x00\x00'
+
+
+
+
+
+
+
+2023-05-28 15:01:17,548 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_QuelquechoseDUtile
+
+
+
+
+
+
+
+message 4: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528190109', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_0926936c6c7b2968e12487b5e10b3bc9:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528190109', 'source': 'WXO-DD', 'mtime': '20230528T190111.364', 'atime': '20230528T190111.364', 'pubTime': '20230528T190111.364', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CVKU/2023-05-28-1900-CVKU-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CVKU'], 'identity': {'method': 'md5', 'value': 'WEEsvB9/BKQC1Pv9hgO3LA=='}, 'size': 6426, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}
+le premier 50 octets du fichier annoncé: b'<?xml version="1.0" encoding="UTF-8" standalone="n'
+5 messages bien reçus
+
+
+

2ème exemple … combinez baseURL + relPath (en parlant de retPath) et récupérez les données … utilisez newMessages() au lieu de getNewMessage pour afficher une autre interface utilisateur de consommation. Parler de http, et comment la récupération variera en fonction du protocole répertorié dans la baseUrl, et peut être compliqué.

+
+
[12]:
+
+
+
+import urllib.request
+import xml.etree.ElementTree as ET
+
+
+options['bindings'] = [('xpublic', [ 'v02', 'post'], \
+        [ '*', 'WXO-DD', 'observations', 'swob-ml', '#'] )]
+
+h = sarracenia.moth.Moth.subFactory(options)
+
+count=0
+
+while count < 10:
+    messages = h.newMessages()  #get all received Messages, upto options['batch'] of them at a time.
+    for m in messages:
+        dataUrl = m['baseUrl']
+        if 'retPath' in m:
+           dataUrl += m['retPath']
+        else:
+           dataUrl += m['relPath']
+
+        print("url %d: %s" % (count,dataUrl) )
+        with urllib.request.urlopen( dataUrl ) as f:
+            vxml = f.read().decode('utf-8')
+            xmlData = ET.fromstring(vxml)
+
+            stn_name=''
+            tc_id=''
+            lat=''
+            lon=''
+            air_temp=''
+
+            for i in xmlData.iter():
+                name = i.get('name')
+                if name == 'stn_nam' :
+                   stn_name= i.get('value')
+                elif name == 'tc_id' :
+                   tc_id = i.get('value')
+                elif name == 'lat' :
+                   lat =  i.get('value')
+                elif name == 'long' :
+                   lon  = i.get('value')
+                elif name == 'air_temp' :
+                   air_temp = i.get('value')
+
+            print( 'station: %s, tc_id: %s, lat: %s, long: %s, air_temp: %s' %
+                   ( stn_name, tc_id, lat, lon, air_temp  ))
+        h.ack(m)
+        count += 1
+        if count > 10:
+            break
+    time.sleep(1)
+
+h.cleanup() # remove server-side queue defined by Factory.
+h.close()
+print("obtained 10 product temperatures")
+
+
+
+
+
+
+
+
+
+2023-05-28 15:01:57,568 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_QuelquechoseDUtile (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+2023-05-28 15:01:57,568 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_QuelquechoseDUtile with v02.post.*.WXO-DD.observations.swob-ml.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)
+
+
+
+
+
+
+
+url 0: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CWZU/2023-05-28-1900-CWZU-AUTO-swob.xml
+station: SHEARWATER JETTY, tc_id: WZU, lat: 44.628055, long: -63.5225, air_temp: 23.3
+url 1: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CYVR/2023-05-28-1900-CYVR-MAN-swob.xml
+station: Vancouver International, tc_id: , lat: 49.19470, long: -123.18400, air_temp: 18.2
+url 2: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CAHK/2023-05-28-1900-CAHK-AUTO-swob.xml
+station: HALIFAX KOOTENAY, tc_id: AHK, lat: 44.5875, long: -63.55, air_temp: 25.2
+url 3: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CPCN/2023-05-28-1900-CPCN-AUTO-swob.xml
+station: CAPPON, tc_id: PCN, lat: 51.066947, long: -110.796689, air_temp: 20.8
+url 4: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CWMT/2023-05-28-1901-CWMT-AUTO-minute-swob.xml
+station: WHATI, tc_id: WMT, lat: 63.1343, long: -117.244497, air_temp: 11.3
+url 5: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CWPO/2023-05-28-1901-CWPO-AUTO-minute-swob.xml
+station: PILOT MOUND (AUT), tc_id: WPO, lat: 49.187933, long: -98.9064, air_temp: 30.3
+url 6: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CVOC/2023-05-28-1858-CVOC-AUTO-minute-swob.xml
+station: WHISTLER - NESTERS, tc_id: VOC, lat: 50.1289, long: -122.9546, air_temp: 18.1
+url 7: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CXCP/2023-05-28-1900-CXCP-AUTO-swob.xml
+station: CHAMPION AGDM, tc_id: XCP, lat: 50.281945, long: -113.350278, air_temp: 23.7
+url 8: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CYZG/2023-05-28-1900-CYZG-MAN-swob.xml
+station: Salluit, tc_id: , lat: 62.17940, long: -75.66720, air_temp: 2.4
+url 9: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CVOC/2023-05-28-1900-CVOC-AUTO-swob.xml
+station: WHISTLER - NESTERS, tc_id: VOC, lat: 50.1289, long: -122.9546, air_temp: 18.1
+url 10: https://hpfx.collab.science.gc.ca/20230528/WXO-DD/observations/swob-ml/20230528/CNZS/2023-05-28-1900-CNZS-AUTO-swob.xml
+station: CORAL HARBOUR RCS, tc_id: NZS, lat: 64.187831, long: -83.347054, air_temp: -1.5
+
+
+
+
+
+
+
+2023-05-28 15:02:01,891 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_QuelquechoseDUtile
+
+
+
+
+
+
+
+obtained 10 product temperatures
+
+
+
+
+

Télécharger des données avec Python

+

Vous pouvez utiliser la bibliothèque python urllib pour télécharger des données, puis les analyser. Dans cet exemple, les données sont une structure XML par message téléchargé et lu en mémoire. Certaines données de station sont ensuite imprimées.

+

Cela fonctionne bien avec urllib pour les ressources de protocole de transport hyper-test, mais d’autres ressources peuvent être annoncées à l’aide d’autres protocoles, tels que sftp ou ftp. Le code python devra être étendu pour traiter avec d’autres protocoles, ainsi que des conditions d’erreur, telles que des pannes temporaires.

+
+
+

Conclusion

+

Sarracenia.moth.amqp est le moyen le plus léger d’ajouter la consommation de messages Sarracenia à votre pile python existante. Vous demandez explicitement de nouveaux messages lorsque vous êtes prêt à les utiliser.

+

Ce type d’intégration ne fournit pas:

+
    +
  • data retrieval: vous avez besoin de votre propre code pour télécharger les données correspondantes,

  • +
  • error recovery: s’il y a des erreurs transitoires, vous devez créer un code de récupération d’erreur (pour récupérer des téléchargements partiels.)

  • +
  • async/event/data driven: une façon de dire “faites ceci chaque fois que vous obtenez un fichier” … définissez les rappels à exécuter lorsqu’un événement particulier se produit, plutôt que le flux séquentiel illustré ci-dessus.

  • +
+

La classe sarracenia.flow fournit des téléchargements, une récupération d’erreur et une API asynchrone à l’aide de la classe sarracenia.flowcb (flowCallback).

+
+
[ ]:
+
+
+
+
+
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/4_api_moth_sub_demo.ipynb b/fr/Tutoriel/4_api_moth_sub_demo.ipynb new file mode 100644 index 000000000..7df2d2adf --- /dev/null +++ b/fr/Tutoriel/4_api_moth_sub_demo.ipynb @@ -0,0 +1,418 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "annoying-preservation", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "# Un premier exemple utilisant l'API Sarracenia Moth\n", + "\n", + "Sarracenia est un package conçu pour annoncer la disponibilité de nouvelles données, généralement sous forme de fichiers. \n", + "Nous plaçons les fichiers sur des serveurs standard, les rendons disponibles via le Web ou sftp, \n", + "et informons les utilisateurs qu'ils sont arrivés à l'aide de messages. \n", + "\n", + "Sarracenia utilise des protocoles de transmission de messages standard existants, comme rabbitmq/AMQP pour transporter les messages, \n", + "et dans les cercles de transmission de messages, car le serveur qui distribue les messages est appelé un *courtier* (broker).\n", + "\n", + "Nous appelons la combinaison d'un courtier de messages et d'un serveur de fichiers (qui peut être un serveur unique ou un grand cluster) une **pompe de données** (data pump).\n", + "\n", + "En supposant que vous avez installé le paquet **metpx-sr3**, soit en tant que paquet debian, ou via pip, \n", + "les annonces d'accès à sens unique à utiliser avec la classe sarracenia.moth (Messages Organisés par les en-têtes de sujet), \n", + "qui permet à un programme python de se connecter à un serveur Sarracenia, \n", + "et commencer à recevoir des messages qui annoncent des ressources.\n", + "\n", + "La fabrique pour construire les objets sarracenia.moth prend deux arguments : \n", + "\n", + "* courtier : un objet (Credential) contenant une url pointant vers le serveur de messagerie qui annonce des produits, et d'autres options associées.\n", + "* options : un dictionnaire d'autres paramètres que la classe pourrait utiliser.\n", + "\n", + "L'exemple ci-dessous construit un appel à un courtier auquel tout le monde peut accéder \n", + "et demander 10 annonces.\n", + "\n", + "Vous pouvez l'exécuter, puis nous pourrons discuter de quelques paramètres :" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "romance-handy", + "metadata": { + "scrolled": true, + "slideshow": { + "slide_type": "slide" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "options: {'acceptUnmatched': True, 'batch': 25, 'bindings': [('xpublic', ['v02', 'post'], ['#'])], 'broker': , 'dry_run': False, 'exchange': None, 'expire': None, 'inline': False, 'inlineEncoding': 'guess', 'inlineByteMax': 4096, 'logFormat': '%(asctime)s [%(levelname)s] %(name)s %(funcName)s %(message)s', 'logLevel': 'info', 'messageDebugDump': False, 'message_strategy': {'reset': True, 'stubborn': True, 'failure_duration': '5m'}, 'message_ttl': 0, 'topicPrefix': ['v02', 'post'], 'tlsRigour': 'normal', 'auto_delete': False, 'durable': True, 'exchangeDeclare': True, 'prefetch': 25, 'queueName': 'q_anonymous_fractal_QuelquechoseDUtile', 'queueBind': True, 'queueDeclare': True, 'reset': False, 'subtopic': [], 'vhost': '/', 'queue_name': 'q_anonymous_fractal_QuelquechoseDUtile'}\n" + ] + } + ], + "source": [ + "import sarracenia.moth\n", + "import sarracenia.moth.amqp\n", + "import sarracenia.credentials\n", + "\n", + "import time\n", + "import socket\n", + "\n", + "\n", + "options = sarracenia.moth.default_options\n", + "options.update(sarracenia.moth.amqp.default_options)\n", + "options['broker'] = sarracenia.credentials.Credential('amqps://anonymous:anonymous@hpfx.collab.science.gc.ca')\n", + "options['topicPrefix'] = [ 'v02', 'post' ]\n", + "options['bindings'] = [('xpublic', ['v02', 'post'], ['#'])]\n", + "options['queueName'] = 'q_anonymous_' + socket.getfqdn() + '_QuelquechoseDUtile'\n", + "\n", + "print('options: %s' % options)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "id": "figured-estimate", + "metadata": { + "slideshow": { + "slide_type": "slide" + } + }, + "source": [ + "\n", + "Le paramètre **courtier**(broker) est un objet contenant une URL conventionnelle et d'autres options, indiquant le protocole de messagerie à utiliser pour accéder au serveur en amont. Lorsque vous vous connectez à un courtier, vous devez lui indiquer les messages qui vous intéressent. \n", + "Dans Moth, tous les courtiers auxquels nous accédons doivent utiliser des hiérarchies de sujets. Vous pouvez les voir si vous avez \n", + "exécuté avec succès l'exemple ci-dessus, il devrait y avoir dans les impressions de message un élément \"sujet\"(topic) dans les dictionnaires. \n", + "En voici un exemple :\n", + "\n", + "__v02.post.20210213.WXO-DD.observations.swob-ml.20210213.CTZR__\n", + "\n", + "Celle-ci se divise en deux parties :\n", + "\n", + "* topic_prefix: v02.post\n", + "* le reste de l'arborescence des rubriques est le reflet du chemin vers le produit annoncé, par rapport à un répertoire de base.\n", + "\n", + "\n", + "Dans AMQP, il y a le concept des \"échanges\" qui sont en quelque sorte comparables aux chaînes de télévision... ce sont des regroupements d'annonces. donc pour se connecter à un courtier AMQP, il faut spécifier:\n", + "\n", + "* exchange: Sarracenia promulgue xpublic comme défaut conventionnel.\n", + "* topic_prefix: décidez quelle version des messages vous souhaitez obtenir. Ce serveur produit des v02.\n", + "* subtopic: à quel sous-ensemble de messages topic_prefix voulons-nous nous abonner.\n", + "\n", + "\n", + "## Liaisons\n", + "\n", + "L'option de liaisons définit les trois valeurs ci-dessus. dans l'exemple, les liaisons sont :\n", + "\n", + "* topic_prefix: v02.post (obtenir des messages v02.)\n", + "* exchange: xpublic (celui par défaut.)\n", + "* subtopic: # ( un joker AMQP signifiant tout. )\n", + "\n", + "on se connecte au courtier\n", + "\n", + "amqp://hpfx.collab.science.gc.ca, sur l'échange *xpublic*, et nous serons intéressés par tous les messages correspondant à la spécification de sujet v02.post.#... (c'est-à-dire tous les messages v02 disponibles .)\n", + "\n", + "### sous-thème\n", + "\n", + "Le sous-thème ici ( __#__ ) correspond à tout ce qui est produit sur le serveur. Plus le sous-thème est large, plus il y a de messages à envoyer et plus le traitement est important. Il est préférable de le rendre plus étroit. En prenant l'exemple ci-dessus, si nous sommes intéressés par swob, un sous-thème comme:\n", + "\n", + "* *.WXO-DD.observations.swob-ml.#\n", + "\n", + "correspondrait à tous les swobs similaires à celui ci-dessus, mais évitez de vous envoyer des messages pour des non-swobs.\n", + "\n", + "## queue_name\n", + "\n", + "Par convention, dans les courtiers administrés par Sarracenia, les utilisateurs ne peuvent créer que des files d'attente commençant par q_ suivi de leur nom d'utilisateur. nous nous sommes connectés en tant qu'anonymes, et donc q_anonymous doit être utilisé. Après cela, le reste peut être ce que vous voulez, mais il y a quelques considérations :\n", + "\n", + "* Si vous souhaitez démarrer plusieurs processus Python pour partager un flux de données, ils spécifient tous le même nom de file d'attente et ils partageront le flux de messages. Il s'adapte bien à quelques dizaines de téléchargeurs coopérants, mais ne s'adapte pas à l'infini, ne vous attendez pas à plus d'environ 99 processus pour pouvoir partager efficacement une charge à partir d'une seule file d'attente. Pour évoluer au-delà de cela avec AMQP, plusieurs sélections sont préférables.\n", + "\n", + "* si vous allez demander de l'aide aux administrateurs de la pompe de données ... vous devrez leur fournir le nom de la file d'attente, et ils devront peut-être pouvoir le choisir parmi des centaines ou des milliers qui se trouvent sur le serveur.\n", + "\n", + "## Messages\n", + "\n", + "Différents protocoles de messagerie ont différentes structures et conventions de stockage. la classe MoTH renvoie les messages sous forme de dictionnaires python, \n", + "quel que soit le protocole utilisé pour les obtenir ou, en cas de transfert, pour les transmettre. On peut ajouter des champs pour une utilisation programmatique aux messages simplement en ajoutant des éléments au dictionnaire. \n", + "S'ils sont uniquement destinés à un usage interne, ajoutez le nom de l'élément du dictionnaire à la clé spéciale '\\_deleteOnPost', afin que l'élément du dictionnaire soit supprimé lors du transfert du message.\n", + "\n", + "## Ack\n", + "\n", + "Les messages sont marqués en transit par le courtier, et si vous ne les reconnaissez pas, la pompe de données les conservera et les réexpédiera éventuellement. conserver les messages en attente en mémoire ralentira également le traitement de tous les messages. Il faut accuser réception des messages dès que possible, mais pas si tôt que vous perdrez des données si le programme est interrompu. Dans l'exemple, nous reconnaissons après avoir fait notre travail d'impression du message.\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "little-louis", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-28 15:01:16,250 [INFO] sarracenia.moth.amqp __getSetup queue declared q_anonymous_fractal_QuelquechoseDUtile (as: amqps://anonymous@hpfx.collab.science.gc.ca) \n", + "2023-05-28 15:01:16,251 [INFO] sarracenia.moth.amqp __getSetup binding q_anonymous_fractal_QuelquechoseDUtile with v02.post.# to xpublic (as: amqps://anonymous@hpfx.collab.science.gc.ca)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message 0: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_5eebe93b78f7f20d6c58dff7079f17f8:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190113.733', 'atime': '20230528T190113.733', 'pubTime': '20230528T190113.733', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_HGT_ISBL_1000_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'DcEZ6+fx637myOUf83VyDQ=='}, 'size': 236654, 'exchange': 'xpublic', 'ack_id': 1, 'local_offset': 0}\n", + "le premier 50 octets du fichier annoncé: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x03\\x9cn\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1c\\x0c\\x00\\x00\\x01\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x12q1\\x00\\x00\\x00'\n", + "message 1: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_abed0a37b3c8b8511cc78b6f8c5c6a82:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190114.13', 'atime': '20230528T190114.13', 'pubTime': '20230528T190114.13', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_SPFH_ISBL_0150_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'oMQDWV/QlF9aLLGOu+Tumw=='}, 'size': 330883, 'exchange': 'xpublic', 'ack_id': 2, 'local_offset': 0}\n", + "le premier 50 octets du fichier annoncé: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x05\\x0c\\x83\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1c\\x0c\\x00\\x00\\x01\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x12q1\\x00\\x00\\x00'\n", + "message 2: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190111', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_7af6caa9fd11fae11919525225783fad:CMC:HRDPS:GRIB2:BIN::20230528190111', 'source': 'WXO-DD', 'mtime': '20230528T190113.823', 'atime': '20230528T190113.823', 'pubTime': '20230528T190113.823', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_DEPR_ISBL_0850_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'zSw+zw6P1XlQayy+CjoLAg=='}, 'size': 194315, 'exchange': 'xpublic', 'ack_id': 3, 'local_offset': 0}\n", + "le premier 50 octets du fichier annoncé: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x02\\xf7\\x0b\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1c\\x0c\\x00\\x00\\x01\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x12q1\\x00\\x00\\x00'\n", + "message 3: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'CMC:HRDPS:GRIB2:BIN::20230528190112', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_e02209b0564eb4b19dd746af9eb5ee9c:CMC:HRDPS:GRIB2:BIN::20230528190112', 'source': 'WXO-DD', 'mtime': '20230528T190114.89', 'atime': '20230528T190114.89', 'pubTime': '20230528T190114.89', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/model_hrdps/north/grib2/12/006/CMC_hrdps_north_WDIR_TGL_40_ps2.5km_2023052812_P006-00.grib2', 'subtopic': ['20230528', 'WXO-DD', 'model_hrdps', 'north', 'grib2', '12', '006'], 'identity': {'method': 'md5', 'value': 'GpPL5qQEOn0ALfuOzQrIHw=='}, 'size': 529466, 'exchange': 'xpublic', 'ack_id': 4, 'local_offset': 0}\n", + "le premier 50 octets du fichier annoncé: b'GRIB\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x08\\x14:\\x00\\x00\\x00\\x15\\x01\\x006\\x00\\x00\\x04\\x00\\x01\\x07\\xe7\\x05\\x1c\\x0c\\x00\\x00\\x01\\x02\\x00\\x00\\x00A\\x03\\x00\\x00\\x12q1\\x00\\x00\\x00'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-05-28 15:01:17,548 [INFO] sarracenia.moth.amqp getCleanUp deleteing queue q_anonymous_fractal_QuelquechoseDUtile\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "message 4: {'_format': 'v02', '_deleteOnPost': {'subtopic', 'ack_id', '_format', 'local_offset', 'exchange'}, 'sundew_extension': 'DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528190109', 'from_cluster': 'DDSR.CMC', 'to_clusters': 'ALL', 'filename': 'msg_ddsr-WXO-DD_0926936c6c7b2968e12487b5e10b3bc9:DMS:WXO_RENAMED_SWOB2:MSC:XML::20230528190109', 'source': 'WXO-DD', 'mtime': '20230528T190111.364', 'atime': '20230528T190111.364', 'pubTime': '20230528T190111.364', 'baseUrl': 'https://hpfx.collab.science.gc.ca', 'relPath': '/20230528/WXO-DD/observations/swob-ml/20230528/CVKU/2023-05-28-1900-CVKU-AUTO-minute-swob.xml', 'subtopic': ['20230528', 'WXO-DD', 'observations', 'swob-ml', '20230528', 'CVKU'], 'identity': {'method': 'md5', 'value': 'WEEsvB9/BKQC1Pv9hgO3LA=='}, 'size': 6426, 'exchange': 'xpublic', 'ack_id': 5, 'local_offset': 0}\n", + "le premier 50 octets du fichier annoncé: b' 10:\n", + " break\n", + " time.sleep(1)\n", + "\n", + "h.cleanup() # remove server-side queue defined by Factory.\n", + "h.close()\n", + "print(\"obtained 10 product temperatures\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "neither-radius", + "metadata": {}, + "source": [ + "## Télécharger des données avec Python\n", + "\n", + "Vous pouvez utiliser la bibliothèque python urllib pour télécharger des données, puis les analyser. \n", + "Dans cet exemple, les données sont une structure XML par message téléchargé et lu en mémoire. \n", + "Certaines données de station sont ensuite imprimées.\n", + "\n", + "Cela fonctionne bien avec urllib pour les ressources de protocole de transport hyper-test, mais d'autres ressources peuvent être annoncées à l'aide d'autres protocoles, tels que sftp ou ftp. Le code python devra être étendu pour traiter\n", + "avec d'autres protocoles, ainsi que des conditions d'erreur, telles que des pannes temporaires.\n" + ] + }, + { + "cell_type": "markdown", + "id": "blank-emphasis", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "[Sarracenia.moth.amqp](../Reference/code.html#module-sarracenia.moth) est le moyen le plus léger d'ajouter la consommation de messages Sarracenia à votre pile python existante. Vous demandez explicitement de nouveaux messages lorsque vous êtes prêt à les utiliser. \n", + "\n", + "Ce type d'intégration ne fournit pas:\n", + "\n", + "* data retrieval: vous avez besoin de votre propre code pour télécharger les données correspondantes,\n", + "\n", + "* error recovery: s'il y a des erreurs transitoires, vous devez créer un code de récupération d'erreur (pour récupérer des téléchargements partiels.)\n", + "\n", + "* async/event/data driven: une façon de dire \"faites ceci chaque fois que vous obtenez un fichier\" ... définissez les rappels à exécuter lorsqu'un événement particulier se produit, plutôt que le flux séquentiel illustré ci-dessus.\n", + "\n", + "La classe sarracenia.flow fournit des téléchargements, une récupération d'erreur et une API asynchrone à l'aide de la classe sarracenia.flowcb (flowCallback).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "senior-dressing", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "celltoolbar": "Slideshow", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fr/Tutoriel/5_api_moth_post_demo.html b/fr/Tutoriel/5_api_moth_post_demo.html new file mode 100644 index 000000000..f4cce9496 --- /dev/null +++ b/fr/Tutoriel/5_api_moth_post_demo.html @@ -0,0 +1,499 @@ + + + + + + + Publication à partir du code Python — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Publication à partir du code Python

+

Si vous avez du code python qui crée déjà des fichiers, et vous ne souhaitez pas invoquer un programme séparé pour publier les fichiers, vous pouvez accéder facilement à la logique de publication des messages, étant donné un fichier existant.

+

Cet exemple concerne la création par programme de publications pour des fichiers. Il nécessite un accès en écriture à un courtier (broker), avec un utilisateur autorisé à publier sur un échange.

+

Besoin d’établir une configuration de publication, puis d’instancier un posting_engine (une instance qui peut être utilisée pour publier des messages.)

+
+
[1]:
+
+
+
+import sarracenia
+import sarracenia.moth
+import sarracenia.credentials
+from sarracenia.config import default_config
+
+import os
+import time
+import socket
+
+cfg = default_config()
+cfg.logLevel = 'debug'
+cfg.broker = sarracenia.credentials.Credential('amqp://tfeed:password@localhost')
+cfg.exchange = 'xpublic'
+cfg.post_baseUrl = 'http://host'
+cfg.post_baseDir = '/tmp'
+
+# moth wants a dict as options, rather than sarracenia.config.Config instance.
+posting_engine = sarracenia.moth.Moth.pubFactory(cfg.broker, cfg.dictify() )
+
+
+
+
+
+
+
+
+2022-02-04 14:56:31,477 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@flow', 'copyright': 'Copyright (c) 2007-2019 Pivotal Software, Inc.', 'information': 'Licensed under the MPL 1.1. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 22.2.7', 'product': 'RabbitMQ', 'version': '3.8.2'}, mechanisms: [b'AMQPLAIN'], locales: ['en_US']
+2022-02-04 14:56:31,494 [DEBUG] amqp __init__ using channel_id: 1
+2022-02-04 14:56:31,529 [DEBUG] amqp _on_open_ok Channel open
+
+
+

ensuite, nous créons un fichier texte…

+
+
[2]:
+
+
+
+sample_fileName = '/tmp/sample.txt'
+sample_file = open( sample_fileName , 'w')
+sample_file.write(
+"""
+CACN00 CWAO 161800
+PMN
+160,2021,228,1800,1065,100,-6999,20.49,43.63,16.87,16.64,323.5,9.32,27.31,1740,317.8,19.22,1.609,230.7,230.7,230.7,230.7,0,0,0,16.38,15.59,305.
+9,17.8,16.38,19.35,55.66,15.23,14.59,304,16.67,3.844,20.51,18.16,0,0,-6999,-6999,-6999,-6999,-6999,-6999,-6999,-6999,0,0,0,0,0,0,0,0,0,0,0,0,0,
+13.41,13.85,27.07,3473
+"""
+)
+sample_file.close()
+
+
+
+

vous donnez le nom du fichier, la configuration initialisée ci-dessus et l’enregistrement de statistiques du fichier à la fonction msg_init(). Il renverra un message prêt à être envoyé au posting_engine.

+
+
[3]:
+
+
+
+# you can supply msg_init with your files, it will build a message appropriate for it.
+m = sarracenia.Message.fromFileData(sample_fileName, cfg, os.stat(sample_fileName) )
+# here is the resulting message.
+print(m)
+
+# feed the message to the posting engine.
+posting_engine.putNewMessage(m)
+
+# when done, should close... cleaner...
+posting_engine.close()
+
+
+
+
+
+
+
+
+2022-02-04 14:56:38,509 [DEBUG] amqp collect Closed channel #1
+
+
+
+
+
+
+
+{'exchange': 'xpublic', 'local_offset': 0, '_deleteOnPost': {'exchange', 'local_offset', 'new_relPath', 'new_subtopic', 'new_file', 'new_baseUrl', 'new_dir'}, 'pubTime': '20220204T205638.499809504', 'new_dir': '/tmp', 'new_file': 'sample.txt', 'new_baseUrl': 'http://host', 'new_relPath': 'sample.txt', 'new_subtopic': [], 'relPath': 'sample.txt', 'subtopic': [], 'baseUrl': 'http://host', 'from_cluster': 'localhost', 'size': 335, 'mtime': '20220204T205635.203999996', 'atime': '20220204T195639.0199999809', 'mode': '664', 'identity': {'method': 'sha512', 'value': 'w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw=='}}
+
+
+

On peut publier autant de messages que nécessaire avec putNewMessage, lorsque vous avez terminé de publier des fichiers, pour couper proprement la connexion avec le courtier, veuillez fermer le posting_engine.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/5_api_moth_post_demo.ipynb b/fr/Tutoriel/5_api_moth_post_demo.ipynb new file mode 100644 index 000000000..97e73c79e --- /dev/null +++ b/fr/Tutoriel/5_api_moth_post_demo.ipynb @@ -0,0 +1,168 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "absent-economy", + "metadata": {}, + "source": [ + "# Publication à partir du code Python\n", + "\n", + "Si vous avez du code python qui crée déjà des fichiers, et\n", + "vous ne souhaitez pas invoquer un programme séparé pour publier les fichiers, \n", + "vous pouvez accéder facilement à la logique de publication des messages, \n", + "étant donné un fichier existant.\n", + "\n", + "Cet exemple concerne la création par programme de publications pour des fichiers. \n", + "Il nécessite un accès en écriture à un courtier (broker), \n", + "avec un utilisateur autorisé à publier sur un échange.\n", + "\n", + "Besoin d'établir une configuration de publication, puis d'instancier un posting_engine (une instance qui peut être utilisée pour publier des messages.)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "prescribed-mortgage", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2022-02-04 14:56:31,477 [DEBUG] amqp _on_start Start from server, version: 0.9, properties: {'capabilities': {'publisher_confirms': True, 'exchange_exchange_bindings': True, 'basic.nack': True, 'consumer_cancel_notify': True, 'connection.blocked': True, 'consumer_priorities': True, 'authentication_failure_close': True, 'per_consumer_qos': True, 'direct_reply_to': True}, 'cluster_name': 'rabbit@flow', 'copyright': 'Copyright (c) 2007-2019 Pivotal Software, Inc.', 'information': 'Licensed under the MPL 1.1. Website: https://rabbitmq.com', 'platform': 'Erlang/OTP 22.2.7', 'product': 'RabbitMQ', 'version': '3.8.2'}, mechanisms: [b'AMQPLAIN'], locales: ['en_US']\n", + "2022-02-04 14:56:31,494 [DEBUG] amqp __init__ using channel_id: 1\n", + "2022-02-04 14:56:31,529 [DEBUG] amqp _on_open_ok Channel open\n" + ] + } + ], + "source": [ + "import sarracenia\n", + "import sarracenia.moth\n", + "import sarracenia.credentials\n", + "from sarracenia.config import default_config\n", + "\n", + "import os\n", + "import time\n", + "import socket\n", + "\n", + "cfg = default_config()\n", + "cfg.logLevel = 'debug'\n", + "cfg.broker = sarracenia.credentials.Credential('amqp://tfeed:password@localhost')\n", + "cfg.exchange = 'xpublic'\n", + "cfg.post_baseUrl = 'http://host'\n", + "cfg.post_baseDir = '/tmp'\n", + "\n", + "# moth wants a dict as options, rather than sarracenia.config.Config instance.\n", + "posting_engine = sarracenia.moth.Moth.pubFactory(cfg.broker, cfg.dictify() )" + ] + }, + { + "cell_type": "markdown", + "id": "pharmaceutical-airport", + "metadata": {}, + "source": [ + "ensuite, nous créons un fichier texte..." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "early-background", + "metadata": {}, + "outputs": [], + "source": [ + "sample_fileName = '/tmp/sample.txt'\n", + "sample_file = open( sample_fileName , 'w')\n", + "sample_file.write(\n", + "\"\"\"\n", + "CACN00 CWAO 161800\n", + "PMN\n", + "160,2021,228,1800,1065,100,-6999,20.49,43.63,16.87,16.64,323.5,9.32,27.31,1740,317.8,19.22,1.609,230.7,230.7,230.7,230.7,0,0,0,16.38,15.59,305.\n", + "9,17.8,16.38,19.35,55.66,15.23,14.59,304,16.67,3.844,20.51,18.16,0,0,-6999,-6999,-6999,-6999,-6999,-6999,-6999,-6999,0,0,0,0,0,0,0,0,0,0,0,0,0,\n", + "13.41,13.85,27.07,3473\n", + "\"\"\"\n", + ")\n", + "sample_file.close()" + ] + }, + { + "cell_type": "markdown", + "id": "affected-oxygen", + "metadata": {}, + "source": [ + "vous donnez le nom du fichier, la configuration initialisée ci-dessus et l'enregistrement de statistiques du fichier à la fonction msg_init(). Il renverra un message prêt à être envoyé au posting_engine." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "precise-delivery", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2022-02-04 14:56:38,509 [DEBUG] amqp collect Closed channel #1\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'exchange': 'xpublic', 'local_offset': 0, '_deleteOnPost': {'exchange', 'local_offset', 'new_relPath', 'new_subtopic', 'new_file', 'new_baseUrl', 'new_dir'}, 'pubTime': '20220204T205638.499809504', 'new_dir': '/tmp', 'new_file': 'sample.txt', 'new_baseUrl': 'http://host', 'new_relPath': 'sample.txt', 'new_subtopic': [], 'relPath': 'sample.txt', 'subtopic': [], 'baseUrl': 'http://host', 'from_cluster': 'localhost', 'size': 335, 'mtime': '20220204T205635.203999996', 'atime': '20220204T195639.0199999809', 'mode': '664', 'identity': {'method': 'sha512', 'value': 'w5ZwUT1IMAjnQT6TLR9NSLzG5RKijhxq46FjMx5UWtsHM/FNOaYNRmGwonIPfnhE5xUORf3z5dRyI6zdL6ygNw=='}}\n" + ] + } + ], + "source": [ + "# you can supply msg_init with your files, it will build a message appropriate for it.\n", + "m = sarracenia.Message.fromFileData(sample_fileName, cfg, os.stat(sample_fileName) )\n", + "# here is the resulting message.\n", + "print(m)\n", + "\n", + "# feed the message to the posting engine.\n", + "posting_engine.putNewMessage(m)\n", + "\n", + "# when done, should close... cleaner...\n", + "posting_engine.close() " + ] + }, + { + "cell_type": "markdown", + "id": "cleared-worst", + "metadata": {}, + "source": [ + "On peut publier autant de messages que nécessaire avec putNewMessage, lorsque vous avez terminé de publier des fichiers, pour couper proprement la connexion avec le courtier, veuillez fermer le posting_engine." + ] + }, + { + "cell_type": "markdown", + "id": "f26ac712", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fr/Tutoriel/Installer.html b/fr/Tutoriel/Installer.html new file mode 100644 index 000000000..24349f45f --- /dev/null +++ b/fr/Tutoriel/Installer.html @@ -0,0 +1,491 @@ + + + + + + + Installation de MetPX Sarracenia — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Installation de MetPX Sarracenia

+
+

Enregistrement de révision

+
+
version:
+

UNKNOWN

+
+
date:
+

May 21, 2024

+
+
+
+
+

L’avez-vous déjà?

+

Si vous êtes sur un serveur avec celui-ci déjà installé, vous pouvez l’appeler comme suit:

+
fractal% sr3 status
+status:
+Component/Config                         State        Run  Miss   Exp Retry
+----------------                         -----        ---  ----   --- -----
+      total running configs:   0 ( processes: 0 missing: 0 stray: 0 )
+fractal%
+
+
+

Si cela fonctionne, alors vous n’avez pas besoin de l’installer. En supposant que ce n’est pas déjà fait +installé, le package doit-il être installé à l’échelle du système ? ou seulement pour +un seul utilisateur ? Pour une utilisation par un seul utilisateur, la méthode python #PIP doit fonctionner, +donnant accès à sr3 a toutes les bibliothèques nécessaires à l’accès programmatique.

+

Pour une utilisation opérationnelle, un accès administratif peut être nécessaire pour l’installation du package, +et l’intégration avec systemd. Quelle que soit la façon dont il est installé, du traitement +périodique (sur Linux généralement connu sous le nom de cron jobs) peut également avoir besoin d’être configuré.

+

Des fois, Sarracenia peut être partiellement installé. Pour voir un inventaire des modules de Sarracenia +qui sont disponible, on peut se servir de sr3 features:

+
fractal% sr3 features
+
+Status:    feature:   python imports:      Description:
+Installed  amqp       amqp                 can connect to rabbitmq brokers
+Installed  appdirs    appdirs              place configuration and state files appropriately for platform (windows/mac/linux)
+Installed  filetypes  magic                able to set content headers
+Installed  ftppoll    dateparser,pytz      able to poll with ftp
+Installed  humanize   humanize             humans numbers that are easier to read.
+Absent     mqtt       paho.mqtt.client     cannot connect to mqtt brokers
+Installed  redis      redis,redis_lock     can use redis implementations of retry and nodupe
+Installed  sftp       paramiko             can use sftp or ssh based services
+Installed  vip        netifaces            able to use the vip option for high availability clustering
+Installed  watch      watchdog             watch directories
+Installed  xattr      xattr                on linux, will store file metadata in extended attributes
+MISSING    clamd      pyclamd              cannot use clamd to av scan files transferred
+
+ state dir: /home/peter/.cache/sr3
+ config dir: /home/peter/.config/sr3
+
+fractal%
+
+
+

le sens de chaque feature est expliqué (en anglais) et le modules python nécessaire pour +permettre cette fonctionalité sont indiqué dans la troisième colonne.

+

Dans l´exemple on peut voir que pyclamd manque à l´appel, tandis que paramiko nécessaire +pour la fonctionallité SFTP est disponible.

+
+
+

Installation Client

+

Le package est conçu pour python version 3.6 ou supérieure. Sur les systèmes où +ils sont disponibles, les paquets debian sont recommandés. Ceux-ci peuvent être obtenus auprès du +référentiel launchpad. Si vous ne pouvez pas utiliser les paquets Debian, envisagez les paquets pip +avialable de PyPI. Dans les deux cas, les autres paquets python (ou dépendances) nécessaires +seront installé automatiquement par le gestionnaire de paquets.

+

Notez que dans certains cas, le système d’exploitation ne fournit pas tous les +fonctionnalité pour toutes les fonctionnalités, de sorte que l’on peut compléter avec des packages pip, qui +peut être installé à l’échelle du système, dans l’environnement d’un utilisateur ou même dans +venv. Tant que sr3 features signale la fonctionnalité comme disponible, elle +être utilisé.

+
+

Ubuntu/Debian (apt/dpkg) Recommandé

+

Sur Ubuntu 22.04 et dérivés du même:

+
sudo add-apt-repository ppa:ssc-hpc-chp-spc/metpx
+sudo apt update
+sudo apt install metpx-sr3  # pacquet principale.
+sudo apt install metpx-sr3c # client binaire (en C) .
+sudo apt install python3-amqp  # support optionnel pour les courtiers AMWP (rabbitmq)
+sudo apt install python3-paramiko  # support optionnel pour SFTP/SSH
+sudo apt install python3-magic  # support optionnel pour les entêtes "content-type" dans les messages
+sudo apt install python3-xattr  # support optionnel pour stocker les checksum des fichiers dans les attributs étendus.
+sudo apt install python3-paho-mqtt  # support optionnel pour les courtiers MQTT
+sudo apt install python3-netifaces # support optionnel pour les vip (haut-disponibilité)
+sudo apt install python3-dateparser python3-pytz # support optionnel pour les sondages ftp.
+
+
+

Si les paquets ne sont pas disponibles, on peut les remplacer en utilisant python install package (pip) +Actuellement, seuls les paquets Debian incluent des pages de manuel. Les guides sont seulement +disponible dans le référentiel de source. Pour les versions antérieures d’Ubuntu, installez +via pip est requis en raison de dépendances manquantes dans l’environnement python +livré avec des systèmes d’exploitation antérieurs.

+
+

Note

+

pour bien juger si in fichier est un duplicat, dans le cas d’un application ou le checksum ne peut +être dérivé du contenu (méthode: abritrary ( arbitraire ) ) le module python3-xattr est nécessaire. +Un example d’un source affecté sera les données DMS.

+
+

Si une option n’est pas installée, mais est nécessaire pour une configuration donnée, alors sr3 le +détectera et se plaindra, et il faut installer le support manquant:

+
fractal% sr3 foreground subscribe/data-ingest
+.2022-04-01 13:44:48,551 [INFO] 2428565 sarracenia.flow loadCallbacks plugins to load: ['sarracenia.flowcb.post.message.Message', 'sarracenia.flowcb.gather.message.Message', 'sarracenia.flowcb.retry.Retry', 'sarracenia.flowcb.housekeeping.resources.Resources', 'sarracenia.flowcb.log.Log']
+2022-04-01 13:44:48,551 [CRITICAL] 2428565 sarracenia.moth ProtocolPresent support for amqp missing, please install python packages: ['amqp']
+2022-04-01 13:44:48,564 2428564 [CRITICAL] root run_command subprocess.run failed err=Command '['/usr/bin/python3', '/home/peter/Sarracenia/sr3/sarracenia/instance.py', '--no', '0', 'foreground', 'subscribe/data-ingest']' returned non-zero exit status 1.
+
+fractal%
+fractal%
+fractal% sudo apt install python3-amqp
+[sudo] password for peter:
+Reading package lists... Done
+Building dependency tree... Done
+Reading state information... Done
+The following packages were automatically installed and are no longer required:
+  fonts-lyx g++-9 libblosc1 libgdk-pixbuf-xlib-2.0-0 libgdk-pixbuf2.0-0 libjs-jquery-ui liblbfgsb0 libnetplan0 libqhull-r8.0 libstdc++-9-dev python-babel-localedata
+  python-matplotlib-data python-tables-data python3-alabaster python3-brotli python3-cycler python3-decorator python3-et-xmlfile python3-imagesize python3-jdcal python3-kiwisolver
+  python3-lz4 python3-mpmath python3-numexpr python3-openpyxl python3-pandas-lib python3-protobuf python3-pymacaroons python3-pymeeus python3-regex python3-scipy python3-sip
+  python3-smartypants python3-snowballstemmer python3-sympy python3-tables python3-tables-lib python3-tornado python3-unicodedata2 python3-xlrd python3-xlwt sphinx-common
+  unicode-data
+Use 'sudo apt autoremove' to remove them.
+Suggested packages:
+  python-amqp-doc
+The following NEW packages will be installed:
+  python3-amqp
+0 upgraded, 1 newly installed, 0 to remove and 1 not upgraded.
+Need to get 0 B/43.2 kB of archives.
+After this operation, 221 kB of additional disk space will be used.
+Selecting previously unselected package python3-amqp.
+(Reading database ... 460430 files and directories currently installed.)
+Preparing to unpack .../python3-amqp_5.0.9-1_all.deb ...
+Unpacking python3-amqp (5.0.9-1) ...
+Setting up python3-amqp (5.0.9-1) ...
+fractal%
+
+
+

On peut satisfaire les exigences manquantes en utilisant des paquets Debian ou pip. pour utiliser les courtiers mqtt avec +ubuntu 18.04, il faut obtenir la bibliothèque via pip, car les paquets debian sont pour une version trop ancienne.:

+
fractal% pip3 install paho-mqtt
+Defaulting to user installation because normal site-packages is not writeable
+Collecting paho-mqtt
+  Using cached paho_mqtt-1.6.1-py3-none-any.whl
+Installing collected packages: paho-mqtt
+Successfully installed paho-mqtt-1.6.1
+fractal%
+
+
+
+
+

Distributions Redhat/Suse (basées sur rpm)

+

Python distutils sur les distributions basées sur le gestionnaire de paquets redhat ne gère pas les dépendances +avec l’emballage actuel, il faut donc les installer manuellement. +Par exemple, sur fedora 28 obligatoirement:

+
$ sudo dnf install python3-appdirs
+$ sudo dnf install python3-humanize
+$ sudo dnf install python3-psutil
+$ sudo dnf install python3-watchdog
+
+
+

Facultatifs:

+
$ sudo dnf install python3-paramiko  # support pour SFTP/SSH
+$ sudo dnf install python3-amqp   # support pour les messages AMQP (couriers rabbitmq)
+$ sudo dnf install python3-file-magic   # support optionnel pour le champs ¨content-type¨
+$ sudo dnf install python3-xattr   # support optionnel stocké certains métadonnées dans les attributs étendus.
+$ sudo dnf install python3-netifaces # support optionnel pour l´optio vip
+$ sudo dnf install python3-paho-mqtt # support optionnel pour les courtiers MQTT
+
+$ sudo dnf install python3-setuptools # needed to build rpm package.
+
+
+

Si les paquets ne sont pas disponibles, l’un peut remplacer en utilisant python install package (pip)

+

Une fois les dépendances en place, on peut construire un fichier RPM en utilisant setuptools:

+
$ git clone https://github.com/MetPX/sarracenia
+$ cd sarracenia
+
+$ python3 setup.py bdist_rpm
+$ sudo rpm -i dist/*.noarch.rpm
+
+
+

Cette procédure installe uniquement l’application python (pas celle en C). +Aucune page de manuel ni aucune autre documentation n’est installée non plus.

+

The paquet RPM n’inclut pas non plus l’intégration avec Systemd (pour rouler sarra +comme une service.) Il faut faire cette installation manuellement:

+
groupadd sarra
+useradd -g sarra sarra
+wget  https://github.com/MetPX/sarracenia/blob/development/debian/metpx-sr3.service
+cp metpx-sr3.service /lib/systemd/system
+cp metpx-sr3.service /etc/systemd/system
+
+
+

Après cette intervention, on peut utiliser les commandes de SystemD normalement.

+

(Oui, on est d´accord que ceci est triste, plus d´informations: https://github.com/MetPX/sarracenia/issues/863 )

+
+
+

PIP

+

Sur les distributions Windows ou Linux où les packages de système ne sont pas +disponible, ou d’autres cas particuliers, tels que l’utilisation de python dans un virtual env, où +il est plus pratique d’installer le paquet en utilisant pip (python install package) +de http://pypi.python.org/.

+

Il est simple de le faire juste l’essentiel:

+
$ pip install metpx-sr3
+
+
+

on pourrait aussi ajouter les extras:

+
$ pip install metpx-sr3[amqp,mqtt,vip]
+
+
+

Si veut avoir tous les extras:

+
$ pip install metpx-sr3[all]
+
+
+

et à mettre à niveau après l’installation initiale:

+
$ pip install metpx-sr3
+
+
+
    +
  • Pour installer à l’échelle du serveur sur un serveur Linux, préfixez avec sudo

  • +
+

NOTE:

+
* Sur de nombreux systèmes sur lesquels pythons 2 et 3 sont installés, vous devrez peut-être spécifier pip3 plutôt que pip.
+
+* En Windows, pour que la fonction de type de fichier fonctionne, il faut manuellement *pip install python-magic-bin*
+  Pour plus de détails : https://pypi.org/project/python-magic/
+
+
+
+
+

Démarrage et arrêt du système

+

Si l’intention est d’implémenter une pompe de données, il s’agit d’un serveur ayant un rôle à jouer dans la réalisation +de grandes quantités de transferts de données, alors la convention est de créer une application sarra +et de l’organiser pour qu’elle soit démarré au démarrage et arrêté à l’arrêt.

+

Lorsque Sarracenia est installé à l’aide d’un paquet Debian :

+
    +
  • Le fichier d’unité SystemD est installé au bon endroit.

  • +
  • l’utilisateur sarra est créé,

  • +
+

Si vous effectuez l’installation à l’aide de méthodes python3 (pip), ce fichier doit être installé :

+
+
+

au bon endroit. Il peut être installé dans:

+
/lib/systemd/system/metpx-sr3.service
+
+
+

une fois installé, il peut être activé de la manière normale. Il s’attendait à un utilisateur de sarra +pour exister, qui pourrait être créé comme ça:

+
groupadd sarra
+useradd --system --create-home sarra
+
+
+

Les répertoires doivent être read/write pour sarra. Les préférences iront dans +~sarra/.config, et les fichiers d’état seront dans ~sarra/.cache, et le +le traitement périodique (voir la prochaine session) doit également être mis en œuvre.

+
+
+

Traitement périodique/Tâches Cron

+

Quelle que soit la façon dont il est installé, un traitement périodique supplémentaire peut être nécessaire:

+
+
    +
  • pour exécuter sr3 sanity pour s’assurer que les processus appropriés sont en cours d’exécution.

  • +
  • pour nettoyer les anciens répertoires et éviter de remplir le systèmes de fichiers.

  • +
+
+

exemples:

+
# tuer les processus errants ou redémarrer ceux qui auraient pu mourir.
+# en évitant le haut de l’heure ou le bas.
+7,14,21,28,35,42,49,56 * * * sr3 sanity
+# exemple de travaux de nettoyage de répertoire, le script est inclus dans exemples / sous-répertoire.
+17 5,11,17,23 * * *    IPALIAS='192.168.1.27';RESULT=`/sbin/ip addr show | grep $IPALIAS|wc|awk '{print $1}'`; if [ $RESULT -eq 1 ]; then tools/old_hour_dirs.py 6 /Projects/web_root ; fi
+
+
+
+
+

Windows

+

Sous Windows, il existe 2 (autres) options possibles :

+
+
Sans Python

Téléchargez le fichier d’installation de Sarracenia à partir de here, +exécutez-le et suivez les instructions. +N’oubliez pas d’ajouter Le répertoire Python de Sarracenia à votre PATH.

+
+
Avec Anaconda

Créez votre environnement avec le file suggéré par ce référentiel. +L’exécution de cette commande à partir de l’invite Anaconda devrait tout installer:

+
$ conda env create -f sarracenia_env.yml
+
+
+
+
+

Voir Windows user manual pour plus d’informations sur la façon d’exécuter Sarracenia sous Windows.

+
+
+

Paquets

+

Les paquets Debian et les roues python peuvent être téléchargés directement +De: launchpad

+
+
+
+

Source

+

Le code source de chaque module est disponible https://github.com/MetPX:

+
$ git clone https://github.com/MetPX/sarracenia sarracenia
+$ cd sarracenia
+
+
+

Le développement se fait sur la branche principale. On veut probablement une vraie release, +alors exécutez git tag et faites un checkout de la dernière (la dernière version stable):

+
$ git tag
+  .
+  .
+  .
+  v3.00.48
+  v3.00.49
+$ git checkout v3.00.49
+$ python3 -m build --no-isolation
+$ pip3 install dist/metpx_sarracenia-3.00.49-py3-none-any.whl
+
+
+
+
+

Sarrac

+

Le client C est disponible dans des binaires prédéfinis dans les launchpad référentiels aux côtés des paquets python

+
$ sudo add-apt-repository ppa:ssc-hpc-chp-spc/metpx
+$ sudo apt-get update
+$ sudo apt-get install metpx-sr3c
+
+
+

Pour toute version récente d’ubuntu. Le librabbitmq-0.8.0 a été rétroporté dans le PPA. +la dépendance de sarrac. Pour d’autres architectures ou distributions, on peut construire à partir de la source

+
$ git clone https://github.com/MetPX/sarrac
+
+
+

sur n’importe quel système Linux, tant que la dépendance librabbitmq est satisfaite. Notez que le package ne peux +pas se construire ou s’exécuter sur des systèmes non-Linux.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/Mettre_en_place_un_subscriber_distant.html b/fr/Tutoriel/Mettre_en_place_un_subscriber_distant.html new file mode 100644 index 000000000..7952b2e0a --- /dev/null +++ b/fr/Tutoriel/Mettre_en_place_un_subscriber_distant.html @@ -0,0 +1,281 @@ + + + + + + + Comment configurer un abonné distant — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Comment configurer un abonné distant

+

Cet exemple explique comment s’abonner aux fichiers swob du bureau météo d’Environnement Canada.

+
+

Mise en Place

+

Initialiser le stockage des informations d’identification dans le fichier ~/.config/sr3/credentials.conf

+
$ sr3 edit credentials.conf
+  amqps://anonymous:anonymous@dd.weather.gc.ca
+
+
+

Le format est un URL complet sur chaque ligne (amqps://<user>:<password>@<target.url>). +Le fichier credentials.conf devrait être privé (permissions octale linux: 0600). +Les fichiers .conf placés dans ~/.config/sr3/subscribe_directory``seront automatiquement trouvé par ``sr_subscribe, plutôt que de donner le chemin complet.

+

La commande edit démarre l’éditeur configuré par l’utilisateur sur le fichier à créer, dans le répertoire approprié:

+
$ sr3 edit subscribe/swob.conf
+  broker amqps://anonymous@dd.weather.gc.ca
+  subtopic observations.swob-ml.#
+  topicPrefix v02.post
+  directory /tmp/swob_downloads
+  accept .*
+$ mkdir /tmp/swob_downloads
+$ sr3 status subscribe/swob
+  2017-12-14 06:54:54,010 [INFO] sr_subscribe swob 01 is stopped
+
+
+
+

Error

+

La modification actuelle échoue s’il n’y a pas de fichier à l’emplacement prévu +(en fait, il ne crée pas de fichier)act, not create a file) +Voir l’issue #251 pour plus d’informations ou pour se plaindre. +En attendant, utilisez plutôt:

+
$ mkdir -p .config/sarra/subscribe
+$ touch $_/swob.conf
+$ sr3 edit swob.conf
+
+
+
+

broker indique où se connecter pour recevoir le flux de notifications. +Le terme broker est tiré de AMQP (http://www.amqp.org), le protocole utilisé pour transférer les notifications. +Les notifications qui seront reçues ont toutes des topics qui correspondent à leur URL.

+
+

Note

+

Omettre directory du fichier de configuration écrira les fichiers dans le répertoire actuel. +Compte tenu de la rapidité avec laquelle ils arrivent, soyez prêt à nettoyer.

+
+
+
+

Démarrage

+

Maintenant, démarrez l’abonné nouvellement créé:

+
$ sr3 start swob
+  2015-12-03 06:53:35,268 [INFO] user_config = 0 ../swob.conf
+  2015-12-03 06:53:35,269 [INFO] instances 1
+  2015-12-03 06:53:35,270 [INFO] sr3 subscribe swob 0001 started
+
+
+

L’activité peut être surveillée via des fichiers journaux dans ~/.cache/sarra/log/ ou avec la commande log

+
$ sr3 log swob
+
+  2015-12-03 06:53:35,635 [INFO] Binding queue q_anonymous.sr_subscribe.swob.21096474.62787751 with key v02.post.observations.swob-ml.# to exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+  2015-12-03 17:32:01,834 [INFO] user_config = 1 ../swob.conf
+  2015-12-03 17:32:01,835 [INFO] sr_subscribe start
+  2015-12-03 17:32:01,835 [INFO] sr_subscribe run
+  2015-12-03 17:32:01,835 [INFO] AMQP  broker(dd.weather.gc.ca) user(anonymous) vhost(/)
+  2015-12-03 17:32:01,835 [INFO] AMQP  input :    exchange(xpublic) topic(v02.post.observations.swob-ml.#)
+  2015-12-03 17:32:01,835 [INFO] AMQP  output:    exchange(xs_anonymous) topic(v02.report.#)
+
+  2015-12-03 17:32:08,191 [INFO] Binding queue q_anonymous.sr_subscribe.swob.21096474.62787751 with key v02.post.observations.swob-ml.# to exchange xpublic on broker amqps://anonymous@dd.weather.gc.ca/
+
+
+

[Ctrl] + [C] pour quitter la surveillance des journaux. +Le journal de démarrage semble normal, indiquant que les informations d’authentification ont été acceptées. +sr_subscribe recevra la notification et téléchargera le fichier dans le répertoire actuel +(sauf indication contraire dans le fichier de configuration).

+

Un téléchargement normal ressemble à ceci

+
2015-12-03 17:32:15,031 [INFO] Received topic   v02.post.observations.swob-ml.20151203.CMED
+2015-12-03 17:32:15,031 [INFO] Received notice  20151203223214.699 http://dd2.weather.gc.ca/observations/swob-ml/20151203/CMED/2015-12-03-2200-CMED-AUTO-swob.xml
+2015-12-03 17:32:15,031 [INFO] Received headers {'filename': '2015-12-03-2200-CMED-AUTO-swob.xml', 'parts': '1,3738,1,0,0', 'sum': 'd,157a9e98406e38a8252eaadf68c0ed60', 'source': 'metpx', 'to_clusters': 'DD,DDI.CMC,DDI.ED M', 'from_cluster': 'DD'}
+2015-12-03 17:32:15,031 [INFO] downloading/copying into ./2015-12-03-2200-CMED-AUTO-swob.xml
+
+
+

Donner toutes les informations contenues dans la notification. +Voici un échec

+
2015-12-03 17:32:30,715 [INFO] Downloads: http://dd2.weather.gc.ca/observations/swob-ml/20151203/CXFB/2015-12-03-2200-CXFB-AUTO-swob.xml  into ./2015-12-03-2200-CXFB-AUTO-swob.xml 0-6791
+2015-12-03 17:32:30,786 [ERROR] Download failed http://dd2.weather.gc.ca/observations/swob-ml/20151203/CXFB/2015-12-03-2200-CXFB-AUTO-swob.xml
+2015-12-03 17:32:30,787 [ERROR] Server couldn't fulfill the request. Error code: 404, Not Found
+
+
+

Ce message n’est pas toujours un échec car sr_subscribe tente à plusieurs reprises avant d’abandonner. +Après quelques minutes, voici à quoi ressemble le répertoire de téléchargement

+
$ ls -al | tail
+  -rw-rw-rw-  1 peter peter   7875 Dec  3 17:36 2015-12-03-2236-CL3D-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7868 Dec  3 17:37 2015-12-03-2236-CL3G-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7022 Dec  3 17:37 2015-12-03-2236-CTRY-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   6876 Dec  3 17:37 2015-12-03-2236-CYPY-AUTO-swob.xml
+  -rw-rw-rw-  1 peter peter   6574 Dec  3 17:36 2015-12-03-2236-CYZP-AUTO-swob.xml
+  -rw-rw-rw-  1 peter peter   7871 Dec  3 17:37 2015-12-03-2237-CL3D-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7873 Dec  3 17:37 2015-12-03-2237-CL3G-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7037 Dec  3 17:37 2015-12-03-2237-CTBF-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter   7022 Dec  3 17:37 2015-12-03-2237-CTRY-AUTO-minute-swob.xml
+  -rw-rw-rw-  1 peter peter 122140 Dec  3 17:38 sr_subscribe_dd_swob_0001.log
+
+
+
+
+

Nettoyage

+

Pour ne pas télécharger plus de fichiers, arrêtez l’abonné

+
$ sr_subscribe stop swob
+  2015-12-03 17:32:22,219 [INFO] sr_subscribe swob 01 stopped
+
+
+

Cela laisse cependant la fil d’attente que sr_subscribe start a configuré sur le courtier active, +pour permettre à un abonné défaillant de tenter de se reconnecter sans perdre de progression. +C’est jusqu’à ce que le courtier expire la fil d’attente et la supprime. +Pour indiquer au courtier que nous avons terminé la fil d’attente, demandez à l’abonné de nettoyer

+
$ sr_subscribe cleanup swob
+2015-12-03 17:32:22,008 [INFO] sr_subscribe swob cleanup
+2015-12-03 17:32:22,008 [INFO] AMQP broker(dd.weatheer.gc.ca) user(anonymous) vhost()
+2015-12-03 17:32:22,008 [INFO] Using amqp module (AMQP 0-9-1)
+2015-12-03 17:32:22,008 [INFO] deleting queue q_anonymous.sr_subscribe.swob.21096474.62787751 (anonymous@dd.weather.gc.ca)
+
+
+

La meilleure pratique consiste à effacer la fil d’attente lorsque terminé afin de réduire la charge sur le courtier.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/Mettre_en_place_un_subscriber_local.html b/fr/Tutoriel/Mettre_en_place_un_subscriber_local.html new file mode 100644 index 000000000..323c86896 --- /dev/null +++ b/fr/Tutoriel/Mettre_en_place_un_subscriber_local.html @@ -0,0 +1,253 @@ + + + + + + + Administrateur du serveur : un abonné local — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Administrateur du serveur : un abonné local

+

Cet exemple explique comment s’abonner aux fichiers swob du bureau météorologique d’Environnement Canada.

+
$ sudo apt install rabbitmq-server
+$ sudo rabbitmqctl list_users
+  Listing users ...
+  user    tags
+  guest   [administrator]
+
+$ sudo rabbitmqctl add_user 'bob'
+  Adding user "bob" ...
+  Password: robert
+
+$ sudo rabbitmqctl list_vhosts
+  Listing vhosts ...
+  name
+  /
+
+
+

Définir les autorisations d’utilisateur pour vhost pour configurer bob:read:write:

+
$ sudo rabbitmqctl set_permissions -p "/" "bob" ".*" ".*" ".*"
+  Setting permissions for user "bob" in vhost "/" ...
+
+$ sudo rabbitmqctl set_user_tags bob management
+  Setting tags for user "bob" to [management] ...
+
+$ sudo rabbitmq-plugins enable rabbitmq_management
+$ /etc/init.d/rabbitmq-server restart
+
+
+

Pour plus d’informations sur les différents types de balises d’utilisateur, voir rabbitmq access and permissions. +Ouvrez http://localhost:15672/ dans un navigateur Web. +Connectez-vous avec le nom d’utilisateur/mot de passe créé ci-dessus. +Cliquez sur l’onglet Queues pour surveiller la progression du point de vue du courtier. +Retour dans le terminal:

+
$ mkdir .config/sarra/subscribe
+$ vi .config/sarra/subscribe/test-subscribe.conf
+  broker amqp://bob:robert@localhost/
+  exchange xs_bob
+  directory /tmp/sarra/output
+  accept .*
+
+
+

Configurez les bits qui publient les modifications apportées à l’échange

+
$ mkdir .config/sarra/watch
+$ vi $_/test-watch.conf
+  post_broker amqp://bob:robert@localhost/
+  post_exchange xs_bob
+  path /tmp/sarra/input/
+  events modify,create
+
+$ mkdir -p /tmp/sarra/{in,out}put
+$ sr3 start
+$ sr3_watch log test-watch
+
+
+

–> Tous les rapports normaux.:

+
$ sr_subscribe log test-subscribe
+  .
+  .
+  2020-08-20 16:29:26,111 [ERROR] standard queue name based on:
+    prefix=q_bob
+    component=subscribe
+    exchangeSplit=False
+    no=1
+
+
+

–> Notez la ligne avec [ERROR], elle n’a pas pu trouver la fil d’attente. +c’est parce que la fil d’attente doit d’abord être créée par sr3_watch et puis que que nous avons commencé l’abonné et +watch en même temps avec ‘sr start’ nous sommes tombés dans une petite condition de course. +Cela a été résolu peu de temps après car le sr_subscribe a un temps de nouvelle tentative de 1 seconde. +Cela peut être confirmé avec la page ‘RabbitMQ Queues’ affichant une q_bob.sr_subscribe.test_subscribe. ... fil d’attente dans la liste.:

+
$ touch /tmp/sarra/input/testfile1.txt
+$ ls /tmp/sarra/input/
+  testfile1.txt
+$ ls /tmp/sarra/output/
+    testfile1.txt
+$ sr_subscribe log test-subscribe
+  .
+  .
+  2020-08-20 16:29:26,078 [INFO] file_log downloaded to: /tmp/sarra/output/testfile1.txt
+
+$ sr3_watch log test-watch
+  2020-08-20 16:29:20,612 [INFO] post_log notice=20200820212920.611807823 file:/ /tmp/sarra/input/testfile1.txt headers={'to_clusters':'localhost', 'mtime':'20200820212920.0259232521', 'atime': '20200820212920.0259232521', 'mode': '644', 'parts': '1,0,1,0,0', 'sum':'d,d41d8cd98f00b204e9800998ecf8427e'}
+
+$ touch /tmp/sarra/input/testfile{2..9}.txt
+$ for i in {001..015}; do echo "file #$i" > file$i.txt; done
+$ watch -n 1 ls /tmp/sarra/output/
+
+
+

Maintenant, vous pouvez regarder les fichiers ruisseler dans le dossier de sortie, +Regardez également la page ‘RabbitMQ Queues’ qui recoit et traite les messages AMQP. +Lorsque tout est terminé, vous pouvez arrêter à la fois l’abonné et le watcher avec:

+
$ sr3 stop
+  ...
+$ sr_subscribe cleanup test-subscribe
+  ...
+
+
+

Maintenant, la fil d’attente a été supprimée de RabbitMQ et tous les services ont été arrêtés.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/Windows.html b/fr/Tutoriel/Windows.html new file mode 100644 index 000000000..75ccecfcc --- /dev/null +++ b/fr/Tutoriel/Windows.html @@ -0,0 +1,227 @@ + + + + + + + Manuel de l’utilisateur Windows — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Manuel de l’utilisateur Windows

+

Ce document enseigne aux utilisateurs novices de Python sur Windows comment ils pourraient facilement exécuter Sarracenia de différentes manières. +Les captures d’écran ont été prises à partir de l’édition Windows Server 2012 R2 Standard. N’hésitez pas à créer des issues si +vous croyez que ce document pourrait être enrichi d’un (ou de plusieurs) cas important(s).

+
+

Exécution de Sarracenia avec une invite de commandes

+
+

Dans le menu Démarrer :

+

Cliquez sur Sarracenia (il exécutera sr3.exe redémarrer):

+../../_images/start-menu-1.png +

Cela fera apparaître l’invite de commande de Sarracenia, démarrera les processus Sarracenia comme indiqué par vos configurations et affichera les informations de journalisation.

+../../_images/01_prompt_cmd.png +

Gardez cette fenêtre en vie jusqu’à ce que vous en ayez fini avec Sarracenia. Le fermer ou taper ctrl-c tuera tous les +processus Sarracenia. Vous pouvez également redémarrer Sarracenia qui arrêtera ces processus proprement.

+
+
+

À partir d’une session Windows Powershell :

+

Lancez une session Powershell powershell et tapez cette commande à l’invite

+
sr3 restart
+
+
+

Cela démarrera les processus Sarracenia comme indiqué par vos configurations et affichera les informations de journalisation

+../../_images/02_prompt_powershell.png +

Gardez cette session Powershell en vie jusqu’à ce que vous ayez terminé avec Sarracenia. Pour arrêter Sarracenia, vous pouvez taper:

+
sr3 stop
+
+
+

Cela arrêtera tous les processus Sarracenia proprement, comme le ferait un redémarrage. La fermeture de cette fenêtre tuera également tous les processus.

+
+
+

À partir de l’invite Anaconda :

+

Exécutez cette commande

+
activate sr3 && s3r restart
+
+
+
+
+
+

Exécution de Sarracenia sans invite de commandes

+

Voici un cas où quelqu’un (comme un administrateur de système) doit exécuter Sarracenia sans invite de commande et +s’assurer que le système démarre au démarrage de Windows. +La façon évidente de le faire serait à partir du Planificateur de tâches.

+
+

À partir du Planificateur de tâches :

+

Ouvrez le Planificateur de tâches :

+../../_images/03_task_scheduler.png +

Sélectionnez Créer une tâche de base… dans le panneau d’action à droite :

+../../_images/04_create_basic_task.png +

Cela lancera l’assistant de création de tâche de base où vous …

+
+

Remplissez le nom :

+../../_images/05_fill_the_name.png +

Choisissez le déclencheur :

+../../_images/06_choose_trigger.png +

Choisissez l’action :

+../../_images/07_choose_action.png +

Définissez l’action :

+../../_images/08_define_action.png +

Passez en revue la tâche et choisissez Terminer :

+../../_images/09_finish.png +
+

Ouvrez la boîte de dialogue propriétés et choisissez Exécuter, que l’utilisateur soit connecté ou non et +Exécuter avec les privilèges les plus élevés :

+../../_images/10_properties_dialog.png +

La tâche doit maintenant apparaître dans votre Bibliothèque du Planificateur de tâches avec l’état Prêt.

+../../_images/12_task_scheduler_ready.png +

Ensuite, vous pouvez l’exécuter immédiatement avec le bouton run_action .

+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/Tutoriel/index.html b/fr/Tutoriel/index.html new file mode 100644 index 000000000..3577c48fa --- /dev/null +++ b/fr/Tutoriel/index.html @@ -0,0 +1,220 @@ + + + + + + + Tutoriel — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Tutoriel

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/fr/index.html b/fr/index.html new file mode 100644 index 000000000..33dd0d6d0 --- /dev/null +++ b/fr/index.html @@ -0,0 +1,173 @@ + + + + + + + S´abonner et répliquer — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

S´abonner et répliquer

+

S´abonner et répliquer 3 (Sr3) est la version 3 de Sarracenia : une boîte à outils de gestion de partage de données pub/sub en temps réel pour les utilisateurs en ligne de commande, python et C.

+

Page d’accueil: https://metpx.github.io/sarracenia

+

Discussions: https://github.com/MetPX/sarracenia/discussions +Source: https://github.com/MetPX/sarracenia/tree/development

+PyPI version + +Supported Python versions +License (GPLv2) + +Issue Tracker + +Docker Image Build Status +

CI/CD:

+flow tests status +

Mises en garde :

+ + +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/genindex.html b/genindex.html new file mode 100644 index 000000000..1494ca83d --- /dev/null +++ b/genindex.html @@ -0,0 +1,1828 @@ + + + + + + Index — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Index
  • +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | F + | G + | H + | I + | L + | M + | N + | O + | P + | R + | S + | T + | U + | V + | W + +
+

_

+ + + +
+ +

A

+ + + +
+ +

B

+ + + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

H

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

N

+ + + +
+ +

O

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
    +
  • Sample (class in sarracenia.flowcb.destfn.sample) + +
  • +
  • Sarra (class in sarracenia.flow.sarra) +
  • +
  • + sarracenia + +
  • +
  • Sarracenia (class in sarracenia) +
  • +
  • + sarracenia.config + +
  • +
  • + sarracenia.credentials + +
  • +
  • + sarracenia.diskqueue + +
  • +
  • + sarracenia.filemetadata + +
  • +
  • + sarracenia.flow + +
  • +
  • + sarracenia.flow.poll + +
  • +
  • + sarracenia.flow.post + +
  • +
  • + sarracenia.flow.report + +
  • +
  • + sarracenia.flow.sarra + +
  • +
  • + sarracenia.flow.sender + +
  • +
  • + sarracenia.flow.shovel + +
  • +
  • + sarracenia.flow.subscribe + +
  • +
  • + sarracenia.flow.watch + +
  • +
  • + sarracenia.flow.winnow + +
  • +
  • + sarracenia.flowcb + +
  • +
  • + sarracenia.flowcb.accept + +
  • +
  • + sarracenia.flowcb.accept.delete + +
  • +
  • + sarracenia.flowcb.accept.downloadbaseurl + +
  • +
  • + sarracenia.flowcb.accept.hourtree + +
  • +
  • + sarracenia.flowcb.accept.httptohttps + +
  • +
  • + sarracenia.flowcb.accept.longflow + +
  • +
  • + sarracenia.flowcb.accept.posthourtree + +
  • +
  • + sarracenia.flowcb.accept.postoverride + +
  • +
  • + sarracenia.flowcb.accept.printlag + +
  • +
  • + sarracenia.flowcb.accept.rename4jicc + +
  • +
  • + sarracenia.flowcb.accept.renamedmf + +
  • +
  • + sarracenia.flowcb.accept.renamewhatfn + +
  • +
  • + sarracenia.flowcb.accept.save + +
  • +
  • + sarracenia.flowcb.accept.speedo + +
  • +
  • + sarracenia.flowcb.accept.sundewpxroute + +
  • +
  • + sarracenia.flowcb.accept.testretry + +
  • +
  • + sarracenia.flowcb.accept.toclusters + +
  • +
  • + sarracenia.flowcb.accept.tohttp + +
  • +
  • + sarracenia.flowcb.accept.tolocal + +
  • +
  • + sarracenia.flowcb.accept.tolocalfile + +
  • +
  • + sarracenia.flowcb.accept.wmotypesuffix + +
  • +
  • + sarracenia.flowcb.clamav + +
  • +
  • + sarracenia.flowcb.destfn.sample + +
  • +
  • + sarracenia.flowcb.download + +
  • +
  • + sarracenia.flowcb.filter + +
  • +
  • + sarracenia.flowcb.filter.deleteflowfiles + +
  • +
  • + sarracenia.flowcb.filter.fdelay + +
  • +
  • + sarracenia.flowcb.filter.pclean_f90 + +
  • +
  • + sarracenia.flowcb.filter.pclean_f92 + +
  • +
  • + sarracenia.flowcb.filter.wmo2msc + +
  • +
  • + sarracenia.flowcb.gather + +
  • +
  • + sarracenia.flowcb.gather.file + +
  • +
  • + sarracenia.flowcb.gather.message + +
  • +
  • + sarracenia.flowcb.housekeeping + +
  • +
+ +

T

+ + + +
+ +

U

+ + + +
+ +

V

+ + + +
+ +

W

+ + + +
+ + + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..ddf5410b2 --- /dev/null +++ b/index.html @@ -0,0 +1,178 @@ + + + + + + + Subscribe and Replicate 3 — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ + + +
+

Subscribe and Replicate 3

+

Subscribe and Replicate 3 (Sr3) is version 3 of Sarracenia: +a real-time pub/sub data sharing management toolkit for +command line, python, and C users.

+

Home page: https://metpx.github.io/sarracenia

+

Discussions: https://github.com/MetPX/sarracenia/discussions +Source Repo: https://github.com/MetPX/sarracenia/tree/development

+PyPI version + +Supported Python versions +License (GPLv2) + +Issue Tracker + +Docker Image Build Status +

CI/CD:

+flow tests status + +

Caveats:

+ + +
+
+

Indices and tables

+ +
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/objects.inv b/objects.inv new file mode 100644 index 000000000..fb933bb5f Binary files /dev/null and b/objects.inv differ diff --git a/py-modindex.html b/py-modindex.html new file mode 100644 index 000000000..ff4b4dda2 --- /dev/null +++ b/py-modindex.html @@ -0,0 +1,572 @@ + + + + + + Python Module Index — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Python Module Index
  • +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ s +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ s
+ sarracenia +
    + sarracenia.config +
    + sarracenia.credentials +
    + sarracenia.diskqueue +
    + sarracenia.filemetadata +
    + sarracenia.flow +
    + sarracenia.flow.poll +
    + sarracenia.flow.post +
    + sarracenia.flow.report +
    + sarracenia.flow.sarra +
    + sarracenia.flow.sender +
    + sarracenia.flow.shovel +
    + sarracenia.flow.subscribe +
    + sarracenia.flow.watch +
    + sarracenia.flow.winnow +
    + sarracenia.flowcb +
    + sarracenia.flowcb.accept +
    + sarracenia.flowcb.accept.delete +
    + sarracenia.flowcb.accept.downloadbaseurl +
    + sarracenia.flowcb.accept.hourtree +
    + sarracenia.flowcb.accept.httptohttps +
    + sarracenia.flowcb.accept.longflow +
    + sarracenia.flowcb.accept.posthourtree +
    + sarracenia.flowcb.accept.postoverride +
    + sarracenia.flowcb.accept.printlag +
    + sarracenia.flowcb.accept.rename4jicc +
    + sarracenia.flowcb.accept.renamedmf +
    + sarracenia.flowcb.accept.renamewhatfn +
    + sarracenia.flowcb.accept.save +
    + sarracenia.flowcb.accept.speedo +
    + sarracenia.flowcb.accept.sundewpxroute +
    + sarracenia.flowcb.accept.testretry +
    + sarracenia.flowcb.accept.toclusters +
    + sarracenia.flowcb.accept.tohttp +
    + sarracenia.flowcb.accept.tolocal +
    + sarracenia.flowcb.accept.tolocalfile +
    + sarracenia.flowcb.accept.wmotypesuffix +
    + sarracenia.flowcb.clamav +
    + sarracenia.flowcb.destfn.sample +
    + sarracenia.flowcb.download +
    + sarracenia.flowcb.filter +
    + sarracenia.flowcb.filter.deleteflowfiles +
    + sarracenia.flowcb.filter.fdelay +
    + sarracenia.flowcb.filter.pclean_f90 +
    + sarracenia.flowcb.filter.pclean_f92 +
    + sarracenia.flowcb.filter.wmo2msc +
    + sarracenia.flowcb.gather +
    + sarracenia.flowcb.gather.file +
    + sarracenia.flowcb.gather.message +
    + sarracenia.flowcb.housekeeping +
    + sarracenia.flowcb.housekeeping.resources +
    + sarracenia.flowcb.log +
    + sarracenia.flowcb.mdelaylatest +
    + sarracenia.flowcb.nodupe +
    + sarracenia.flowcb.nodupe.data +
    + sarracenia.flowcb.nodupe.name +
    + sarracenia.flowcb.pclean +
    + sarracenia.flowcb.poll +
    + sarracenia.flowcb.poll.airnow +
    + sarracenia.flowcb.poll.mail +
    + sarracenia.flowcb.poll.nasa_mls_nrt +
    + sarracenia.flowcb.poll.noaa_hydrometric +
    + sarracenia.flowcb.poll.usgs +
    + sarracenia.flowcb.post +
    + sarracenia.flowcb.post.message +
    + sarracenia.flowcb.retry +
    + sarracenia.flowcb.sample +
    + sarracenia.flowcb.scheduled +
    + sarracenia.flowcb.send +
    + sarracenia.flowcb.send.email +
    + sarracenia.flowcb.shiftdir2baseurl +
    + sarracenia.flowcb.v2wrapper +
    + sarracenia.flowcb.work +
    + sarracenia.flowcb.work.age +
    + sarracenia.flowcb.work.rxpipe +
    + sarracenia.identity +
    + sarracenia.identity.arbitrary +
    + sarracenia.identity.md5 +
    + sarracenia.identity.random +
    + sarracenia.identity.sha512 +
    + sarracenia.instance +
    + sarracenia.moth +
    + sarracenia.moth.amq1 +
    + sarracenia.moth.amqp +
    + sarracenia.moth.pika +
    + sarracenia.rabbitmq_admin +
    + sarracenia.transfer +
    + sarracenia.transfer.file +
    + sarracenia.transfer.ftp +
    + sarracenia.transfer.https +
    + sarracenia.transfer.sftp +
+ + +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + \ No newline at end of file diff --git a/search.html b/search.html new file mode 100644 index 000000000..08ed0773e --- /dev/null +++ b/search.html @@ -0,0 +1,122 @@ + + + + + + Search — Sarracenia UNKNOWN documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • »
  • +
  • Search
  • +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+
+ +
+ +
+

© Copyright 2022, Shared Services Canada, Government of Canada, GPLv2.

+
+ + Built with Sphinx using a + theme + provided by Read the Docs. + + +
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/searchindex.js b/searchindex.js new file mode 100644 index 000000000..635a6a5ea --- /dev/null +++ b/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"-h|\u2013help": [[64, "h-help"], [129, "h-help"]], ".sr\u00a7 suffix": [[15, "sr-suffix"]], "1.1\u00a0\u00a0\u00a0Assumptions/Constraints": [[2, "assumptions-constraints"]], "1.1\u00a0\u00a0\u00a0Hypoth\u00e8ses/contraintes": [[91, "hypotheses-contraintes"]], "1.2\u00a0\u00a0\u00a0Nombre de commutateurs": [[91, "nombre-de-commutateurs"]], "1.2\u00a0\u00a0\u00a0Number of Switches": [[2, "number-of-switches"]], "1.3\u00a0\u00a0\u00a0AMQP Feature Selection": [[2, "amqp-feature-selection"]], "1.3\u00a0\u00a0\u00a0S\u00e9lection des fonctionnalit\u00e9s AMQP": [[91, "selection-des-fonctionnalites-amqp"]], "1.4\u00a0\u00a0\u00a0Application": [[2, "application"], [91, "application"]], "1.5.1\u00a0\u00a0\u00a0Routage des publications": [[91, "routage-des-publications"]], "1.5.1\u00a0\u00a0\u00a0Routing Posts": [[2, "routing-posts"]], "1.5.2\u00a0\u00a0\u00a0Journaux de routage": [[91, "journaux-de-routage"]], "1.5.2\u00a0\u00a0\u00a0Routing Logs": [[2, "routing-logs"]], "1.5\u00a0\u00a0\u00a0Routage": [[91, "routage"]], "1.5\u00a0\u00a0\u00a0Routing": [[2, "routing"]], "1.6.1\u00a0\u00a0\u00a0Users, Queues & Exchanges": [[2, "users-queues-exchanges"]], "1.6.1\u00a0\u00a0\u00a0Utilisateurs, files d\u2019attente et \u00e9changes": [[91, "utilisateurs-files-dattente-et-echanges"]], "1.6.2\u00a0\u00a0\u00a0Pre-Validation": [[2, "pre-validation"]], "1.6.2\u00a0\u00a0\u00a0Pr\u00e9-validation": [[91, "pre-validation"]], "1.6.3\u00a0\u00a0\u00a0Post-Validation": [[2, "post-validation"], [91, "post-validation"]], "1.6.4\u00a0\u00a0\u00a0Log Validation": [[2, "log-validation"]], "1.6.4\u00a0\u00a0\u00a0Validation du journal": [[91, "validation-du-journal"]], "1.6.5\u00a0\u00a0\u00a0Private vs. Public Data Transfer": [[2, "private-vs-public-data-transfer"]], "1.6.5\u00a0\u00a0\u00a0Transfert de donn\u00e9es priv\u00e9 vs public": [[91, "transfert-de-donnees-prive-vs-public"]], "1.6.6\u00a0\u00a0\u00a0Acc\u00e8s priv\u00e9 HTTPS": [[91, "acces-prive-https"]], "1.6.6\u00a0\u00a0\u00a0HTTPS Private Access": [[2, "https-private-access"]], "1.6\u00a0\u00a0\u00a0Mod\u00e8le de s\u00e9curit\u00e9": [[91, "modele-de-securite"]], "1.6\u00a0\u00a0\u00a0Security Model": [[2, "security-model"]], "1.7.1\u00a0\u00a0\u00a0Autonome": [[91, "autonome"]], "1.7.1\u00a0\u00a0\u00a0Standalone": [[2, "standalone"]], "1.7.2\u00a0\u00a0\u00a0DDSR : Configuration de commutation/routage": [[91, "ddsr-configuration-de-commutation-routage"]], "1.7.2\u00a0\u00a0\u00a0DDSR: Switching/Routing Configuration": [[2, "ddsr-switching-routing-configuration"]], "1.7.3\u00a0\u00a0\u00a0DDSR ind\u00e9pendant": [[91, "ddsr-independant"]], "1.7.3\u00a0\u00a0\u00a0Independent DDSR": [[2, "independent-ddsr"]], "1.7.4\u00a0\u00a0\u00a0Courtier partag\u00e9 DDSR": [[91, "courtier-partage-ddsr"]], "1.7.4\u00a0\u00a0\u00a0Shared Broker DDSR": [[2, "shared-broker-ddsr"]], "1.7.5\u00a0\u00a0\u00a0DD : Configuration de la diffusion des donn\u00e9es (AKA : Data Mart)": [[91, "dd-configuration-de-la-diffusion-des-donnees-aka-data-mart"]], "1.7.5\u00a0\u00a0\u00a0DD: Data Dissemination Configuration (AKA: Data Mart)": [[2, "dd-data-dissemination-configuration-aka-data-mart"]], "1.7.6\u00a0\u00a0\u00a0DD ind\u00e9pendant": [[91, "dd-independant"]], "1.7.6\u00a0\u00a0\u00a0Independent DD": [[2, "independent-dd"]], "1.7.7\u00a0\u00a0\u00a0Shared-Broker DD": [[2, "shared-broker-dd"], [91, "shared-broker-dd"]], "1.7.8\u00a0\u00a0\u00a0DD de donn\u00e9es partag\u00e9es": [[91, "dd-de-donnees-partagees"]], "1.7.8\u00a0\u00a0\u00a0Shared-Data DD": [[2, "shared-data-dd"]], "1.7.9\u00a0\u00a0\u00a0SEP: Shared End-Point Configuration": [[2, "sep-shared-end-point-configuration"], [91, "sep-shared-end-point-configuration"]], "1.7\u00a0\u00a0\u00a0Topologies": [[2, "topologies"], [91, "topologies"]], "1\u00a0\u00a0\u00a0Conception Strawman": [[91, "conception-strawman"]], "1\u00a0\u00a0\u00a0Strawman Design": [[2, "strawman-design"]], "3.0.14": [[51, "id16"], [85, "id14"]], "3.0.15": [[51, "id15"], [85, "id13"]], "3.0.17": [[51, "id14"], [85, "id12"]], "3.0.22": [[51, "id13"], [85, "id11"]], "3.0.23": [[51, "id12"]], "3.0.25": [[51, "id11"], [85, "id10"]], "3.0.26": [[51, "id10"], [85, "id9"]], "3.0.37": [[51, "id9"]], "3.0.40": [[51, "id8"], [85, "id8"]], "3.0.41": [[51, "id7"], [85, "id7"]], "3.0.45": [[51, "id6"], [85, "id6"]], "3.0.47": [[51, "id5"], [85, "id5"]], "3.0.51": [[51, "id4"], [85, "id4"]], "3.0.52": [[51, "id3"], [85, "id3"]], "3.0.53": [[51, "id2"], [85, "id2"]], "3.0.54": [[51, "id1"], [85, "id1"]], "A first Example using Sarracenia Moth API": [[69, "A-first-Example-using-Sarracenia-Moth-API"]], "A votre gout!": [[120, "a-votre-gout"]], "ACTIONS": [[21, "actions"], [60, "actions"], [108, "actions"], [125, "actions"]], "AMQP": [[27, "amqp"], [107, "amqp"]], "AMQP - Introduction \u00e0 sarrac\u00e9nia": [[90, "amqp-introduction-a-sarracenia"]], "AMQP - Primer for Sarracenia": [[0, "amqp-primer-for-sarracenia"]], "AMQP Feature Selection": [[0, "amqp-feature-selection"]], "AMQP QUEUE BINDINGS": [[21, "amqp-queue-bindings"], [108, "amqp-queue-bindings"]], "AMQP Topic:": [[130, "amqp-topic"]], "AMQP Topic: .{.}": [[65, "amqp-topic-version-dir"]], "AMQP v09 (Rabbitmq) Settings": [[22, "amqp-v09-rabbitmq-settings"], [105, "amqp-v09-rabbitmq-settings"]], "AMQP: not 1.0, but 0.8 or 0.9": [[0, "amqp-not-1-0-but-0-8-or-0-9"]], "AMQP: pas 1.0, mais 0.8 or 0.9": [[90, "amqp-pas-1-0-mais-0-8-or-0-9"]], "API Documentation": [[78, "api-documentation"]], "API Level Access": [[55, "api-level-access"]], "API NOAA CO-OPS": [[82, "api-noaa-co-ops"]], "ARGUMENTS AND OPTIONS": [[64, "arguments-and-options"]], "ARGUMENTS ET OPTIONS": [[129, "arguments-et-options"]], "Abonnements partag\u00e9s": [[100, "abonnements-partages"]], "Abonn\u00e9s (anglais: Subscribers)": [[107, "abonnes-anglais-subscribers"]], "Accel Overhaul": [[20, "accel-overhaul"]], "Accessing Messages from Python": [[39, "accessing-messages-from-python"]], "Accessing Options": [[39, "accessing-options"]], "Acc\u00e8s au niveau de l\u2019API": [[88, "acces-au-niveau-de-l-api"]], "Acc\u00e8s au syst\u00e8me privil\u00e9gi\u00e9": [[106, "acces-au-systeme-privilegie"]], "Acc\u00e8s aux messages \u00e0 partir de Python": [[118, "acces-aux-messages-a-partir-de-python"]], "Acc\u00e8s aux options": [[118, "acces-aux-options"]], "Ack": [[69, "Ack"], [134, "Ack"]], "Acquisition de GOES-R": [[112, "acquisition-de-goes-r"]], "Add RabbitMQ": [[11, "Add-RabbitMQ"], [11, "id1"]], "Adding a New Dependency": [[3, "adding-a-new-dependency"]], "Administering AMQP Data Pumps": [[46, "administering-amqp-data-pumps"]], "Administering Rabbitmq Adddendum": [[47, "administering-rabbitmq-adddendum"]], "Administrateur du serveur : un abonn\u00e9 local": [[138, "administrateur-du-serveur-un-abonne-local"]], "Administration de Rabbitmq Adddendum": [[79, "administration-de-rabbitmq-adddendum"]], "Administration des pompes de donn\u00e9es AMQP": [[80, "administration-des-pompes-de-donnees-amqp"]], "Advanced File Reception": [[39, "advanced-file-reception"]], "Advanced Installations": [[46, "advanced-installations"]], "Advanced Polling": [[21, "advanced-polling"]], "Affichage de tous les param\u00e8tres": [[118, "affichage-de-tous-les-parametres"]], "Affiner la s\u00e9lection": [[88, "affiner-la-selection"]], "After Parity: True Improvements": [[20, "after-parity-true-improvements"]], "Ajout de D\u00e9pendance Python dans les Callbacks": [[118, "ajout-de-dependance-python-dans-les-callbacks"]], "Ajout manuel d\u2019utilisateurs": [[80, "ajout-manuel-d-utilisateurs"]], "Algorithm": [[19, "algorithm"]], "Algorithme": [[96, "algorithme"]], "Almost-Coherent File System or DB Performance": [[8, "Almost-Coherent-File-System-or-DB-Performance"]], "Amdahl\u2019s Law Applied": [[7, "Amdahl\u2019s-Law-Applied"]], "Analogie FTP": [[90, "analogie-ftp"]], "Analogy FTP": [[0, "analogy-ftp"]], "Analyse d\u2019antivirus": [[88, "analyse-d-antivirus"]], "And an Agreed Directory Tree": [[32, "and-an-agreed-directory-tree"]], "Annexe A : Contexte": [[90, "annexe-a-contexte"]], "Annonce de Sr3": [[109, "annonce-de-sr3"]], "Announcing Sr3": [[36, "announcing-sr3"]], "Another example": [[65, "another-example"]], "Anti-Virus Scanning": [[55, "anti-virus-scanning"]], "Apatridie/Crawlable": [[114, "apatridie-crawlable"]], "Aper\u00e7u": [[80, "apercu"], [103, "apercu"]], "Aper\u00e7u D\u00e9taill\u00e9": [[103, "apercu-detaille"]], "Appendix A: Background": [[0, "appendix-a-background"]], "Application Changes in 2017": [[30, "application-changes-in-2017"]], "Applications M\u00e9t\u00e9orologiques": [[112, "applications-meteorologiques"]], "Apr\u00e8s la parit\u00e9 : de vraies am\u00e9liorations": [[101, "apres-la-parite-de-vraies-ameliorations"]], "Assurance de Qualit\u00e9": [[95, "assurance-de-qualite"]], "Assurer la livraison (inflight)": [[104, "assurer-la-livraison-inflight"]], "Audience": [[39, "audience"], [118, "audience"]], "Automated Build": [[14, "automated-build"]], "Autonome": [[106, "autonome"]], "Autres champs de rapport": [[130, "autres-champs-de-rapport"]], "Autres exemples": [[83, "autres-exemples"]], "Autres param\u00e8tres": [[90, "autres-parametres"]], "Avec les avis AMQP sur un serveur de fichiers standard": [[114, "avec-les-avis-amqp-sur-un-serveur-de-fichiers-standard"]], "BODY: { } (JSON encoding.)": [[130, "body-en-tete-json-encoding"]], "BODY: { } (JSON encoding.)": [[65, "body-headers-json-encoding"]], "BUGS/Concerns/Issues": [[20, "bugs-concerns-issues"]], "BUGS/Pr\u00e9occupations/Probl\u00e8mes": [[101, "bugs-preoccupations-problemes"]], "BUNNY-OP and dd_dispatcher.py": [[47, "bunny-op-and-dd-dispatcher-py"]], "BUNNY-OP et dd_dispatcher.py": [[79, "bunny-op-et-dd-dispatcher-py"]], "Back Pressure": [[18, "back-pressure"], [27, "back-pressure"]], "Backporting a Dependency": [[14, "backporting-a-dependency"]], "Basic Idea": [[1, "basic-idea"]], "Better File Reception": [[39, "better-file-reception"]], "Biblioth\u00e8que Shim": [[111, "bibliotheque-shim"]], "Biblioth\u00e8que Shim n\u00e9cessaire": [[111, "bibliotheque-shim-necessaire"]], "Bindings": [[69, "Bindings"]], "Bonus": [[12, "Bonus"]], "Branches Git pour la pr\u00e9-publication": [[95, "branches-git-pour-la-pre-publication"]], "Branches Principales": [[93, "branches-principales"]], "Brief Introduction to Regular Expressions": [[21, "brief-introduction-to-regular-expressions"], [108, "brief-introduction-to-regular-expressions"]], "Bringing Amdahl & CAP Together": [[8, "Bringing-Amdahl-&-CAP-Together"]], "Broker": [[128, "broker"]], "Build Automatis\u00e9e": [[95, "build-automatisee"]], "Build Python3.6 Compatbile Branch": [[14, "build-python3-6-compatbile-branch"]], "Building a Windows Installer": [[14, "building-a-windows-installer"]], "Building an RPM": [[14, "building-an-rpm"]], "Builds quotidiennes": [[95, "builds-quotidiennes"]], "CAP Theorem": [[8, "CAP-Theorem"]], "CAP Theorem Applied": [[8, "CAP-Theorem-Applied"]], "CFG messages": [[33, "cfg-messages"]], "COMPONENTS": [[21, "components"], [60, "components"]], "COMPOSANTS": [[108, "composants"], [125, "composants"]], "CONFIGURATION": [[17, "configuration"], [61, "configuration"], [62, "configuration"], [99, "configuration"], [126, "configuration"], [127, "configuration"]], "CONFIGURATION 1 : R\u00c9PLICATION POMPE \u00c0 POMPE": [[108, "configuration-1-replication-pompe-a-pompe"]], "CONFIGURATION DE DESTINATION 2 : DIFFUSION DE TYPE METPX-SUNDEW": [[108, "configuration-de-destination-2-diffusion-de-type-metpx-sundew"]], "CONFIGURATION FILES": [[21, "configuration-files"]], "CONFIGURATIONS": [[60, "configurations"], [125, "configurations"]], "CONSIDERATIONS WITH ONE TO ONE FILTERS": [[40, "considerations-with-one-to-one-filters"]], "CPUMP": [[21, "cpump"], [108, "cpump"]], "CREDENTIALS": [[21, "credentials"]], "Callbacks that need Python Modules": [[39, "callbacks-that-need-python-modules"]], "Cap Theorem": [[8, "Cap-Theorem"]], "Cartes R\u00e9seau (anglais: Network Maps)": [[107, "cartes-reseau-anglais-network-maps"]], "Cas d\u2019utilisation": [[82, "cas-d-utilisation"], [84, "cas-d-utilisation"]], "Case Study: HPC Mirroring": [[29, "case-study-hpc-mirroring"]], "Caveat: Solution for This Problem, Not Every Problem": [[32, "caveat-solution-for-this-problem-not-every-problem"]], "Ce qui fonctionnera sans changement": [[89, "ce-qui-fonctionnera-sans-changement"]], "Ce qui ne fonctionnera pas sans changement": [[89, "ce-qui-ne-fonctionnera-pas-sans-changement"]], "Central Data Flows": [[30, "central-data-flows"]], "Champs dans les Messages": [[118, "champs-dans-les-messages"]], "Change Defaults": [[46, "change-defaults"]], "Changements de conception depuis l\u2019original (2015)": [[102, "changements-de-conception-depuis-loriginal-2015"]], "Changes Done to Address Pain Points": [[20, "changes-done-to-address-pain-points"]], "Changes Made to create v03": [[35, "changes-made-to-create-v03"]], "Changes from v02": [[35, "changes-from-v02"]], "Choosing AP:": [[8, "Choosing-AP:"]], "Choosing CA:": [[8, "Choosing-CA:"]], "Choosing CP:": [[8, "Choosing-CP:"]], "Cleanup": [[46, "cleanup"], [73, "cleanup"]], "Client": [[23, "client"], [106, "client"]], "Client Installation": [[71, "client-installation"]], "Client-side Filtering": [[21, "client-side-filtering"], [108, "client-side-filtering"]], "Cloud Solves This!": [[8, "Cloud-Solves-This!"]], "Clustered Broker Keepalived Setup": [[46, "clustered-broker-keepalived-setup"]], "Code Reference": [[57, "code-reference"]], "Code Style": [[4, "code-style"]], "Coding Differences between plugins in v2 vs. Sr3": [[56, "coding-differences-between-plugins-in-v2-vs-sr3"]], "Coming in 2018": [[30, "coming-in-2018"]], "Command Line Difference": [[56, "command-line-difference"]], "Command Line Guide": [[21, "command-line-guide"]], "Comment Faire": [[86, "comment-faire"]], "Comment adopter l\u2019AMQP": [[90, "comment-adopter-lamqp"]], "Comment configurer un abonn\u00e9 distant": [[137, "comment-configurer-un-abonne-distant"]], "Comment s\u00e9lectionner Chunksize": [[97, "comment-selectionner-chunksize"]], "Commit de Code": [[93, "commit-de-code"]], "Commits to the Development Branch": [[3, "commits-to-the-development-branch"]], "Commits vers la branche principale": [[93, "commits-vers-la-branche-principale"]], "Committing Code": [[3, "committing-code"]], "Commutateurs/Routage": [[106, "commutateurs-routage"]], "Comparison to 2015 Video": [[30, "comparison-to-2015-video"], [112, "comparison-to-2015-video"]], "Components": [[19, "components"]], "Composants": [[96, "composants"]], "Concepts g\u00e9n\u00e9raux de Sarracenia": [[105, "concepts-generaux-de-sarracenia"]], "Conclusion": [[20, "conclusion"], [66, "Conclusion"], [69, "Conclusion"], [101, "conclusion"], [131, "Conclusion"], [134, "Conclusion"]], "Conclusion:": [[68, "Conclusion:"], [133, "Conclusion:"]], "Concrete Plan (Done)": [[20, "concrete-plan-done"]], "Config": [[78, "config"]], "Config File Entries and Callbacks": [[67, "Config-File-Entries-and-Callbacks"]], "Config File Entries to use Flow_Callbacks": [[49, "config-file-entries-to-use-flow-callbacks"]], "Configuration Files": [[56, "configuration-files"]], "Configuration Upgrade": [[56, "configuration-upgrade"]], "Configuration de fil d\u00b4attente": [[108, "configuration-de-fil-d-attente"]], "Configuration keepalived d\u2019un courtier Clustered": [[80, "configuration-keepalived-d-un-courtier-clustered"]], "Configurations": [[21, "configurations"], [108, "configurations"]], "Configurations a Distance": [[125, "configurations-a-distance"]], "Configurations homologues simples/\u00e9volutives pour les nations": [[114, "configurations-homologues-simples-evolutives-pour-les-nations"]], "Configurer l\u2019environnement de test de flux": [[93, "configurer-lenvironnement-de-test-de-flux"]], "Configurer une branche compatible Python3.6": [[95, "configurer-une-branche-compatible-python3-6"]], "Consid\u00e9rations de s\u00e9curit\u00e9": [[106, "considerations-de-securite"]], "Consid\u00e9rations relatives au d\u00e9ploiement": [[106, "considerations-relatives-au-deploiement"]], "Consid\u00e9rations relatives au serveur/protocole": [[97, "considerations-relatives-au-serveur-protocole"]], "Consid\u00e9rations relatives \u00e0 l\u2019efficacit\u00e9": [[87, "considerations-relatives-a-lefficacite"]], "Contents": [[0, "contents"], [2, "contents"], [30, "contents"], [32, "contents"], [40, "contents"], [42, "contents"], [43, "contents"], [56, "contents"], [57, "contents"], [58, "contents"], [89, "contents"], [90, "contents"], [91, "contents"], [112, "contents"], [114, "contents"]], "Contents:": [[13, null], [16, null], [31, null], [41, null], [44, null], [59, null], [75, null], [86, null], [124, null], [140, null], [141, null], [142, null]], "Contenu en ligne dans les messages": [[114, "contenu-en-ligne-dans-les-messages"]], "Contenu:": [[113, null], [121, null]], "Contenus:": [[98, null]], "Continuous Mirroring": [[29, "continuous-mirroring"]], "Continuously Mirror 27 Million File Tree Very Quickly": [[29, "continuously-mirror-27-million-file-tree-very-quickly"]], "Contre-Pression (anglais: Back Pressure)": [[107, "contre-pression-anglais-back-pressure"]], "Contre-pression": [[100, "contre-pression"]], "Contribuer \u00e0 Sarracenia": [[98, "contribuer-a-sarracenia"]], "Contributing": [[16, "contributing"]], "Contribution": [[4, "contribution"], [92, "contribution"]], "Contributions": [[29, "contributions"], [111, "contributions"]], "Contr\u00f4le de la journalisation": [[118, "controle-de-la-journalisation"]], "Convention d\u00b4appellation de files d\u00b4attente": [[108, "convention-d-appellation-de-files-d-attente"]], "Convention d\u00b4appellation des EXCHANGES": [[108, "convention-d-appellation-des-exchanges"]], "Conventions": [[3, "conventions"], [93, "conventions"]], "Copie de fichiers": [[111, "copie-de-fichiers"]], "Copying Files": [[29, "copying-files"]], "Corr\u00e9spondance des concepts AMQP avec Sarracenia": [[105, "correspondance-des-concepts-amqp-avec-sarracenia"]], "Cost": [[45, "cost"]], "Co\u00fbt": [[122, "cout"]], "Creating the Queue": [[21, "creating-the-queue"]], "Credential Details": [[21, "credential-details"], [62, "credential-details"]], "Crochets de Sundew": [[80, "crochets-de-sundew"]], "Cr\u00e9ation d\u2019un programme d\u2019installation Windows": [[95, "creation-dun-programme-dinstallation-windows"]], "Customize File handling with Callbacks.": [[67, "Customize-File-handling-with-Callbacks."]], "Customizing Duplicate Suppression": [[49, "customizing-duplicate-suppression"]], "Customizing post_exchangeSplit": [[49, "customizing-post-exchangesplit"]], "DESCRIPTION": [[17, "description"], [40, "description"], [42, "description"], [43, "description"], [60, "description"], [61, "description"], [63, "description"], [64, "description"], [65, "description"], [99, "description"], [125, "description"], [126, "description"], [128, "description"], [129, "description"], [130, "description"]], "DESTFNSCRIPT": [[56, "destfnscript"], [89, "destfnscript"]], "DESTFNSCRIPTS": [[39, "destfnscripts"]], "DESTINATION SETUP 2 : METPX-SUNDEW LIKE DISSEMINATION": [[21, "destination-setup-2-metpx-sundew-like-dissemination"]], "Daily Builds": [[14, "daily-builds"]], "Dans le menu D\u00e9marrer :": [[139, "dans-le-menu-demarrer"]], "Data (bas\u00e9 uniquement sur le contenu)": [[120, "data-base-uniquement-sur-le-contenu"]], "Data Loss": [[55, "data-loss"]], "Data Oriented": [[25, "data-oriented"]], "Data Sources": [[54, "data-sources"]], "Databases are Optimal for Fixed Sized Records. No One Uses Them That Way": [[32, "databases-are-optimal-for-fixed-sized-records-no-one-uses-them-that-way"]], "Dataless Avec Sr_poll": [[106, "dataless-avec-sr-poll"]], "Dataless Pumps": [[27, "dataless-pumps"]], "Dataless Transfers": [[27, "dataless-transfers"]], "Dataless With Sr_poll": [[23, "dataless-with-sr-poll"]], "Dataless or S=0": [[23, "dataless-or-s-0"]], "Dataless ou S=0": [[106, "dataless-ou-s-0"]], "Dataless vann\u00e9e": [[106, "dataless-vannee"]], "De URP-1/2 annoncer \u00e0 BUNNY-OP qu\u2019un produit est pr\u00eat": [[79, "de-urp-1-2-annoncer-a-bunny-op-quun-produit-est-pret"]], "Debian/Ubuntu": [[3, "debian-ubuntu"], [93, "debian-ubuntu"]], "Debugging in callbacks": [[39, "debugging-in-callbacks"]], "Decoding Contents": [[48, "decoding-contents"]], "Definitions": [[8, "Definitions"]], "Delivery Completion (inflight)": [[26, "delivery-completion-inflight"]], "Deployment Considerations": [[23, "deployment-considerations"]], "Deployments/Use Cases": [[28, "deployments-use-cases"]], "Deprecated v2 plugins": [[21, "deprecated-v2-plugins"]], "Description des Tests sr_insects": [[93, "description-des-tests-sr-insects"]], "Description du protocole / format du message v02": [[116, "description-du-protocole-format-du-message-v02"]], "Description of the message v02 protocol / format": [[34, "description-of-the-message-v02-protocol-format"]], "Design Changes since Original (2015)": [[5, "design-changes-since-original-2015"]], "Design Decisions/Assumptions": [[9, "Design-Decisions/Assumptions"]], "Designs": [[23, "designs"]], "Detailles": [[95, "detailles"]], "Details": [[14, "details"]], "Details d\u2019Identifiants": [[108, "details-d-identifiants"]], "Detection Methods: Inotify, Policy, SHIM": [[29, "detection-methods-inotify-policy-shim"]], "Development": [[3, "development"]], "Dictionaries or Members for Properties?": [[20, "dictionaries-or-members-for-properties"]], "Dictionary Pre-Routing": [[12, "Dictionary-Pre-Routing"], [12, "id2"]], "Dictionnaires ou membres pour les propri\u00e9t\u00e9s ?": [[101, "dictionnaires-ou-membres-pour-les-proprietes"]], "Diff\u00e9rence de ligne de commande": [[89, "difference-de-ligne-de-commande"]], "Diff\u00e9rences de codage entre les plugins dans v2 vs Sr3": [[89, "differences-de-codage-entre-les-plugins-dans-v2-vs-sr3"]], "Diff\u00e9rences par rapport \u00e0 v02": [[117, "differences-par-rapport-a-v02"]], "Digression about ZSync": [[15, "digression-about-zsync"]], "Digression sur ZSync": [[97, "digression-sur-zsync"]], "Discussion of File Modification Propagation": [[15, "discussion-of-file-modification-propagation"]], "Discussion sur la propagation de la modification de fichiers": [[97, "discussion-sur-la-propagation-de-la-modification-de-fichiers"]], "Dispatcher": [[9, "Dispatcher"]], "Distributed Duplicate Suppression": [[11, "Distributed-Duplicate-Suppression"]], "Distributions Redhat/Suse (bas\u00e9es sur rpm)": [[136, "distributions-redhat-suse-basees-sur-rpm"]], "Do you already have it?": [[71, "do-you-already-have-it"]], "Documentation": [[3, "documentation"], [93, "documentation"]], "Documentation Standards": [[4, "documentation-standards"]], "Does it Work?": [[29, "does-it-work"]], "Done": [[20, "done"]], "DoneTodo": [[20, "donetodo"]], "Download Renaming": [[49, "download-renaming"]], "Downloading": [[48, "downloading"]], "Downloading Data with Python": [[69, "Downloading-Data-with-Python"]], "Downloading Using the Command Line": [[66, "Downloading-Using-the-Command-Line"]], "Duplicate Suppression": [[11, "Duplicate-Suppression"], [25, "duplicate-suppression"]], "Dynamic Flow": [[3, "dynamic-flow"], [93, "dynamic-flow"]], "Dynamic Flow Test Length": [[3, "dynamic-flow-test-length"]], "D\u00e9codage du contenu": [[84, "decodage-du-contenu"]], "D\u00e9finir la version": [[95, "definir-la-version"]], "D\u00e9marrage": [[80, "demarrage"], [137, "demarrage"]], "D\u00e9marrage et arr\u00eat du syst\u00e8me": [[136, "demarrage-et-arret-du-systeme"]], "D\u00e9ploiements/cas d\u2019utilisation": [[110, "deploiements-cas-dutilisation"]], "D\u00e9pots de pacquets Ubuntu: Launchpad.net": [[93, "depots-de-pacquets-ubuntu-launchpad-net"]], "D\u00e9p\u00f4ts et recettes": [[95, "depots-et-recettes"]], "D\u00e9veloppement": [[93, "developpement"]], "D\u2019o\u00f9 vient l\u2019AMQP?": [[90, "dou-vient-lamqp"]], "ENVIRONMENT VARIABLES": [[17, "environment-variables"], [61, "environment-variables"], [64, "environment-variables"]], "ERROR RECOVERY": [[21, "error-recovery"]], "EXAMPLE": [[65, "example"]], "EXAMPLES": [[21, "examples"], [60, "examples"]], "EXEMPLE": [[130, "exemple"]], "EXEMPLES": [[108, "exemples"], [125, "exemples"]], "Efficiency Considerations": [[54, "efficiency-considerations"]], "Eliminate Dispatcher": [[12, "Eliminate-Dispatcher"], [12, "id1"]], "Eliminate Log Locking": [[12, "Eliminate-Log-Locking"]], "Email Ingesting with Sarracenia (v2)": [[48, "email-ingesting-with-sarracenia-v2"]], "En-t\u00eates d\u2019utilisateur": [[87, "en-tetes-d-utilisateur"]], "En-t\u00eates facultatives": [[130, "en-tetes-facultatives"]], "Engins de transport": [[106, "engins-de-transport"]], "Enregistrement de r\u00e9vision": [[87, "enregistrement-de-revision"], [88, "enregistrement-de-revision"], [136, "enregistrement-de-revision"]], "Ensuring Things are Up": [[46, "ensuring-things-are-up"]], "Entretien m\u00e9nager - sr_audit": [[80, "entretien-menager-sr-audit"]], "Entry Points": [[49, "entry-points"]], "Entr\u00e9es de fichier de configuration et rappels": [[132, "Entr\u00e9es-de-fichier-de-configuration-et-rappels"]], "Entr\u00e9es de fichier de configuration pour utiliser Flow_Callbacks": [[83, "entrees-de-fichier-de-configuration-pour-utiliser-flow-callbacks"]], "Environment Variables": [[21, "environment-variables"], [108, "environment-variables"]], "Erreurs de configuration fr\u00e9quentes": [[104, "erreurs-de-configuration-frequentes"]], "Est-ce que \u00e7a marche?": [[111, "est-ce-que-ca-marche"]], "Est-ce rapide?": [[111, "est-ce-rapide"]], "Et une arborescence de r\u00e9pertoires convenue": [[114, "et-une-arborescence-de-repertoires-convenue"]], "Examples of Integrating APIs into Plugins": [[50, "examples-of-integrating-apis-into-plugins"]], "Examples of Non-Shared Approaches": [[8, "Examples-of-Non-Shared-Approaches"]], "Excess Queueing/Performance": [[46, "excess-queueing-performance"]], "Exc\u00e8s de fil d\u2019attente/performance": [[80, "exces-de-fil-d-attente-performance"]], "Executive Summary": [[32, "executive-summary"]], "Exemple de sous-classe Flowcb": [[83, "exemple-de-sous-classe-flowcb"], [132, "Exemple-de-sous-classe-Flowcb"]], "Exemple d\u2019API de flux": [[133, "Exemple-d\u2019API-de-flux"]], "Exemples d\u2019int\u00e9gration d\u2019API dans des plugins": [[82, "exemples-dintegration-dapi-dans-des-plugins"]], "Exigences minimales": [[80, "exigences-minimales"]], "Exigences sp\u00e9cifiques de consommation": [[108, "exigences-specifiques-de-consommation"]], "Explanation": [[4, "explanation"], [44, "explanation"]], "Explication": [[92, "explication"], [121, "explication"]], "Extending Classes": [[39, "extending-classes"]], "Extending Polling Protocols": [[48, "extending-polling-protocols"], [50, "extending-polling-protocols"], [84, "extending-polling-protocols"]], "Extension Ideas": [[39, "extension-ideas"]], "Extension des classes": [[118, "extension-des-classes"]], "Extension des protocoles de poll": [[82, "extension-des-protocoles-de-poll"]], "Extensions": [[21, "extensions"], [108, "extensions"]], "Ex\u00e9cuter un test de Flux": [[93, "executer-un-test-de-flux"]], "Ex\u00e9cution de MetPX via Docker": [[81, "execution-de-metpx-via-docker"]], "Ex\u00e9cution de Sarracenia avec une invite de commandes": [[139, "execution-de-sarracenia-avec-une-invite-de-commandes"]], "Ex\u00e9cution de Sarracenia sans invite de commandes": [[139, "execution-de-sarracenia-sans-invite-de-commandes"]], "Ex\u00e9cution de tests de Flux": [[93, "execution-de-tests-de-flux"]], "FICHIERS DE CONFIGURATION": [[108, "fichiers-de-configuration"]], "FINAL REMARKS ON ONE TO ONE FILTER": [[40, "final-remarks-on-one-to-one-filter"]], "FIXME/Deferred": [[20, "fixme-deferred"]], "FIXME/Diff\u00e9r\u00e9": [[101, "fixme-differe"]], "FLOW": [[21, "flow"], [108, "flow"]], "FTP": [[45, "ftp"], [122, "ftp"]], "FTPS": [[45, "ftps"], [122, "ftps"]], "Fait": [[101, "fait"]], "Features": [[20, "features"]], "Fewer Klocs, Better klocs": [[28, "fewer-klocs-better-klocs"]], "Fiabilit\u00e9 et sommes de contr\u00f4le": [[87, "fiabilite-et-sommes-de-controle"]], "Fiabilit\u00e9/Complexit\u00e9": [[122, "fiabilite-complexite"]], "Fichiers de configuration": [[89, "fichiers-de-configuration"]], "Fichiers journal et Suivi": [[108, "fichiers-journal-et-suivi"]], "Fichiers trop vieux": [[120, "fichiers-trop-vieux"]], "Fichiers volumineux": [[87, "fichiers-volumineux"]], "Field Replacements": [[21, "field-replacements"], [108, "field-replacements"]], "Fields in Messages": [[39, "fields-in-messages"]], "File Detection Strategies": [[24, "file-detection-strategies"], [119, "file-detection-strategies"]], "File Detection Strategy Table": [[24, "file-detection-strategy-table"]], "File Notification Without Downloading": [[39, "file-notification-without-downloading"]], "File Partitioning": [[21, "file-partitioning"]], "File Placement": [[56, "file-placement"]], "File Re-assembling": [[19, "file-re-assembling"]], "File d\u00b4attente trop important": [[88, "file-d-attente-trop-important"]], "Files That Change Too Often (mdelaylatest)": [[25, "files-that-change-too-often-mdelaylatest"]], "Files That are Too Old": [[25, "files-that-are-too-old"]], "First Subscribe": [[46, "first-subscribe"]], "Flakey Broker": [[3, "flakey-broker"], [93, "flakey-broker"]], "Flots Centrales": [[112, "flots-centrales"]], "Flow Callback Points": [[39, "flow-callback-points"]], "Flow Callback Poll Customization": [[39, "flow-callback-poll-customization"]], "Flow Callbacks": [[39, "flow-callbacks"]], "Flow Cleanup": [[3, "flow-cleanup"], [93, "flow-cleanup"]], "Flow Test Coinc\u00e9": [[93, "flow-test-coince"]], "Flow Test Stuck": [[3, "flow-test-stuck"]], "Flow Through Pumps": [[22, "flow-through-pumps"]], "Flow tests avec MQTT": [[93, "flow-tests-avec-mqtt"]], "Flow tests with MQTT": [[3, "flow-tests-with-mqtt"]], "FlowCallback Reference": [[58, "flowcallback-reference"]], "Flux de travail v2": [[93, "flux-de-travail-v2"]], "Flux de travail v3": [[93, "flux-de-travail-v3"]], "Flux programm\u00e9": [[89, "flux-programme"]], "Folder Structure": [[4, "folder-structure"]], "Fonctionnalit\u00e9: Plages d\u2019octets": [[122, "fonctionnalite-plages-doctets"]], "Fonctionnalit\u00e9s": [[101, "fonctionnalites"]], "Format de fichier de configuration SR3": [[128, "format-de-fichier-de-configuration-sr3"]], "Format/Protocole de messages d\u2019annonce Sarracenia": [[130, "format-protocole-de-messages-d-annonce-sarracenia"]], "Frais g\u00e9n\u00e9raux": [[111, "frais-generaux"]], "Frequent Configuration Errors": [[26, "frequent-configuration-errors"]], "From Anaconda Prompt:": [[74, "from-anaconda-prompt"]], "From URP-1/2 announce to BUNNY-OP that a product is ready": [[47, "from-urp-1-2-announce-to-bunny-op-that-a-product-is-ready"]], "From a Windows Powershell session:": [[74, "from-a-windows-powershell-session"]], "From the Start Menu:": [[74, "from-the-start-menu"]], "From the Task Scheduler:": [[74, "from-the-task-scheduler"]], "Functionality: Byte Ranges": [[45, "functionality-byte-ranges"]], "GOES-R Acquisition": [[30, "goes-r-acquisition"]], "GTS a \u00e9t\u00e9 con\u00e7u il y a longtemps": [[114, "gts-a-ete-concu-il-y-a-longtemps"]], "GTS est limit\u00e9 et inflexible": [[114, "gts-est-limite-et-inflexible"]], "GTS is Limited & Inflexible": [[32, "gts-is-limited-inflexible"]], "GTS was Designed Long Ago": [[32, "gts-was-designed-long-ago"]], "GUIDE DE MISE A NIVEAU": [[85, "guide-de-mise-a-niveau"]], "General Sarracenia Concepts": [[22, "general-sarracenia-concepts"]], "Gestion des utilisateurs d\u2019une pompe \u00e0 l\u2019aide de Sr_audit": [[80, "gestion-des-utilisateurs-d-une-pompe-a-l-aide-de-sr-audit"]], "Git Branches for Pre-release": [[14, "git-branches-for-pre-release"]], "Global Duplication Suppression": [[11, "Global-Duplication-Suppression"]], "Glossaire": [[106, "glossaire"], [107, "glossaire"]], "Glossary": [[23, "glossary"], [27, "glossary"]], "Guide De Ligne De Commande": [[108, "guide-de-ligne-de-commande"]], "Guide de Style": [[92, "guide-de-style"]], "Guide de l\u2019abonn\u00e9": [[88, "guide-de-labonne"]], "Guide de programmation sarracenia": [[118, "guide-de-programmation-sarracenia"]], "Guide du d\u00e9veloppeur MetPX-Sarracenia": [[93, "guide-du-developpeur-metpx-sarracenia"]], "HOWTOS": [[53, "howtos"]], "HPC Acquisition Feeds": [[30, "hpc-acquisition-feeds"]], "HPC Mirroring": [[30, "hpc-mirroring"], [112, "hpc-mirroring"]], "HPC mirrorring": [[25, "hpc-mirrorring"]], "HPCR Solution Overview": [[29, "hpcr-solution-overview"]], "HTTP Injection": [[54, "http-injection"]], "Hierarchical Settings": [[39, "hierarchical-settings"]], "High Performance Duplicate Suppression": [[55, "high-performance-duplicate-suppression"]], "High Priority Delivery": [[55, "high-priority-delivery"]], "High volume sample": [[3, "high-volume-sample"]], "Histoire": [[113, "histoire"]], "Histoire/Contexte de Sarracenia": [[110, "histoire-contexte-de-sarracenia"]], "History": [[31, "history"]], "History/Context of Sarracenia": [[28, "history-context-of-sarracenia"]], "Hooks from Sundew": [[46, "hooks-from-sundew"]], "Housekeeping - sr3 sanity": [[46, "housekeeping-sr3-sanity"]], "Housekeeping Metrics": [[55, "housekeeping-metrics"]], "How to Adopt AMQP": [[0, "how-to-adopt-amqp"]], "How to Select Chunksize": [[15, "how-to-select-chunksize"]], "How to setup a Remote Subscriber": [[73, "how-to-setup-a-remote-subscriber"]], "How2Guides": [[4, "how2guides"]], "How2Guides (Comment Faire)": [[92, "how2guides-comment-faire"]], "IDENTIFICATION (CREDENTIALS)": [[108, "identification-credentials"]], "INSTANCES": [[21, "instances"], [108, "instances"]], "IPv6": [[23, "ipv6"], [106, "ipv6"]], "Identifiants et Details": [[127, "identifiants-et-details"]], "Identity": [[21, "identity"], [108, "identity"]], "Id\u00e9e de Base": [[94, "idee-de-base"]], "Id\u00e9es d\u2019extension": [[118, "idees-dextension"]], "Implementations": [[38, "implementations"]], "Implementing POP/IMAP": [[48, "implementing-pop-imap"]], "Impl\u00e9mentations": [[103, "implementations"]], "Importation d\u2019extensions": [[108, "importation-dextensions"]], "Importing Extensions": [[21, "importing-extensions"]], "Incompatibilities": [[20, "incompatibilities"]], "Incompatibilit\u00e9s": [[101, "incompatibilites"]], "Indices and tables": [[142, "indices-and-tables"]], "Inflight Table": [[26, "inflight-table"]], "Ingestion par e-mail avec Sarracenia": [[84, "ingestion-par-e-mail-avec-sarracenia"]], "Init Integration": [[46, "init-integration"]], "Initialisation et param\u00e8tres": [[83, "initialisation-et-parametres"]], "Initialization and Settings": [[49, "initialization-and-settings"]], "Injecting Data into a MetPX-Sarracenia Pump Network": [[54, "injecting-data-into-a-metpx-sarracenia-pump-network"]], "Injection HTTP": [[87, "injection-http"]], "Injection SFTP": [[87, "injection-sftp"]], "Injection de donn\u00e9es dans un r\u00e9seau de pompes MetPX-Sarracenia": [[87, "injection-de-donnees-dans-un-reseau-de-pompes-metpx-sarracenia"]], "Inline Content in Messages": [[32, "inline-content-in-messages"]], "Input Validation": [[23, "input-validation"]], "Inqui\u00e9tudes abord\u00e9es": [[101, "inquietudes-abordees"]], "Install Servers on Workstation": [[3, "install-servers-on-workstation"]], "Installation": [[46, "installation"], [80, "installation"]], "Installation Client": [[136, "installation-client"]], "Installation Instructions": [[51, "installation-instructions"]], "Installation Rabbitmq": [[80, "installation-rabbitmq"]], "Installation de MetPX Sarracenia": [[136, "installation-de-metpx-sarracenia"]], "Installation d\u2019un RABBITMQ-SERVER": [[79, "installation-d-un-rabbitmq-server"]], "Installation locale": [[93, "installation-locale"]], "Installations avanc\u00e9es": [[80, "installations-avancees"]], "Installer des serveurs sur un poste de travail": [[93, "installer-des-serveurs-sur-un-poste-de-travail"]], "Instances": [[63, "instances"], [128, "instances"]], "Instructions d\u2019installation": [[85, "instructions-dinstallation"]], "Intel/Cray HPC Stack": [[0, "intel-cray-hpc-stack"]], "Internet Push est un mauvais ajustement pour les gros flux": [[114, "internet-push-est-un-mauvais-ajustement-pour-les-gros-flux"]], "Internet Push is a Poor Fit for Large Feeds": [[32, "internet-push-is-a-poor-fit-for-large-feeds"]], "Interrogation de sources externes": [[87, "interrogation-de-sources-externes"]], "Introduction": [[39, "introduction"], [39, "id1"], [47, "introduction"], [52, "introduction"], [55, "introduction"], [79, "introduction"], [81, "introduction"], [88, "introduction"], [118, "introduction"]], "Int\u00e9gration Init": [[80, "integration-init"]], "Int\u00e9gration LDAP": [[80, "integration-ldap"]], "Is it Fast?": [[29, "is-it-fast"]], "It\u00b4s About Time": [[6, "It\u00b4s-About-Time"]], "Journalisation": [[81, "journalisation"], [83, "journalisation"]], "Journalisation et d\u00e9bogage": [[88, "journalisation-et-debogage"]], "Journaux des messages": [[115, "journaux-des-messages"]], "Key Branches": [[3, "key-branches"]], "Known Problems (Solved in sr3)": [[20, "known-problems-solved-in-sr3"]], "LDAP Integration": [[46, "ldap-integration"]], "LES EN-T\u00caTES FIXES": [[130, "les-en-tetes-fixes"]], "LOGS and MONITORING": [[21, "logs-and-monitoring"]], "La ceuillette de messages": [[108, "la-ceuillette-de-messages"]], "La lecture de l\u2019arbre prend trop de temps": [[111, "la-lecture-de-larbre-prend-trop-de-temps"]], "Large Files": [[54, "large-files"]], "Large Numbers of Processors Need High Parallelism": [[7, "Large-Numbers-of-Processors-Need-High-Parallelism"]], "Latence (anglais: Latency)": [[107, "latence-anglais-latency"]], "Latency": [[27, "latency"]], "Launchpad": [[14, "launchpad"], [95, "launchpad"]], "Launchpad.net": [[95, "launchpad-net"]], "Le fichiers trop changeant (mdelaylatest)": [[120, "le-fichiers-trop-changeant-mdelaylatest"]], "Le flux \u00e0 travers des Pompes": [[105, "le-flux-a-travers-des-pompes"]], "Les bases de donn\u00e9es sont optimales pour les enregistrements de taille fixe. Personne ne les utilise de cette fa\u00e7on": [[114, "les-bases-de-donnees-sont-optimales-pour-les-enregistrements-de-taille-fixe-personne-ne-les-utilise-de-cette-facon"]], "Les champs override": [[83, "les-champs-override"]], "Let the Computer Do It": [[6, "Let-the-Computer-Do-It"]], "Liaisons": [[134, "Liaisons"]], "Limitation": [[3, "limitation"], [93, "limitation"]], "Listes De Travail": [[132, "Listes-De-Travail"]], "Listes de travail (Worklist)": [[118, "listes-de-travail-worklist"]], "Little Data": [[0, "little-data"]], "Livraison hautement prioritaire": [[88, "livraison-hautement-prioritaire"]], "Local Installation": [[3, "local-installation"]], "Local Pip install": [[3, "local-pip-install"]], "Local Python": [[3, "local-python"], [93, "local-python"]], "Localisation": [[92, "localisation"]], "Localization": [[4, "localization"]], "Log messages": [[33, "log-messages"]], "Logging": [[49, "logging"], [52, "logging"], [67, "Logging"]], "Logging Control": [[39, "logging-control"]], "Logging and Debugging": [[55, "logging-and-debugging"]], "Logs": [[60, "logs"], [125, "logs"]], "Longer Overview": [[38, "longer-overview"]], "Longueur du test Dynamic Flow": [[93, "longueur-du-test-dynamic-flow"]], "L\u2019algorithme de Flux": [[105, "l-algorithme-de-flux"]], "L\u2019avez-vous d\u00e9j\u00e0?": [[136, "lavez-vous-deja"]], "METHODOLOGY": [[42, "methodology"]], "MIGRATING FILTERS": [[42, "migrating-filters"]], "MIGRATING SENDER": [[42, "migrating-sender"]], "MQTT": [[27, "mqtt"], [107, "mqtt"]], "MQTT (version =5) Param\u00e8tres": [[105, "mqtt-version-5-parametres"]], "MQTT (version =5) Settings": [[22, "mqtt-version-5-settings"]], "MQTT Implementation Notes": [[18, "mqtt-implementation-notes"]], "Managing Users on a Pump Using Sr_audit": [[46, "managing-users-on-a-pump-using-sr-audit"]], "Mandatory Settings": [[64, "mandatory-settings"]], "Manual Process": [[14, "manual-process"]], "Manually Adding Users": [[46, "manually-adding-users"]], "Manuel de l\u2019utilisateur Windows": [[139, "manuel-de-lutilisateur-windows"]], "Mappage des concepts AMQP \u00e0 Sarracenia": [[90, "mappage-des-concepts-amqp-a-sarracenia"]], "Mappage des points d\u2019entr\u00e9e v2 aux Callbacks v3": [[89, "mappage-des-points-d-entree-v2-aux-callbacks-v3"]], "Mappage vers MQTT": [[130, "mappage-vers-mqtt"]], "Mappages": [[101, "mappages"]], "Mapping AMQP Concepts to Sarracenia": [[0, "mapping-amqp-concepts-to-sarracenia"], [22, "mapping-amqp-concepts-to-sarracenia"]], "Mapping to MQTT": [[65, "mapping-to-mqtt"]], "Mapping v2 Entry Points to v3 Callbacks": [[56, "mapping-v2-entry-points-to-v3-callbacks"]], "Mappings": [[20, "mappings"]], "Maturity": [[32, "maturity"]], "Maturit\u00e9": [[114, "maturite"]], "Meilleure r\u00e9ception des fichiers": [[118, "meilleure-reception-des-fichiers"]], "Mesh-Style Data Exchange for the WIS-GTS in 2019": [[32, "mesh-style-data-exchange-for-the-wis-gts-in-2019"]], "Message Gathering": [[21, "message-gathering"]], "Message v01 Format": [[33, "message-v01-format"], [115, "message-v01-format"]], "Messages": [[69, "Messages"], [134, "Messages"]], "Messages de Rapport (anglais: Report messages)": [[107, "messages-de-rapport-anglais-report-messages"]], "Messages de rapport": [[87, "messages-de-rapport"]], "Messages du CFG": [[115, "messages-du-cfg"]], "MetPX Sarracenia Wiki": [[77, "metpx-sarracenia-wiki"]], "MetPX-Sarracenia Developer\u2019s Guide": [[3, "metpx-sarracenia-developer-s-guide"]], "MetPX-Sarracenia Installation": [[71, "metpx-sarracenia-installation"]], "Minimum Requirements": [[46, "minimum-requirements"]], "Miroitage CHP": [[120, "miroitage-chp"]], "Mise en Place": [[137, "mise-en-place"]], "Mise en garde: Solution \u00e0 ce probl\u00e8me, pas tous les probl\u00e8mes": [[114, "mise-en-garde-solution-a-ce-probleme-pas-tous-les-problemes"]], "Mise en miroir continue": [[111, "mise-en-miroir-continue"]], "Mise en miroir continue de 27 millions d\u2019arborescence de fichiers tr\u00e8s rapidement": [[111, "mise-en-miroir-continue-de-27-millions-darborescence-de-fichiers-tres-rapidement"]], "Mise en \u0153uvre de POP/IMAP": [[84, "mise-en-oeuvre-de-pop-imap"]], "Mise \u00e0 jour du site Web sf.net": [[110, "mise-a-jour-du-site-web-sf-net"]], "Mise \u00e0 niveau de la configuration": [[89, "mise-a-niveau-de-la-configuration"]], "Modification de fichiers en transit": [[83, "modification-de-fichiers-en-transit"]], "Modifications apport\u00e9es pour cr\u00e9er la v03": [[117, "modifications-apportees-pour-creer-la-v03"]], "Modifications apport\u00e9es pour r\u00e9soudre les probl\u00e8mes": [[101, "modifications-apportees-pour-resoudre-les-problemes"]], "Modifications d\u2019application en 2017": [[112, "modifications-d-application-en-2017"]], "Modifier les valeurs par d\u00e9faut": [[80, "modifier-les-valeurs-par-defaut"]], "Modifying Files in Flight": [[49, "modifying-files-in-flight"]], "Moins de Klocs, de meilleurs Klocs": [[110, "moins-de-klocs-de-meilleurs-klocs"]], "More Information": [[55, "more-information"]], "Multiple processes: Instances, Singletons and VIP": [[22, "multiple-processes-instances-singletons-and-vip"], [105, "multiple-processes-instances-singletons-and-vip"]], "M\u00e9thodes de d\u00e9tection : Inotify, Policy, SHIM": [[111, "methodes-de-detection-inotify-policy-shim"]], "M\u00e9trique Housekeeping": [[88, "metrique-housekeeping"]], "NAMING EXCHANGES": [[21, "naming-exchanges"]], "NAMING QUEUES": [[21, "naming-queues"]], "NEWS FLASH: Serial 9x Faster than Parallel!": [[7, "NEWS-FLASH:-Serial-9x-Faster-than-Parallel!"]], "NOAA CO-OPS API": [[50, "noaa-co-ops-api"]], "Name (bas\u00e9 uniquement sur le nom)": [[120, "name-base-uniquement-sur-le-nom"]], "Name Oriented": [[25, "name-oriented"]], "Named Exchanges and Queues": [[0, "named-exchanges-and-queues"]], "Navire de Th\u00e9s\u00e9e": [[101, "navire-de-thesee"]], "Nettoyage": [[80, "nettoyage"], [137, "nettoyage"]], "Network Maps": [[27, "network-maps"]], "Non-Shared Approaches.": [[8, "Non-Shared-Approaches."]], "Normes de documentation": [[92, "normes-de-documentation"]], "Not Baked/Thinking": [[20, "not-baked-thinking"]], "Notes de mise en \u0153uvre MQTT": [[100, "notes-de-mise-en-oeuvre-mqtt"]], "Notification de fichier sans t\u00e9l\u00e9chargement": [[118, "notification-de-fichier-sans-telechargement"]], "Notifications for DD": [[47, "notifications-for-dd"]], "Notifications on DD": [[46, "notifications-on-dd"]], "Notifications pour DD": [[79, "notifications-pour-dd"]], "Notifications sur DD": [[80, "notifications-sur-dd"]], "N\u00e9cessite RABBITMQ > 3.3.3.x": [[80, "necessite-rabbitmq-3-3-3-x"]], "ONE TO MANY FILTER": [[40, "one-to-many-filter"]], "ONE TO ONE FILTER": [[40, "one-to-one-filter"]], "OPTION TYPES": [[63, "option-types"]], "OPTIONS": [[60, "options"], [63, "options"], [125, "options"], [128, "options"]], "OPTIONS DE COMPATIBILIT\u00c9 SUNDEW": [[108, "options-de-compatibilite-sundew"]], "OPTIONS SR3": [[128, "options-sr3"]], "Object Stores": [[8, "Object-Stores"]], "Observations": [[11, "Observations"]], "Observations:": [[9, "Observations:"]], "OpenStack": [[0, "openstack"], [90, "openstack"]], "Operations": [[46, "operations"]], "Optimiser la s\u00e9lection des fichiers par processus": [[88, "optimiser-la-selection-des-fichiers-par-processus"]], "Optimization Possibilities": [[65, "optimization-possibilities"]], "Optimize File Selection per Process": [[55, "optimize-file-selection-per-process"]], "Optional Headers": [[65, "optional-headers"]], "Options": [[56, "options"], [89, "options"]], "Options callback": [[108, "options-callback"]], "Options obligatoires": [[129, "options-obligatoires"]], "Op\u00e9rations": [[80, "operations"]], "Other Examples": [[49, "other-examples"], [67, "Other-Examples"], [132, "Other-Examples"]], "Other Parameters": [[0, "other-parameters"]], "Other Report Fields": [[65, "other-report-fields"]], "Outage": [[55, "outage"]], "Outillage": [[93, "outillage"]], "Overall Server Algorithmic Ceiling": [[11, "Overall-Server-Algorithmic-Ceiling"]], "Overheads": [[29, "overheads"]], "Override Fields": [[49, "override-fields"]], "Overview": [[38, "overview"], [46, "overview"]], "O\u00f9 documenter les options": [[93, "ou-documenter-les-options"]], "PDS Algorithmic Design": [[9, "PDS-Algorithmic-Design"]], "PDS-OP receptions of dispatch notification messages, wget of radar products": [[47, "pds-op-receptions-of-dispatch-notification-messages-wget-of-radar-products"]], "PERIODIC PROCESSING": [[21, "periodic-processing"]], "PIP": [[71, "id1"], [136, "id1"]], "POLL": [[21, "poll"], [108, "poll"]], "POLLING": [[21, "polling"]], "POST or WATCH": [[21, "post-or-watch"], [108, "post-or-watch"]], "POSTING": [[21, "posting"]], "PUBLICATION (POST)": [[108, "publication-post"]], "Packages": [[71, "packages"]], "Panne trop longue": [[88, "panne-trop-longue"]], "Paquets": [[136, "paquets"]], "Parallel": [[20, "parallel"], [101, "parallel"]], "Param\u00e8tres": [[118, "parametres"]], "Param\u00e8tres hi\u00e9rarchiques": [[118, "parametres-hierarchiques"]], "Partitionnement de fichiers": [[108, "partitionnement-de-fichiers"]], "Pas cuit / \u00c0 penser": [[101, "pas-cuit-a-penser"]], "Pas de FTP ?": [[103, "pas-de-ftp"]], "Performance": [[55, "performance"], [88, "performance"]], "Periodic Processing/Cron Jobs": [[71, "periodic-processing-cron-jobs"]], "Personnalisation de la suppression des doublons": [[83, "personnalisation-de-la-suppression-des-doublons"]], "Personnalisation de post_exchangeSplit": [[83, "personnalisation-de-post-exchangesplit"]], "Personnalisation du Callback de Flux de Poll": [[118, "personnalisation-du-callback-de-flux-de-poll"]], "Personnalisez la gestion des fichiers avec les rappels.": [[132, "Personnalisez-la-gestion-des-fichiers-avec-les-rappels."]], "Perte de donn\u00e9es": [[88, "perte-de-donnees"]], "Peu de donn\u00e9es": [[90, "peu-de-donnees"]], "Pile Intel/Cray HPC": [[90, "pile-intel-cray-hpc"]], "Pip install locale": [[93, "pip-install-locale"]], "Placement de Fichier": [[89, "placement-de-fichier"]], "Plan concret (Fait)": [[101, "plan-concret-fait"]], "Plans de Pompes": [[106, "plans-de-pompes"]], "Plugins": [[55, "plugins"], [88, "plugins"]], "Plugins That Change How a File is Downloaded": [[67, "Plugins-That-Change-How-a-File-is-Downloaded"]], "Plugins That Create New Files": [[67, "Plugins-That-Create-New-Files"]], "Plugins qui Cr\u00e9ent de Nouveaux Fichiers": [[132, "Plugins-qui-Cr\u00e9ent-de-Nouveaux-Fichiers"]], "Plugins qui Traitent un Fichier apr\u00e8s son T\u00e9l\u00e9chargement": [[132, "Plugins-qui-Traitent-un-Fichier-apr\u00e8s-son-T\u00e9l\u00e9chargement"]], "Plugins qui changent la fa\u00e7on dont un fichier est t\u00e9l\u00e9charg\u00e9": [[132, "Plugins-qui-changent-la-fa\u00e7on-dont-un-fichier-est-t\u00e9l\u00e9charg\u00e9"]], "Plugins qui renomment les fichiers": [[132, "Plugins-qui-renomment-les-fichiers"]], "Plugins that Process a file after it is Downloaded": [[67, "Plugins-that-Process-a-file-after-it-is-Downloaded"]], "Plugins that Rename Files": [[67, "Plugins-that-Rename-Files"]], "Plugins v2 Obsol\u00e8tes": [[108, "plugins-v2-obsoletes"]], "Plus d\u2019informations": [[88, "plus-dinformations"]], "Points de rappel de flux": [[118, "points-de-rappel-de-flux"]], "Points d\u2019entr\u00e9e": [[83, "points-dentree"]], "Polling": [[39, "polling"], [48, "polling"], [84, "polling"], [118, "polling"]], "Polling External Sources": [[54, "polling-external-sources"]], "Polling Protocols Natively Supported": [[50, "polling-protocols-natively-supported"]], "Pompe": [[107, "pompe"]], "Pompes sans Donn\u00e9es (anglais: Dataless Pumps)": [[107, "pompes-sans-donnees-anglais-dataless-pumps"]], "Portage des plugins V2 vers Sr3": [[89, "portage-des-plugins-v2-vers-sr3"]], "Porting V2 Plugins to Sr3": [[56, "porting-v2-plugins-to-sr3"]], "Possibilit\u00e9s d\u2019optimisation": [[130, "possibilites-doptimisation"]], "Post, Notice, Notification, Advertisement, Announcement": [[27, "post-notice-notification-advertisement-announcement"]], "Poste, Notice, Notification, Avis, Annonce": [[107, "poste-notice-notification-avis-annonce"]], "Posting from Python Code": [[70, "Posting-from-Python-Code"]], "Pourquoi SFTP est plus souvent choisi que FTPS": [[122, "pourquoi-sftp-est-plus-souvent-choisi-que-ftps"]], "Pourquoi l\u2019API v3 doit \u00eatre utilis\u00e9e dans la mesure du possible": [[118, "pourquoi-lapi-v3-doit-etre-utilisee-dans-la-mesure-du-possible"]], "Pourquoi ne pas simplement utiliser Rsync ?": [[103, "pourquoi-ne-pas-simplement-utiliser-rsync"]], "Pourquoi utiliser AMQP?": [[90, "pourquoi-utiliser-amqp"]], "Pre-Release Overview": [[14, "pre-release-overview"]], "Pre-Requisites": [[46, "pre-requisites"]], "Premier abonnement": [[80, "premier-abonnement"]], "Prepare a Vanilla VM": [[3, "prepare-a-vanilla-vm"]], "Prerequisites": [[66, "Prerequisites"]], "Priorities": [[32, "priorities"]], "Priorit\u00e9s": [[114, "priorites"]], "Privileged System Access": [[23, "privileged-system-access"]], "Problem Statement": [[29, "problem-statement"]], "Problems/Weaknesses of Sundew:": [[11, "Problems/Weaknesses-of-Sundew:"]], "Probl\u00e8mes connus (r\u00e9solus dans sr3)": [[101, "problemes-connus-resolus-dans-sr3"]], "Process": [[4, "process"]], "Processing": [[4, "processing"]], "Processus": [[92, "processus"]], "Processus de Pre-Publication": [[95, "processus-de-pre-publication"]], "Processus de Publication de Version Stable": [[95, "processus-de-publication-de-version-stable"]], "Processus de Renommage": [[119, "processus-de-renommage"]], "Processus manuel": [[95, "processus-manuel"]], "Product Exchangers: Supporting Large Numbers of Transfers": [[45, "product-exchangers-supporting-large-numbers-of-transfers"]], "Programmability/Interoperability": [[32, "programmability-interoperability"]], "Programmabilit\u00e9/interop\u00e9rabilit\u00e9": [[114, "programmabilite-interoperabilite"]], "Project Website": [[28, "project-website"]], "Proof": [[8, "Proof"]], "Protocoles de polling pris en charge en mode natif": [[82, "protocoles-de-polling-pris-en-charge-en-mode-natif"]], "Proxys Web": [[88, "proxys-web"]], "Pr\u00e9-requis": [[80, "pre-requis"]], "Pr\u00e9parer une machine virtuelle Vanilla": [[93, "preparer-une-machine-virtuelle-vanilla"]], "Pr\u00e9requis": [[131, "Pr\u00e9requis"]], "Pr\u00e9sentation de la solution HPCR": [[111, "presentation-de-la-solution-hpcr"]], "Publication \u00e0 partir du code Python": [[135, "Publication-\u00e0-partir-du-code-Python"]], "Publie la Disponibilit\u00e9e d\u2019un fichier aux abonn\u00e9s.": [[129, "publie-la-disponibilitee-d-un-fichier-aux-abonnes"]], "Publier une Version de MetPX-Sarracenia": [[95, "publier-une-version-de-metpx-sarracenia"]], "Publish the Availability of Files": [[64, "publish-the-availability-of-files"]], "Pump": [[27, "pump"]], "Pumping Network": [[27, "pumping-network"]], "PyPi": [[14, "pypi"], [95, "pypi"]], "Python Wheel": [[3, "python-wheel"], [93, "python-wheel"]], "QUEUES and MULTIPLE STREAMS": [[21, "queues-and-multiple-streams"], [108, "queues-and-multiple-streams"]], "Quality Assurance": [[14, "quality-assurance"]], "Quand signaler": [[93, "quand-signaler"]], "Que se passe-t-il ?": [[80, "que-se-passe-t-il"]], "Que se passe-t-il si chaque notification concerne un bloc, pas un fichier ?": [[97, "que-se-passe-t-il-si-chaque-notification-concerne-un-bloc-pas-un-fichier"]], "Quickly Announcing Very Large Trees On Linux": [[54, "quickly-announcing-very-large-trees-on-linux"], [87, "quickly-announcing-very-large-trees-on-linux"]], "Quorum and Voting:": [[8, "Quorum-and-Voting:"]], "RABBITMQ-SERVER cluster installation": [[47, "rabbitmq-server-cluster-installation"]], "RABBITMQ-SERVER installation": [[47, "rabbitmq-server-installation"]], "RABBITMQ-SERVER ldap installation": [[47, "rabbitmq-server-ldap-installation"]], "RADAR": [[112, "radar"]], "RADAR Data Flows": [[30, "radar-data-flows"]], "RELATED CLIENTS": [[42, "related-clients"]], "ROLES - feeder/admin/declare": [[21, "roles-feeder-admin-declare"], [108, "roles-feeder-admin-declare"]], "Rabbitmq Setup": [[46, "rabbitmq-setup"]], "Rappels de Flux (Flow Callbacks)": [[118, "rappels-de-flux-flow-callbacks"]], "Rapports": [[80, "rapports"]], "Reading the Tree Takes Too Long": [[29, "reading-the-tree-takes-too-long"]], "Receiver": [[9, "Receiver"]], "Receiving Data from a MetPX-Sarracenia Data Pump": [[55, "receiving-data-from-a-metpx-sarracenia-data-pump"]], "Redhat/Suse Distros (rpm based)": [[71, "redhat-suse-distros-rpm-based"]], "Redundant File Reception": [[55, "redundant-file-reception"]], "Refactorisation de la version 3": [[101, "refactorisation-de-la-version-3"]], "Reference": [[4, "reference"], [59, "reference"]], "References & Links": [[38, "references-links"]], "Refining Selection": [[55, "refining-selection"]], "Releasing MetPX-Sarracenia": [[14, "releasing-metpx-sarracenia"]], "Relevance?": [[7, "Relevance?"]], "Reliability and Checksums": [[54, "reliability-and-checksums"]], "Reliability/Complexity": [[45, "reliability-complexity"]], "Remote Configurations": [[21, "remote-configurations"], [60, "remote-configurations"], [108, "remote-configurations"]], "Remplacement": [[101, "remplacement"]], "Rename Processing": [[24, "rename-processing"], [64, "rename-processing"]], "Renommage dd_subscribe": [[110, "renommage-dd-subscribe"]], "Repeated Scans and VIP": [[21, "repeated-scans-and-vip"]], "Replacement": [[20, "replacement"]], "Report Messages": [[54, "report-messages"], [65, "report-messages"], [130, "report-messages"]], "Report messages": [[27, "report-messages"]], "Report_Code": [[65, "report-code"], [130, "report-code"]], "Reports": [[46, "reports"]], "Repositories": [[3, "repositories"]], "Repositories & Recipes": [[14, "repositories-recipes"]], "Requires RABBITMQ > 3.3.x": [[46, "requires-rabbitmq-3-3-x"]], "Ressources c\u00f4t\u00e9 serveur allou\u00e9es aux abonn\u00e9s": [[88, "ressources-cote-serveur-allouees-aux-abonnes"]], "Review": [[0, "review"]], "Revision Record": [[39, "revision-record"], [54, "revision-record"], [55, "revision-record"], [71, "revision-record"]], "Roll Your Own": [[25, "roll-your-own"]], "Routage": [[80, "routage"]], "Routing": [[46, "routing"]], "Routing 1 Product": [[9, "Routing-1-Product"]], "Run A Flow Test": [[3, "run-a-flow-test"]], "Running Flow Tests": [[3, "running-flow-tests"]], "Running MetPX via Docker": [[52, "running-metpx-via-docker"]], "Running Sarracenia with a Command Prompt": [[74, "running-sarracenia-with-a-command-prompt"]], "Running Sarracenia without a Command Prompt": [[74, "running-sarracenia-without-a-command-prompt"]], "R\u00c9CUP\u00c9RATION D\u2019ERREUR": [[108, "recuperation-derreur"]], "R\u00e9assemblage de fichiers": [[96, "reassemblage-de-fichiers"]], "R\u00e9ception avanc\u00e9e des fichiers": [[118, "reception-avancee-des-fichiers"]], "R\u00e9ception de donn\u00e9es \u00e0 partir d\u2019une pompe de donn\u00e9es MetPX-Sarracenia": [[88, "reception-de-donnees-a-partir-dune-pompe-de-donnees-metpx-sarracenia"]], "R\u00e9ception de fichiers redondants": [[88, "reception-de-fichiers-redondants"]], "R\u00e9ceptions PDS-OP de messages de notification de r\u00e9partition, wget de produits radar": [[79, "receptions-pds-op-de-messages-de-notification-de-repartition-wget-de-produits-radar"]], "R\u00e9f\u00e9rence": [[92, "reference"], [124, "reference"]], "R\u00e9f\u00e9rences et liens": [[103, "references-et-liens"]], "R\u00e9glage de Broker": [[108, "reglage-de-broker"]], "R\u00e9glage du d\u00e9bogage flowcb/log.py": [[88, "reglage-du-debogage-flowcb-log-py"]], "R\u00e9glage du d\u00e9bogage moth": [[88, "reglage-du-debogage-moth"]], "R\u00e9seau de Pompage (anglais: Pumping Network)": [[107, "reseau-de-pompage-anglais-pumping-network"]], "R\u00e9sum\u00e9": [[101, "resume"], [111, "resume"], [114, "resume"], [122, "resume"]], "R\u00e9troportage d\u2019une d\u00e9pendance": [[95, "retroportage-dune-dependance"]], "R\u00e9vision": [[90, "revision"]], "R\u00e9vision de l\u2019Accel": [[101, "revision-de-laccel"]], "SARRA": [[21, "sarra"], [108, "sarra"]], "SEE ALSO": [[17, "see-also"], [42, "see-also"], [60, "see-also"], [61, "see-also"], [62, "see-also"], [63, "see-also"], [64, "see-also"], [65, "see-also"], [128, "see-also"]], "SENDER": [[21, "sender"], [108, "sender"]], "SETUP": [[43, "setup"]], "SETUP 1 : PUMP TO PUMP REPLICATION": [[21, "setup-1-pump-to-pump-replication"]], "SFTP Injection": [[54, "sftp-injection"]], "SHC SOAP Web Service": [[50, "shc-soap-web-service"]], "SHIM LIBRARY USAGE": [[24, "shim-library-usage"], [64, "shim-library-usage"], [119, "shim-library-usage"]], "SHOVEL": [[21, "shovel"], [108, "shovel"]], "SONDAGE (POLLING)": [[108, "sondage-polling"]], "SR3": [[60, "sr3"], [66, "SR3"], [125, "sr3"], [131, "SR3"]], "SR3 - Everything": [[21, "sr3-everything"]], "SR3 - Tout": [[108, "sr3-tout"]], "SR3 CREDENTIALS": [[62, "sr3-credentials"], [127, "sr3-credentials"]], "SR3 Configuration File Format": [[63, "sr3-configuration-file-format"]], "SR3 Credential File Format": [[62, "sr3-credential-file-format"]], "SR3 Credential: Format du Fichier": [[127, "sr3-credential-format-du-fichier"]], "SR3 OPTIONS": [[63, "sr3-options"]], "SR3_TITLE": [[17, "sr3-title"]], "SR3_TITRE": [[99, "sr3-titre"]], "SR_CPUMP": [[61, "sr-cpump"], [126, "sr-cpump"]], "SR_DEV_APPNAME": [[63, "sr-dev-appname"], [128, "sr-dev-appname"]], "SR_POLL CONFIG": [[42, "sr-poll-config"]], "SR_SARRA CONFIG": [[42, "sr-sarra-config"]], "SR_post": [[65, "sr-post"], [130, "sr-post"]], "STATUS: Stable/Default": [[65, "status-stable-default"], [130, "status-stable-default"]], "STATUS: WIP": [[6, "STATUS:-WIP"]], "SUBSCRIBE": [[21, "subscribe"], [108, "subscribe"]], "SUNDEW COMPATIBILITY OPTIONS": [[21, "sundew-compatibility-options"]], "SUNDEW SENDER CONVERSION PROCESS": [[43, "sundew-sender-conversion-process"]], "SYNOPSIS": [[17, "synopsis"], [60, "synopsis"], [61, "synopsis"], [63, "synopsis"], [64, "synopsis"], [65, "synopsis"], [99, "synopsis"], [125, "synopsis"], [126, "synopsis"], [128, "synopsis"], [129, "synopsis"], [130, "synopsis"]], "Sample Extensions": [[39, "sample-extensions"], [118, "sample-extensions"]], "Sample Flow Callback Class": [[67, "Sample-Flow-Callback-Class"]], "Sample Flowcb Sub-Class": [[49, "sample-flowcb-sub-class"], [67, "Sample-Flowcb-Sub-Class"]], "Sarra D\u2019une source": [[80, "sarra-d-une-source"]], "Sarra From a Source": [[46, "sarra-from-a-source"]], "Sarra d\u2019une autre pompe": [[80, "sarra-d-une-autre-pompe"]], "Sarra from Another Pump": [[46, "sarra-from-another-pump"]], "Sarrac": [[71, "sarrac"], [136, "sarrac"]], "Sarracenia": [[78, "sarracenia"]], "Sarracenia Algorithmic Design": [[11, "Sarracenia-Algorithmic-Design"]], "Sarracenia Design Philosophy": [[13, "sarracenia-design-philosophy"]], "Sarracenia Janvier 2018": [[112, "sarracenia-janvier-2018"]], "Sarracenia Programming Guide": [[39, "sarracenia-programming-guide"]], "Sarracenia Status January 2018": [[30, "sarracenia-status-january-2018"]], "Sarracenia est une application MQP": [[90, "sarracenia-est-une-application-mqp"]], "Sarracenia is an MQP Application": [[0, "sarracenia-is-an-mqp-application"]], "Sarracenia v03 Notification Message Format/Protocol": [[65, "sarracenia-v03-notification-message-format-protocol"]], "Sarracenia.FlowCB": [[78, "sarracenia-flowcb"]], "Sarracenia.Moth": [[78, "sarracenia-moth"]], "Sarracenia: File Systems Flying in Formation": [[8, "Sarracenia:-File-Systems-Flying-in-Formation"]], "Scans r\u00e9p\u00e9t\u00e9s et VIP": [[108, "scans-repetes-et-vip"]], "Scheduled Flow": [[56, "scheduled-flow"]], "Sch\u00e9ma de contr\u00f4le de version": [[95, "schema-de-controle-de-version"]], "Security Considerations": [[23, "security-considerations"]], "Security/Vulnerability": [[45, "security-vulnerability"]], "Sender": [[9, "Sender"]], "Serial Example": [[7, "Serial-Example"]], "Server Admin: A Local Subscriber": [[72, "server-admin-a-local-subscriber"]], "Server-Side Resources Allocated for Subscribers": [[55, "server-side-resources-allocated-for-subscribers"]], "Server/Broker": [[23, "server-broker"]], "Server/Protocol Considerations": [[15, "server-protocol-considerations"]], "Serveur/courtier": [[106, "serveur-courtier"]], "Service Web SHC SOAP": [[82, "service-web-shc-soap"]], "Service Web de valeurs instantan\u00e9es USGS": [[82, "service-web-de-valeurs-instantanees-usgs"]], "Set the Version": [[14, "set-the-version"]], "Setting the Broker": [[21, "setting-the-broker"]], "Settings": [[39, "settings"]], "Setup": [[73, "setup"]], "Setup Flow Test Environment": [[3, "setup-flow-test-environment"]], "Shared Subscriptions": [[18, "shared-subscriptions"]], "Shim Library": [[29, "shim-library"]], "Shim Library Necessary": [[29, "shim-library-necessary"]], "Shim Usage Notes": [[24, "shim-usage-notes"], [119, "shim-usage-notes"]], "Shim Usage Tips": [[64, "shim-usage-tips"]], "Ship of Theseus": [[20, "ship-of-theseus"]], "Si nous faisons des cksums par blocs, chemin \u00e0 partir de v00": [[97, "si-nous-faisons-des-cksums-par-blocs-chemin-a-partir-de-v00"]], "Simple/Scalable Peer Configurations for Nations": [[32, "simple-scalable-peer-configurations-for-nations"]], "Site web du projet": [[110, "site-web-du-projet"]], "So Sarracenia is Not a Parallel App!": [[7, "So-Sarracenia-is-Not-a-Parallel-App!"]], "Sondage avanc\u00e9 (Advanced Polling)": [[108, "sondage-avance-advanced-polling"]], "Sortie d\u2019Ex\u00e9cution": [[132, "Sortie-d\u2019Ex\u00e9cution"]], "Source": [[27, "source"], [71, "source"], [107, "source"], [136, "source"]], "Sources de donn\u00e9es": [[87, "sources-de-donnees"]], "Specific Consuming Requirements": [[21, "specific-consuming-requirements"]], "Speedup": [[7, "Speedup"]], "Sr3_Post": [[64, "sr3-post"], [129, "sr3-post"]], "Sr_Poll": [[46, "sr-poll"], [80, "sr-poll"]], "Sr_sender": [[46, "sr-sender"], [80, "sr-sender"]], "Sr_winnow": [[46, "sr-winnow"], [80, "sr-winnow"]], "Stable Release Process": [[14, "stable-release-process"]], "Standalone": [[23, "standalone"]], "Standard (bas\u00e9 sur le chemin et contenu)": [[120, "standard-base-sur-le-chemin-et-contenu"]], "Standard (path and data oriented)": [[25, "standard-path-and-data-oriented"]], "Standards": [[65, "standards"], [130, "standards"]], "Startup": [[46, "startup"], [73, "startup"]], "State of the Code": [[20, "state-of-the-code"]], "Statelessness/Crawlable": [[32, "statelessness-crawlable"]], "Static Flow": [[3, "static-flow"], [93, "static-flow"]], "Storage/State": [[8, "Storage/State"]], "Store And Forward est souvent meilleur dans la pratique": [[114, "store-and-forward-est-souvent-meilleur-dans-la-pratique"]], "Store And Forward is Often Better in Practice": [[32, "store-and-forward-is-often-better-in-practice"]], "Structure des dossiers": [[92, "structure-des-dossiers"]], "Style Guide": [[4, "style-guide"]], "Style de Code": [[92, "style-de-code"]], "Subclassing Flow": [[49, "subclassing-flow"]], "Subscribe and Replicate 3": [[142, "subscribe-and-replicate-3"]], "Subscriber Guide": [[55, "subscriber-guide"]], "Subscribers": [[27, "subscribers"]], "Substitutions Compatible Sundew": [[128, "substitutions-compatible-sundew"]], "Summary": [[7, "Summary"], [20, "summary"], [29, "summary"], [45, "summary"]], "Sundew": [[27, "sundew"], [107, "sundew"]], "Sundew Algorithmic Design": [[12, "Sundew-Algorithmic-Design"]], "Sundew Compatible Substituions": [[63, "sundew-compatible-substituions"]], "Sundew Migration Guide": [[41, "sundew-migration-guide"]], "Sundew Routing cost:": [[12, "Sundew-Routing-cost:"]], "Sundew filter migration to sarracenia (PXATX)": [[40, "sundew-filter-migration-to-sarracenia-pxatx"]], "Sundew pull migration to sarracenia (PXATX)": [[42, "sundew-pull-migration-to-sarracenia-pxatx"]], "Sundew sender migration to sarracenia (PXATX)": [[43, "sundew-sender-migration-to-sarracenia-pxatx"]], "Support": [[46, "support"], [80, "support"]], "Suppression de Doublons": [[120, "suppression-de-doublons"]], "Suppression des doublons haute performance": [[88, "suppression-des-doublons-haute-performance"]], "Switching/Routing": [[23, "switching-routing"]], "System Startup and Shutdown": [[71, "system-startup-and-shutdown"]], "S\u00b4abonner et r\u00e9pliquer": [[141, "s-abonner-et-repliquer"]], "S\u00e9curit\u00e9/Vuln\u00e9rabilit\u00e9": [[122, "securite-vulnerabilite"]], "S\u00e9lection des fonctionnalit\u00e9s AMQP": [[90, "selection-des-fonctionnalites-amqp"]], "S\u2019assurer que les choses sont en place": [[80, "sassurer-que-les-choses-sont-en-place"]], "TESTING": [[42, "testing"]], "THANKS!": [[7, "THANKS!"]], "THE FIXED HEADERS": [[65, "the-fixed-headers"]], "TH\u00c8ME (TOPIC)": [[130, "theme-topic"]], "TLS": [[46, "tls"], [80, "tls"]], "TODO": [[20, "todo"], [101, "id1"]], "TOPIC": [[65, "topic"]], "TRAITEMENT P\u00c9RIODIQUE": [[108, "traitement-periodique"]], "TYPES D\u2019OPTIONS": [[128, "types-d-options"]], "Table 1: The Algorithm for All Components": [[22, "id1"]], "Table 2: How Each Component Uses the Flow Algorithm": [[22, "id2"]], "Table of v2 and sr3 Equivalents": [[56, "table-of-v2-and-sr3-equivalents"], [89, "table-of-v2-and-sr3-equivalents"]], "Tableau 1 : Algorithme pour tous les composants": [[105, "id1"]], "Tableau 2 : Utilisation de l\u2019algorithme de flux par chaque composant": [[105, "id2"]], "Tableau de Inflight": [[104, "tableau-de-inflight"]], "Tableau de strat\u00e9gie de d\u00e9tection de fichiers": [[119, "tableau-de-strategie-de-detection-de-fichiers"]], "Test": [[96, "test"]], "Testing": [[19, "testing"]], "Thanks!": [[8, "Thanks!"]], "The Algorithmic Cost to Route 1 File": [[11, "The-Algorithmic-Cost-to-Route-1-File"]], "The Flow Algorithm": [[22, "the-flow-algorithm"], [39, "the-flow-algorithm"]], "The Simplest Flow_Callback": [[39, "the-simplest-flow-callback"], [118, "the-simplest-flow-callback"]], "ToDo": [[101, "todo"]], "Too slow, Queue Too Large": [[55, "too-slow-queue-too-large"]], "Tools you Need": [[3, "tools-you-need"]], "Topic-based Exchanges": [[0, "topic-based-exchanges"]], "Traduction de terminologie": [[123, "traduction-de-terminologie"]], "Traitement": [[92, "traitement"]], "Traitement IP virtuel dans le poll": [[89, "traitement-ip-virtuel-dans-le-poll"]], "Traitement de renommage": [[129, "traitement-de-renommage"]], "Traitement p\u00e9riodique/T\u00e2ches Cron": [[136, "traitement-periodique-taches-cron"]], "Transfer": [[21, "transfer"], [108, "transfer"]], "Transferts sans Donn\u00e9es (Dataless Transfers)": [[107, "transferts-sans-donnees-dataless-transfers"]], "Transition": [[20, "transition"], [101, "transition"]], "Transport Engines": [[23, "transport-engines"]], "Travailler avec des plugins": [[118, "travailler-avec-des-plugins"]], "Troubleshooting": [[14, "troubleshooting"]], "Trucs d\u2019utilisation de shim": [[129, "trucs-d-utilisation-de-shim"]], "Tutorials": [[4, "tutorials"], [75, "tutorials"]], "Tutoriel": [[140, "tutoriel"]], "Tutoriels": [[92, "tutoriels"]], "T\u00e9l\u00e9chargement": [[84, "telechargement"]], "T\u00e9l\u00e9chargement en utilisant la console": [[131, "T\u00e9l\u00e9chargement-en-utilisant-la-console"]], "T\u00e9l\u00e9charger des donn\u00e9es avec Python": [[134, "T\u00e9l\u00e9charger-des-donn\u00e9es-avec-Python"]], "UPGRADE GUIDE": [[51, "upgrade-guide"]], "URP": [[25, "urp"], [120, "urp"]], "USGS Instantaneous Values Web Service": [[50, "usgs-instantaneous-values-web-service"]], "UTILISATION DE LA LIBRAIRIE SHIM": [[129, "utilisation-de-la-librairie-shim"]], "Ubuntu 18.04": [[3, "ubuntu-18-04"], [95, "ubuntu-18-04"]], "Ubuntu/Debian (apt/dpkg) Recommand\u00e9": [[136, "ubuntu-debian-apt-dpkg-recommande"]], "Ubuntu/Debian (apt/dpkg) Recommended": [[71, "ubuntu-debian-apt-dpkg-recommended"]], "Un Autre Exemple": [[130, "un-autre-exemple"]], "Un premier exemple utilisant l\u2019API Sarracenia Moth": [[134, "Un-premier-exemple-utilisant-l\u2019API-Sarracenia-Moth"]], "Unit": [[3, "unit"], [93, "unit"]], "Universal Write Scaling": [[8, "Universal-Write-Scaling"]], "Updating The sf.net Website": [[28, "updating-the-sf-net-website"]], "Use Case": [[48, "use-case"], [50, "use-case"]], "Use Folders/Directories": [[11, "Use-Folders/Directories"]], "Use Instances": [[55, "use-instances"]], "Use of AMQP on DD (DDI, DD.BETA)": [[47, "use-of-amqp-on-dd-ddi-dd-beta"]], "User Headers": [[54, "user-headers"]], "Users and Roles": [[22, "users-and-roles"]], "Using AMQP with URP, BUNNY, PDS-OP": [[47, "using-amqp-with-urp-bunny-pds-op"]], "Using An Open Reference Stack": [[32, "using-an-open-reference-stack"]], "Using Credentials in Plugins": [[39, "using-credentials-in-plugins"]], "Using Plugins to Grab Hydrometric Data (v2)": [[50, "using-plugins-to-grab-hydrometric-data-v2"]], "Utilisateurs et r\u00f4les": [[105, "utilisateurs-et-roles"]], "Utilisation de l\u2019AMQP sur DD (DDI, DD.BETA)": [[79, "utilisation-de-lamqp-sur-dd-ddi-dd-beta"]], "Utilisation de plugins pour r\u00e9cup\u00e9rer des donn\u00e9es hydrom\u00e9triques": [[82, "utilisation-de-plugins-pour-recuperer-des-donnees-hydrometriques"]], "Utilisation de plusieurs configurations": [[88, "utilisation-de-plusieurs-configurations"]], "Utilisation des Identifiants dans les Plugins": [[118, "utilisation-des-identifiants-dans-les-plugins"]], "Utilisation d\u2019AMQP avec URP, BUNNY, PDS-OP": [[79, "utilisation-damqp-avec-urp-bunny-pds-op"]], "Utilisation d\u2019une pile (stack)de r\u00e9f\u00e9rence ouverte": [[114, "utilisation-d-une-pile-stack-de-reference-ouverte"]], "Utiliser des instances": [[88, "utiliser-des-instances"]], "Utilitaires install\u00e9s sur les serveurs DD": [[79, "utilitaires-installes-sur-les-serveurs-dd"]], "Utilities installed on DD servers": [[47, "utilities-installed-on-dd-servers"]], "V02 Plugin Pain Points": [[20, "v02-plugin-pain-points"]], "V02 Plugin Points douloureux": [[101, "v02-plugin-points-douloureux"]], "V2 to Sr3": [[51, "v2-to-sr3"], [85, "v2-to-sr3"]], "VARIABLES D\u2019ENVIRONMENT": [[99, "variables-d-environment"]], "VARIABLES ENVIRONNEMENTALES": [[126, "variables-environnementales"], [129, "variables-environnementales"]], "VOIR AUSSI": [[99, "voir-aussi"], [125, "voir-aussi"], [126, "voir-aussi"], [127, "voir-aussi"], [130, "voir-aussi"]], "Validation des entr\u00e9es": [[106, "validation-des-entrees"]], "Variables": [[63, "variables"], [128, "variables"]], "Verification / Troubleshooting": [[47, "verification-troubleshooting"]], "Version 3 Refactor": [[20, "version-3-refactor"]], "Versioning Scheme": [[14, "versioning-scheme"]], "Viewing all Settings": [[39, "viewing-all-settings"]], "Virtual IP processing in poll": [[56, "virtual-ip-processing-in-poll"]], "Voir aussi": [[129, "voir-aussi"]], "Voting Failure Modes": [[8, "Voting-Failure-Modes"]], "V\u00e9rification / D\u00e9pannage": [[79, "verification-depannage"]], "WATCH": [[21, "watch"], [108, "watch"]], "WINNOW": [[21, "winnow"], [108, "winnow"]], "WMO": [[27, "wmo"], [107, "wmo"]], "Weather Application Flows": [[30, "weather-application-flows"]], "Weatheroffice citypages": [[25, "weatheroffice-citypages"]], "Web Proxies": [[55, "web-proxies"]], "Web Sites with non-standard file listings": [[49, "web-sites-with-non-standard-file-listings"]], "WebUI": [[46, "webui"], [80, "webui"]], "What If Each Notification is for a Block, not a File ?": [[15, "what-if-each-notification-is-for-a-block-not-a-file"]], "What Will Work Without Change": [[56, "what-will-work-without-change"]], "What Won\u2019t Work Without Change": [[56, "what-won-t-work-without-change"]], "What difference did it make?": [[12, "What-difference-did-it-make?"]], "What is Amdahl\u2019s Law?": [[7, "What-is-Amdahl\u2019s-Law?"]], "What is Going On?": [[46, "what-is-going-on"]], "When to Report": [[3, "when-to-report"]], "Where does AMQP Come From?": [[0, "where-does-amqp-come-from"]], "Where to Put Options": [[3, "where-to-put-options"]], "Why No FTP?": [[38, "why-no-ftp"]], "Why Not Just Use Rsync?": [[38, "why-not-just-use-rsync"]], "Why SFTP is More Often Chosen than FTPS": [[45, "why-sftp-is-more-often-chosen-than-ftps"]], "Why Use AMQP?": [[0, "why-use-amqp"]], "Why rST?": [[4, "why-rst"], [92, "why-rst"]], "Why v3 API should be used whenever possible": [[39, "why-v3-api-should-be-used-whenever-possible"]], "Windows": [[3, "windows"], [71, "windows"], [93, "windows"], [136, "windows"]], "Windows user manual": [[74, "windows-user-manual"]], "Winnowed Dataless": [[23, "winnowed-dataless"]], "With AMQP Notices on a Standard File Server": [[32, "with-amqp-notices-on-a-standard-file-server"]], "Working with Multiple Configurations": [[55, "working-with-multiple-configurations"]], "Working with Plugins": [[39, "working-with-plugins"]], "Worklists": [[39, "worklists"], [49, "worklists"], [67, "Worklists"], [83, "worklists"]], "Worries Addressed": [[20, "worries-addressed"]], "Write it in Python": [[12, "Write-it-in-Python"], [12, "id3"]], "Writing FlowCallback Plugins": [[49, "writing-flowcallback-plugins"]], "Writing Your Own Callbacks": [[67, "Writing-Your-Own-Callbacks"]], "Yes, Exactly.": [[7, "Yes,-Exactly."]], "[ queue|queue_name|qn ]": [[21, "queue-queue-name-qn-name"], [108, "queue-queue-name-qn-name"]], "[-pbd|\u2013post_baseDir ] (facultatif)": [[108, "pbd-post-basedir-path-facultatif"]], "[-pbd|\u2013post_baseDir ] (optional)": [[21, "pbd-post-basedir-path-optional"]], "[\u2013blocksize ] (default: 0 (auto))": [[21, "blocksize-value-default-0-auto"], [108, "blocksize-value-default-0-auto"]], "accelThreshold default: 0 (disabled.)": [[63, "accelthreshold-size-default-0-disabled"]], "accelTreshold d\u00e9faut: 0 (d\u00e9sactiver.)": [[128, "acceltreshold-size-defaut-0-desactiver"]], "accelXxxCommand": [[63, "accelxxxcommand"], [128, "accelxxxcommand"]], "accept, reject and acceptUnmatched": [[63, "accept-reject-and-acceptunmatched"]], "accept, reject and accept_unmatch": [[21, "accept-reject-and-accept-unmatch"], [108, "accept-reject-and-accept-unmatch"]], "accept, reject et acceptUnmatched": [[128, "accept-reject-et-acceptunmatched"]], "acceptSizeWrong: (default: False)": [[63, "acceptsizewrong-boolean-default-false"]], "acceptSizeWrong: (d\u00e9faut: False)": [[128, "acceptsizewrong-booleen-defaut-false"]], "acquisition HPC": [[112, "acquisition-hpc"]], "atime,mtime,mode": [[65, "atime-mtime-mode"], [130, "atime-mtime-mode"]], "attempts (default: 3)": [[63, "attempts-count-default-3"]], "attempts (d\u00e9faut: 3)": [[128, "attempts-count-defaut-3"]], "baseDir (d\u00e9faut: /)": [[128, "basedir-chemin-defaut"]], "baseDir (default: /)": [[63, "basedir-path-default"]], "baseUrl_relPath (default: off)": [[63, "baseurl-relpath-flag-default-off"]], "baseUrl_relPath (d\u00e9faut: off)": [[128, "baseurl-relpath-flag-defaut-off"]], "batch (default: 100)": [[63, "batch-count-default-100"]], "batch (d\u00e9faut: 100)": [[128, "batch-count-defaut-100"]], "blocksize default: 0 (auto)": [[63, "blocksize-size-default-0-auto"]], "blocksize d\u00e9faut: 0 (auto)": [[128, "blocksize-size-defaut-0-auto"]], "blocksize ": [[64, "blocksize-value"], [129, "blocksize-value"]], "broker": [[63, "broker"]], "bufsize (default: 1MB)": [[63, "bufsize-size-default-1mb"]], "bufsize (d\u00e9faut: 1m)": [[128, "bufsize-size-defaut-1m"]], "byteRateMax (default: 0)": [[63, "byteratemax-size-default-0"]], "byteRateMax (d\u00e9faut: 0)": [[128, "byteratemax-size-defaut-0"]], "callback ": [[128, "callback-speficationdeclass"]], "callback ": [[63, "callback-classspec"]], "callback options": [[21, "callback-options"]], "callback_prepend ": [[128, "callback-prepend-specificationdeclass"]], "callback_prepend ": [[63, "callback-prepend-classspec"]], "cas d\u00b4usage": [[120, "cas-d-usage"]], "cfg.batch": [[68, "cfg.batch"], [133, "cfg.batch"]], "cfg.download": [[68, "cfg.download"], [133, "cfg.download"]], "cfg.masks": [[68, "cfg.masks"], [133, "cfg.masks"]], "cfg.messageCountMax": [[68, "cfg.messageCountMax"], [133, "cfg.messageCountMax"]], "cfg.no, cfg.pid_filename": [[68, "cfg.no,-cfg.pid_filename"], [133, "cfg.no,-cfg.pid_filename"]], "convert": [[21, "convert"], [108, "convert"]], "dangerWillRobinson (default: omis)": [[128, "dangerwillrobinson-default-omis"]], "dangerWillRobinson (default: omitted)": [[63, "dangerwillrobinson-default-omitted"]], "dd_subscribe Renaming": [[28, "dd-subscribe-renaming"]], "debug": [[63, "debug"], [128, "debug"]], "declare": [[21, "declare"], [63, "declare"], [108, "declare"], [128, "declare"]], "delete (default: off)": [[63, "delete-boolean-default-off"]], "delete (d\u00e9faut: off)": [[128, "delete-booleen-defaut-off"]], "destfn_script