every.channel: sanitized baseline
This commit is contained in:
commit
897e556bea
258 changed files with 74298 additions and 0 deletions
3
third_party/iroh-org/iroh-gossip/.cargo/config.toml
vendored
Normal file
3
third_party/iroh-org/iroh-gossip/.cargo/config.toml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[target.wasm32-unknown-unknown]
|
||||
runner = "wasm-bindgen-test-runner"
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
10
third_party/iroh-org/iroh-gossip/.config/nextest.toml
vendored
Normal file
10
third_party/iroh-org/iroh-gossip/.config/nextest.toml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
[test-groups]
|
||||
run-in-isolation = { max-threads = 32 }
|
||||
# these are tests that must not run with other tests concurrently. All tests in
|
||||
# this group can take up at most 32 threads among them, but each one requiring
|
||||
# 16 threads also. The effect should be that tests run isolated.
|
||||
|
||||
[[profile.ci.overrides]]
|
||||
filter = 'test(::run_in_isolation::)'
|
||||
test-group = 'run-in-isolation'
|
||||
threads-required = 32
|
||||
13
third_party/iroh-org/iroh-gossip/.github/dependabot.yaml
vendored
Normal file
13
third_party/iroh-org/iroh-gossip/.github/dependabot.yaml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Keep GitHub Actions up to date with GitHub's Dependabot...
|
||||
# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
|
||||
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*" # Group all Actions updates into a single larger pull request
|
||||
schedule:
|
||||
interval: weekly
|
||||
18
third_party/iroh-org/iroh-gossip/.github/pull_request_template.md
vendored
Normal file
18
third_party/iroh-org/iroh-gossip/.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
## Description
|
||||
|
||||
<!-- A summary of what this pull request achieves and a rough list of changes. -->
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
<!-- Optional, if there are any breaking changes document them, including how to migrate older code. -->
|
||||
|
||||
## Notes & open questions
|
||||
|
||||
<!-- Any notes, remarks or open questions you have to make about the PR. -->
|
||||
|
||||
## Change checklist
|
||||
|
||||
- [ ] Self-review.
|
||||
- [ ] Documentation updates following the [style guide](https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#appendix-a-full-conventions-text), if relevant.
|
||||
- [ ] Tests if relevant.
|
||||
- [ ] All breaking changes documented.
|
||||
43
third_party/iroh-org/iroh-gossip/.github/workflows/beta.yaml
vendored
Normal file
43
third_party/iroh-org/iroh-gossip/.github/workflows/beta.yaml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Run tests using the beta Rust compiler
|
||||
|
||||
name: Beta Rust
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 06:50 UTC every Monday
|
||||
- cron: '50 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: beta-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
uses: './.github/workflows/tests.yaml'
|
||||
with:
|
||||
rust-version: beta
|
||||
notify:
|
||||
needs: tests
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract test results
|
||||
run: |
|
||||
printf '${{ toJSON(needs) }}\n'
|
||||
result=$(echo '${{ toJSON(needs) }}' | jq -r .tests.result)
|
||||
echo TESTS_RESULT=$result
|
||||
echo "TESTS_RESULT=$result" >>"$GITHUB_ENV"
|
||||
- name: Notify discord on failure
|
||||
uses: n0-computer/discord-webhook-notify@v1
|
||||
if: ${{ env.TESTS_RESULT == 'failure' }}
|
||||
with:
|
||||
severity: error
|
||||
details: |
|
||||
Rustc beta tests failed in **${{ github.repository }}**
|
||||
See https://github.com/${{ github.repository }}/actions/workflows/beta.yaml
|
||||
webhookUrl: ${{ secrets.DISCORD_N0_GITHUB_CHANNEL_WEBHOOK_URL }}
|
||||
|
||||
305
third_party/iroh-org/iroh-gossip/.github/workflows/ci.yaml
vendored
Normal file
305
third_party/iroh-org/iroh-gossip/.github/workflows/ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ 'labeled', 'unlabeled', 'opened', 'synchronize', 'reopened' ]
|
||||
merge_group:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
MSRV: "1.89"
|
||||
SCCACHE_CACHE_SIZE: "50G"
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: CI Test Suite
|
||||
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
|
||||
uses: './.github/workflows/tests.yaml'
|
||||
|
||||
cross_build:
|
||||
name: Cross Build Only
|
||||
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
|
||||
timeout-minutes: 30
|
||||
runs-on: [self-hosted, linux, X64]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
# cross tests are currently broken vor armv7 and aarch64
|
||||
# see https://github.com/cross-rs/cross/issues/1311
|
||||
# - armv7-linux-androideabi
|
||||
# - aarch64-linux-android
|
||||
# Freebsd execution fails in cross
|
||||
# - i686-unknown-freebsd # Linking fails :/
|
||||
- x86_64-unknown-freebsd
|
||||
# Netbsd execution fails to link in cross
|
||||
# - x86_64-unknown-netbsd
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cleanup Docker
|
||||
continue-on-error: true
|
||||
run: |
|
||||
docker kill $(docker ps -q)
|
||||
|
||||
# See https://github.com/cross-rs/cross/issues/1222
|
||||
- uses: taiki-e/install-action@cross
|
||||
|
||||
- name: build
|
||||
# cross tests are currently broken vor armv7 and aarch64
|
||||
# see https://github.com/cross-rs/cross/issues/1311. So on
|
||||
# those platforms we only build but do not run tests.
|
||||
run: cross build --all --target ${{ matrix.target }}
|
||||
env:
|
||||
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG'}}
|
||||
|
||||
android_build:
|
||||
name: Android Build Only
|
||||
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
|
||||
timeout-minutes: 30
|
||||
# runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux, X64]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- aarch64-linux-android
|
||||
- armv7-linux-androideabi
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
- name: Install rustup target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Setup Android NDK
|
||||
uses: arqu/setup-ndk@main
|
||||
id: setup-ndk
|
||||
with:
|
||||
ndk-version: r23
|
||||
add-to-path: true
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
run: |
|
||||
cargo install --version 3.5.4 cargo-ndk
|
||||
cargo ndk --target ${{ matrix.target }} build
|
||||
|
||||
cross_test:
|
||||
name: Cross Test
|
||||
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
|
||||
timeout-minutes: 30
|
||||
runs-on: [self-hosted, linux, X64]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- i686-unknown-linux-gnu
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Cleanup Docker
|
||||
continue-on-error: true
|
||||
run: |
|
||||
docker kill $(docker ps -q)
|
||||
|
||||
# See https://github.com/cross-rs/cross/issues/1222
|
||||
- uses: taiki-e/install-action@cross
|
||||
|
||||
- name: test
|
||||
run: cross test --all --target ${{ matrix.target }} -- --test-threads=12
|
||||
env:
|
||||
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG' }}
|
||||
|
||||
wasm_build:
|
||||
name: Build wasm32
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTFLAGS: '--cfg getrandom_backend="wasm_js"'
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add wasm target
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
|
||||
- name: Install wasm-tools
|
||||
uses: bytecodealliance/actions/wasm-tools/setup@v1
|
||||
|
||||
- name: wasm32 build
|
||||
run: cargo build --target wasm32-unknown-unknown
|
||||
|
||||
# If the Wasm file contains any 'import "env"' declarations, then
|
||||
# some non-Wasm-compatible code made it into the final code.
|
||||
- name: Ensure no 'import "env"' in iroh-relay Wasm
|
||||
run: |
|
||||
! wasm-tools print --skeleton target/wasm32-unknown-unknown/debug/iroh_gossip.wasm | grep 'import "env"'
|
||||
|
||||
|
||||
check_semver:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Setup Environment (PR)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "HEAD_COMMIT_SHA=$(git rev-parse origin/${{ github.base_ref }})" >> ${GITHUB_ENV}
|
||||
- name: Setup Environment (Push)
|
||||
if: ${{ github.event_name == 'push' || github.event_name == 'merge_group' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo "HEAD_COMMIT_SHA=$(git rev-parse origin/main)" >> ${GITHUB_ENV}
|
||||
- name: Check semver
|
||||
# uses: obi1kenobi/cargo-semver-checks-action@v2
|
||||
uses: n0-computer/cargo-semver-checks-action@feat-baseline
|
||||
with:
|
||||
package: iroh-gossip
|
||||
baseline-rev: ${{ env.HEAD_COMMIT_SHA }}
|
||||
use-cache: false
|
||||
|
||||
check_fmt:
|
||||
timeout-minutes: 30
|
||||
name: Checking fmt
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- uses: mozilla-actions/sccache-action@v0.0.9
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- run: cargo make format-check
|
||||
|
||||
check_docs:
|
||||
timeout-minutes: 30
|
||||
name: Checking docs
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: iroh-gossip docs
|
||||
run: cargo docs-rs
|
||||
|
||||
clippy_check:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
# TODO: We have a bunch of platform-dependent code so should
|
||||
# probably run this job on the full platform matrix
|
||||
- name: clippy check (all features)
|
||||
run: cargo clippy --workspace --all-features --all-targets --bins --tests --benches
|
||||
|
||||
- name: clippy check (no features)
|
||||
run: cargo clippy --workspace --no-default-features --lib --bins --tests
|
||||
|
||||
- name: clippy check (default features)
|
||||
run: cargo clippy --workspace --all-targets
|
||||
|
||||
msrv:
|
||||
if: "github.event_name != 'pull_request' || ! contains(github.event.pull_request.labels.*.name, 'flaky-test')"
|
||||
timeout-minutes: 30
|
||||
name: Minimal Supported Rust Version
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Check MSRV all features
|
||||
run: |
|
||||
cargo +$MSRV check --workspace --all-targets
|
||||
|
||||
cargo_deny:
|
||||
timeout-minutes: 30
|
||||
name: cargo deny
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
arguments: --workspace --all-features
|
||||
command: check
|
||||
command-arguments: "-Dwarnings"
|
||||
|
||||
codespell:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: pip install --user codespell[toml]
|
||||
- run: codespell --ignore-words-list=ans,atmost,crate,inout,ratatui,ser,stayin,swarmin,worl --skip=CHANGELOG.md
|
||||
45
third_party/iroh-org/iroh-gossip/.github/workflows/cleanup.yaml
vendored
Normal file
45
third_party/iroh-org/iroh-gossip/.github/workflows/cleanup.yaml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Run tests using the beta Rust compiler
|
||||
|
||||
name: Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 06:50 UTC every Monday
|
||||
- cron: '50 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: beta-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
clean_docs_branch:
|
||||
permissions:
|
||||
issues: write
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: generated-docs-preview
|
||||
- name: Clean docs branch
|
||||
run: |
|
||||
cd pr/
|
||||
# keep the last 25 prs
|
||||
dirs=$(ls -1d [0-9]* | sort -n)
|
||||
total_dirs=$(echo "$dirs" | wc -l)
|
||||
dirs_to_remove=$(echo "$dirs" | head -n $(($total_dirs - 25)))
|
||||
if [ -n "$dirs_to_remove" ]; then
|
||||
echo "$dirs_to_remove" | xargs rm -rf
|
||||
fi
|
||||
git add .
|
||||
git commit -m "Cleanup old docs"
|
||||
git push
|
||||
|
||||
|
||||
|
||||
|
||||
19
third_party/iroh-org/iroh-gossip/.github/workflows/commit.yaml
vendored
Normal file
19
third_party/iroh-org/iroh-gossip/.github/workflows/commit.yaml
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
name: Commits
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
env:
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
check-for-cc:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: check-for-cc
|
||||
id: check-for-cc
|
||||
uses: agenthunt/conventional-commit-checker-action@v2.0.0
|
||||
with:
|
||||
pr-title-regex: "^(.+)(?:(([^)s]+)))?!?: (.+)"
|
||||
73
third_party/iroh-org/iroh-gossip/.github/workflows/docs.yaml
vendored
Normal file
73
third_party/iroh-org/iroh-gossip/.github/workflows/docs.yaml
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
name: Docs Preview
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
# ensure job runs sequentially so pushing to the preview branch doesn't conflict
|
||||
concurrency:
|
||||
group: ci-docs-preview
|
||||
|
||||
env:
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
preview_docs:
|
||||
permissions: write-all
|
||||
timeout-minutes: 30
|
||||
name: Docs preview
|
||||
if: ${{ (github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' ) && !github.event.pull_request.head.repo.fork }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
SCCACHE_CACHE_SIZE: "50G"
|
||||
PREVIEW_PATH: pr/${{ github.event.pull_request.number || inputs.pr_number }}/docs
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly-2025-10-09
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Generate Docs
|
||||
run: cargo doc --workspace --all-features --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: --cfg iroh_docsrs
|
||||
|
||||
- name: Deploy Docs to Preview Branch
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./target/doc/
|
||||
destination_dir: ${{ env.PREVIEW_PATH }}
|
||||
publish_branch: generated-docs-preview
|
||||
|
||||
- name: Find Docs Comment
|
||||
uses: peter-evans/find-comment@v4
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: Documentation for this PR has been generated
|
||||
|
||||
- name: Get current timestamp
|
||||
id: get_timestamp
|
||||
run: echo "TIMESTAMP=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
|
||||
|
||||
- name: Create or Update Docs Comment
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
body: |
|
||||
Documentation for this PR has been generated and is available at: https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/${{ env.PREVIEW_PATH }}/iroh_gossip/
|
||||
|
||||
Last updated: ${{ env.TIMESTAMP }}
|
||||
edit-mode: replace
|
||||
99
third_party/iroh-org/iroh-gossip/.github/workflows/flaky.yaml
vendored
Normal file
99
third_party/iroh-org/iroh-gossip/.github/workflows/flaky.yaml
vendored
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# Run all tests, including flaky test.
|
||||
#
|
||||
# The default CI workflow ignores flaky tests. This workflow will run
|
||||
# all tests, including ignored ones.
|
||||
#
|
||||
# To use this workflow you can either:
|
||||
#
|
||||
# - Label a PR with "flaky-test", the normal CI workflow will not run
|
||||
# any jobs but the jobs here will be run. Note that to merge the PR
|
||||
# you'll need to remove the label eventually because the normal CI
|
||||
# jobs are required by branch protection.
|
||||
#
|
||||
# - Manually trigger the workflow, you may choose a branch for this to
|
||||
# run on.
|
||||
#
|
||||
# Additionally this jobs runs once a day on a schedule.
|
||||
#
|
||||
# Currently doctests are not run by this workflow.
|
||||
|
||||
name: Flaky CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [ 'labeled', 'unlabeled', 'opened', 'synchronize', 'reopened' ]
|
||||
schedule:
|
||||
# 06:30 UTC every day
|
||||
- cron: '30 6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: 'Branch to run on, defaults to main'
|
||||
required: true
|
||||
default: 'main'
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: flaky-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
if: "contains(github.event.pull_request.labels.*.name, 'flaky-test') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'"
|
||||
uses: './.github/workflows/tests.yaml'
|
||||
with:
|
||||
flaky: true
|
||||
git-ref: ${{ inputs.branch }}
|
||||
notify:
|
||||
needs: tests
|
||||
if: ${{ always() }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Extract test results
|
||||
run: |
|
||||
printf '${{ toJSON(needs) }}\n'
|
||||
result=$(echo '${{ toJSON(needs) }}' | jq -r .tests.result)
|
||||
echo TESTS_RESULT=$result
|
||||
echo "TESTS_RESULT=$result" >>"$GITHUB_ENV"
|
||||
- name: download nextest reports
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
pattern: libtest_run_${{ github.run_number }}-${{ github.run_attempt }}-*
|
||||
merge-multiple: true
|
||||
path: nextest-results
|
||||
- name: create summary report
|
||||
id: make_summary
|
||||
run: |
|
||||
# prevent the glob expression in the loop to match on itself when the dir is empty
|
||||
shopt -s nullglob
|
||||
# to deal with multiline outputs it's recommended to use a random EOF, the syntax is based on
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
|
||||
EOF=aP51VriWCxNJ1JjvmO9i
|
||||
echo "summary<<$EOF" >> $GITHUB_OUTPUT
|
||||
echo "Flaky tests failure:" >> $GITHUB_OUTPUT
|
||||
echo " " >> $GITHUB_OUTPUT
|
||||
for report in nextest-results/*.json; do
|
||||
# remove the name prefix and extension, and split the parts
|
||||
name=$(echo ${report:16:-5} | tr _ ' ')
|
||||
echo $name
|
||||
echo "- **$name**" >> $GITHUB_OUTPUT
|
||||
# select the failed tests
|
||||
# the tests have this format "crate::module$test_name", the sed expressions remove the quotes and replace $ for ::
|
||||
failure=$(jq --slurp '.[] | select(.["type"] == "test" and .["event"] == "failed" ) | .["name"]' $report | sed -e 's/^"//g' -e 's/\$/::/' -e 's/"//')
|
||||
echo "$failure"
|
||||
echo "$failure" >> $GITHUB_OUTPUT
|
||||
done
|
||||
echo "" >> $GITHUB_OUTPUT
|
||||
echo "See https://github.com/${{ github.repository }}/actions/workflows/flaky.yaml" >> $GITHUB_OUTPUT
|
||||
echo "$EOF" >> $GITHUB_OUTPUT
|
||||
- name: Notify discord on failure
|
||||
uses: n0-computer/discord-webhook-notify@v1
|
||||
if: ${{ env.TESTS_RESULT == 'failure' || env.TESTS_RESULT == 'success' }}
|
||||
with:
|
||||
text: "Flaky tests in **${{ github.repository }}**:"
|
||||
severity: ${{ env.TESTS_RESULT == 'failure' && 'warn' || 'info' }}
|
||||
details: ${{ env.TESTS_RESULT == 'failure' && steps.make_summary.outputs.summary || 'No flaky failures!' }}
|
||||
webhookUrl: ${{ secrets.DISCORD_N0_GITHUB_CHANNEL_WEBHOOK_URL }}
|
||||
50
third_party/iroh-org/iroh-gossip/.github/workflows/release.yaml
vendored
Normal file
50
third_party/iroh-org/iroh-gossip/.github/workflows/release.yaml
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
name: release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release_version:
|
||||
description: "Release version"
|
||||
required: true
|
||||
default: ""
|
||||
create_release:
|
||||
description: "Create release"
|
||||
required: true
|
||||
default: "true"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: create-release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.release.outputs.upload_url }}
|
||||
release_version: ${{ env.RELEASE_VERSION }}
|
||||
steps:
|
||||
- name: Get the release version from the tag (push)
|
||||
shell: bash
|
||||
if: env.RELEASE_VERSION == '' && github.event_name == 'push'
|
||||
run: |
|
||||
# See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
echo "version is: ${{ env.RELEASE_VERSION }}"
|
||||
- name: Get the release version from the tag (dispatch)
|
||||
shell: bash
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "RELEASE_VERSION=${{ github.event.inputs.release_version }}" >> $GITHUB_ENV
|
||||
echo "version is: ${{ env.RELEASE_VERSION }}"
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Create GitHub release
|
||||
id: release
|
||||
if: github.event.inputs.create_release == 'true' || github.event_name == 'push'
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_VERSION }}
|
||||
release_name: ${{ env.RELEASE_VERSION }}
|
||||
53
third_party/iroh-org/iroh-gossip/.github/workflows/simulation.yaml
vendored
Normal file
53
third_party/iroh-org/iroh-gossip/.github/workflows/simulation.yaml
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: run simulations
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
run_sim:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
SCCACHE_GHA_ENABLED: "on"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Run simulations
|
||||
run: |
|
||||
git checkout ${{ github.event.pull_request.base.ref }}
|
||||
cargo run -q --bin sim --features simulator --release -- run -c simulations/all.toml -o /tmp/sim-main
|
||||
git checkout ${{ github.event.pull_request.head.sha }}
|
||||
cargo run -q --bin sim --features simulator --release -- run -c simulations/all.toml -o /tmp/sim-pr --baseline /tmp/sim-main |& tee REPORT
|
||||
echo "<details><summary>Simulation report</summary>" >> COMMENT
|
||||
echo "" >> COMMENT
|
||||
echo '```' >> COMMENT
|
||||
cat REPORT >> COMMENT
|
||||
echo "" >> COMMENT
|
||||
echo '```' >> COMMENT
|
||||
echo "</details>" >> COMMENT
|
||||
echo "" >> COMMENT
|
||||
echo "*Last updated: $(date -u +'%Y-%m-%dT%H:%M:%SZ')*" >> COMMENT
|
||||
|
||||
- name: Find Docs Comment
|
||||
uses: peter-evans/find-comment@v4
|
||||
id: fc
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: Simulation report
|
||||
|
||||
- name: Create or Update Docs Comment
|
||||
uses: peter-evans/create-or-update-comment@v5
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number || inputs.pr_number }}
|
||||
comment-id: ${{ steps.fc.outputs.comment-id }}
|
||||
body-path: COMMENT
|
||||
edit-mode: replace
|
||||
229
third_party/iroh-org/iroh-gossip/.github/workflows/tests.yaml
vendored
Normal file
229
third_party/iroh-org/iroh-gossip/.github/workflows/tests.yaml
vendored
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# Run all tests, with or without flaky tests.
|
||||
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
rust-version:
|
||||
description: 'The version of the rust compiler to run'
|
||||
type: string
|
||||
default: 'stable'
|
||||
flaky:
|
||||
description: 'Whether to also run flaky tests'
|
||||
type: boolean
|
||||
default: false
|
||||
git-ref:
|
||||
description: 'Which git ref to checkout'
|
||||
type: string
|
||||
default: ${{ github.ref }}
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
RUSTFLAGS: -Dwarnings
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
SCCACHE_CACHE_SIZE: "50G"
|
||||
CRATES_LIST: "iroh-gossip"
|
||||
IROH_FORCE_STAGING_RELAYS: "1"
|
||||
|
||||
jobs:
|
||||
build_and_test_nix:
|
||||
timeout-minutes: 30
|
||||
name: "Tests"
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [ubuntu-latest, macOS-arm-latest]
|
||||
rust: [ '${{ inputs.rust-version }}' ]
|
||||
features: [all, none, default]
|
||||
include:
|
||||
- name: ubuntu-latest
|
||||
os: ubuntu-latest
|
||||
release-os: linux
|
||||
release-arch: amd64
|
||||
runner: [self-hosted, linux, X64]
|
||||
- name: macOS-arm-latest
|
||||
os: macOS-latest
|
||||
release-os: darwin
|
||||
release-arch: aarch64
|
||||
runner: [self-hosted, macOS, ARM64]
|
||||
env:
|
||||
# Using self-hosted runners so use local cache for sccache and
|
||||
# not SCCACHE_GHA_ENABLED.
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.git-ref }}
|
||||
|
||||
- name: Install ${{ matrix.rust }} rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
|
||||
- name: Install cargo-nextest
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: nextest@0.9.80
|
||||
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- name: Select features
|
||||
run: |
|
||||
case "${{ matrix.features }}" in
|
||||
all)
|
||||
echo "FEATURES=--all-features" >> "$GITHUB_ENV"
|
||||
;;
|
||||
none)
|
||||
echo "FEATURES=--no-default-features" >> "$GITHUB_ENV"
|
||||
;;
|
||||
default)
|
||||
echo "FEATURES=" >> "$GITHUB_ENV"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
esac
|
||||
|
||||
- name: check features
|
||||
if: ${{ ! inputs.flaky }}
|
||||
run: |
|
||||
for i in ${CRATES_LIST//,/ }
|
||||
do
|
||||
echo "Checking $i $FEATURES"
|
||||
if [ $i = "iroh-cli" ]; then
|
||||
targets="--bins"
|
||||
else
|
||||
targets="--lib --bins"
|
||||
fi
|
||||
echo cargo check -p $i $FEATURES $targets
|
||||
cargo check -p $i $FEATURES $targets
|
||||
done
|
||||
env:
|
||||
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG'}}
|
||||
|
||||
- name: build tests
|
||||
run: |
|
||||
cargo nextest run --workspace ${{ env.FEATURES }} --lib --bins --tests --no-run
|
||||
|
||||
- name: list ignored tests
|
||||
run: |
|
||||
cargo nextest list --workspace ${{ env.FEATURES }} --lib --bins --tests --run-ignored ignored-only
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
mkdir -p output
|
||||
cargo nextest run --workspace ${{ env.FEATURES }} --lib --bins --tests --profile ci --run-ignored ${{ inputs.flaky && 'all' || 'default' }} --no-fail-fast --message-format ${{ inputs.flaky && 'libtest-json' || 'human' }} > output/${{ matrix.name }}_${{ matrix.features }}_${{ matrix.rust }}.json
|
||||
env:
|
||||
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG'}}
|
||||
NEXTEST_EXPERIMENTAL_LIBTEST_JSON: 1
|
||||
|
||||
- name: upload results
|
||||
if: ${{ failure() && inputs.flaky }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libtest_run_${{ github.run_number }}-${{ github.run_attempt }}-${{ matrix.name }}_${{ matrix.features }}_${{ matrix.rust }}.json
|
||||
path: output
|
||||
retention-days: 45
|
||||
compression-level: 0
|
||||
|
||||
- name: doctests
|
||||
if: ${{ (! inputs.flaky) && matrix.features == 'all' }}
|
||||
run: |
|
||||
if [ -n "${{ runner.debug }}" ]; then
|
||||
export RUST_LOG=TRACE
|
||||
else
|
||||
export RUST_LOG=DEBUG
|
||||
fi
|
||||
cargo test --workspace --all-features --doc
|
||||
|
||||
build_and_test_windows:
|
||||
timeout-minutes: 30
|
||||
name: "Tests"
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [windows-latest]
|
||||
rust: [ '${{ inputs.rust-version}}' ]
|
||||
features: [all, none, default]
|
||||
target:
|
||||
- x86_64-pc-windows-msvc
|
||||
include:
|
||||
- name: windows-latest
|
||||
os: windows
|
||||
runner: [self-hosted, windows, x64]
|
||||
env:
|
||||
# Using self-hosted runners so use local cache for sccache and
|
||||
# not SCCACHE_GHA_ENABLED.
|
||||
RUSTC_WRAPPER: "sccache"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ inputs.git-ref }}
|
||||
|
||||
- name: Install ${{ matrix.rust }}
|
||||
run: |
|
||||
rustup toolchain install ${{ matrix.rust }}
|
||||
rustup toolchain default ${{ matrix.rust }}
|
||||
rustup target add ${{ matrix.target }}
|
||||
rustup set default-host ${{ matrix.target }}
|
||||
|
||||
- name: Install cargo-nextest
|
||||
shell: powershell
|
||||
run: |
|
||||
$tmp = New-TemporaryFile | Rename-Item -NewName { $_ -replace 'tmp$', 'zip' } -PassThru
|
||||
Invoke-WebRequest -OutFile $tmp https://get.nexte.st/latest/windows
|
||||
$outputDir = if ($Env:CARGO_HOME) { Join-Path $Env:CARGO_HOME "bin" } else { "~/.cargo/bin" }
|
||||
$tmp | Expand-Archive -DestinationPath $outputDir -Force
|
||||
$tmp | Remove-Item
|
||||
|
||||
- name: Select features
|
||||
run: |
|
||||
switch ("${{ matrix.features }}") {
|
||||
"all" {
|
||||
echo "FEATURES=--all-features" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
}
|
||||
"none" {
|
||||
echo "FEATURES=--no-default-features" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
}
|
||||
"default" {
|
||||
echo "FEATURES=" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf8 -Append
|
||||
}
|
||||
default {
|
||||
Exit 1
|
||||
}
|
||||
}
|
||||
|
||||
- name: Install sccache
|
||||
uses: mozilla-actions/sccache-action@v0.0.9
|
||||
|
||||
- uses: msys2/setup-msys2@v2
|
||||
|
||||
- name: build tests
|
||||
run: |
|
||||
cargo nextest run --workspace ${{ env.FEATURES }} --lib --bins --tests --target ${{ matrix.target }} --no-run
|
||||
|
||||
- name: list ignored tests
|
||||
run: |
|
||||
cargo nextest list --workspace ${{ env.FEATURES }} --lib --bins --tests --target ${{ matrix.target }} --run-ignored ignored-only
|
||||
|
||||
- name: tests
|
||||
run: |
|
||||
mkdir -p output
|
||||
cargo nextest run --workspace ${{ env.FEATURES }} --lib --bins --tests --profile ci --target ${{ matrix.target }} --run-ignored ${{ inputs.flaky && 'all' || 'default' }} --no-fail-fast --message-format ${{ inputs.flaky && 'libtest-json' || 'human' }} > output/${{ matrix.name }}_${{ matrix.features }}_${{ matrix.rust }}.json
|
||||
env:
|
||||
RUST_LOG: ${{ runner.debug && 'TRACE' || 'DEBUG'}}
|
||||
NEXTEST_EXPERIMENTAL_LIBTEST_JSON: 1
|
||||
|
||||
- name: upload results
|
||||
if: ${{ failure() && inputs.flaky }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: libtest_run_${{ github.run_number }}-${{ github.run_attempt }}-${{ matrix.name }}_${{ matrix.features }}_${{ matrix.rust }}.json
|
||||
path: output
|
||||
retention-days: 1
|
||||
compression-level: 0
|
||||
1
third_party/iroh-org/iroh-gossip/.gitignore
vendored
Normal file
1
third_party/iroh-org/iroh-gossip/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
250
third_party/iroh-org/iroh-gossip/CHANGELOG.md
vendored
Normal file
250
third_party/iroh-org/iroh-gossip/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to iroh-gossip will be documented in this file.
|
||||
|
||||
## [0.96.0](https://github.com/n0-computer/iroh-gossip/compare/v0.95.0..0.96.0) - 2026-01-29
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- Add `neighbors()` method to `GossipTopic` ([#124](https://github.com/n0-computer/iroh-gossip/issues/124)) - ([9e4ddaa](https://github.com/n0-computer/iroh-gossip/commit/9e4ddaa904c6e1b853081b9f2e4f9628ed274b08))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Keep topic alive if either senders or receivers exist ([#119](https://github.com/n0-computer/iroh-gossip/issues/119)) - ([34b0e0e](https://github.com/n0-computer/iroh-gossip/commit/34b0e0ea87a2f0a6011d026bcffce78697689072))
|
||||
- Clean-up connections after unexpected disconnects ([#117](https://github.com/n0-computer/iroh-gossip/issues/117)) - ([84f3945](https://github.com/n0-computer/iroh-gossip/commit/84f394577a4e8b52660a83dbbc955f0accb31b5a))
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- Switch from tracing-test to n0-tracing-test ([#125](https://github.com/n0-computer/iroh-gossip/issues/125)) - ([48f522b](https://github.com/n0-computer/iroh-gossip/commit/48f522bb1504a7fbf4eb51112ea724fa1593a35f))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Ignore rustls-pemfile unmaintained advisory ([#122](https://github.com/n0-computer/iroh-gossip/issues/122)) - ([16c68bb](https://github.com/n0-computer/iroh-gossip/commit/16c68bb187c7127eb5694bfbbd1ec3da518bcc25))
|
||||
- Upgrade to `iroh`v0.96 and the latest version of `iroh-quinn` ([#114](https://github.com/n0-computer/iroh-gossip/issues/114)) - ([13ef379](https://github.com/n0-computer/iroh-gossip/commit/13ef379f60e91f7292ae287051245db25ca8dd02))
|
||||
|
||||
## [0.95.0](https://github.com/n0-computer/iroh-gossip/compare/v0.94.0..0.95.0) - 2025-11-06
|
||||
|
||||
### Deps/refactor
|
||||
|
||||
- [**breaking**] Update to iroh main, port to n0-error ([#113](https://github.com/n0-computer/iroh-gossip/issues/113)) - ([4d2cb2f](https://github.com/n0-computer/iroh-gossip/commit/4d2cb2f3891e8dadd89a985fb6b5ad55d92e4c59))
|
||||
|
||||
## [0.94.0](https://github.com/n0-computer/iroh-gossip/compare/v0.93.1..0.94.0) - 2025-10-21
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Use discovery service instead of `Endpoint::add_node_addr` ([#108](https://github.com/n0-computer/iroh-gossip/issues/108)) - ([f7e3ef4](https://github.com/n0-computer/iroh-gossip/commit/f7e3ef478a1c4f1ea934e29f3436582e68de734c))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Upgrade to iroh 0.94 ([#110](https://github.com/n0-computer/iroh-gossip/issues/110)) - ([ad78602](https://github.com/n0-computer/iroh-gossip/commit/ad78602a4bafad8db2a4264bf16fde12b08f7a5e))
|
||||
|
||||
## [0.93.1](https://github.com/n0-computer/iroh-gossip/compare/v0.93.0..0.93.1) - 2025-10-11
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Update nightly version in CI and docs workflows ([#107](https://github.com/n0-computer/iroh-gossip/issues/107)) - ([b5e3414](https://github.com/n0-computer/iroh-gossip/commit/b5e3414f8db03910b6cea691bad69f798f1c34c6))
|
||||
|
||||
## [0.93.0](https://github.com/n0-computer/iroh-gossip/compare/v0.92.0..0.93.0) - 2025-10-09
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- *(ci)* Add auto release on tag version push ([#93](https://github.com/n0-computer/iroh-gossip/issues/93)) - ([afa6e1d](https://github.com/n0-computer/iroh-gossip/commit/afa6e1dca9cef061642bece52fcdad5e877496e3))
|
||||
- Set custom ALPN ([#92](https://github.com/n0-computer/iroh-gossip/issues/92)) - ([ff87b6a](https://github.com/n0-computer/iroh-gossip/commit/ff87b6a380a39c8274376ff26f874824ff80d752))
|
||||
|
||||
### ⚡ Performance
|
||||
|
||||
- Don't allocate in `Timers::wait_next` ([#102](https://github.com/n0-computer/iroh-gossip/issues/102)) - ([65278b7](https://github.com/n0-computer/iroh-gossip/commit/65278b75aa67cb6bed06c9770cacf701e952c0d3))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- *(ci)* Fix url of the beta notification ([#94](https://github.com/n0-computer/iroh-gossip/issues/94)) - ([2021566](https://github.com/n0-computer/iroh-gossip/commit/20215660a71f192562477dafd677d5a974c6a7f3))
|
||||
- Release prep ([#106](https://github.com/n0-computer/iroh-gossip/issues/106)) - ([099196e](https://github.com/n0-computer/iroh-gossip/commit/099196e72d806392ed609dfe36510b130839d52e))
|
||||
|
||||
## [0.92.0](https://github.com/n0-computer/iroh-gossip/compare/v0.91.0..0.92.0) - 2025-09-18
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Upgrade `iroh`, `iroh-base`, `irpc` ([#91](https://github.com/n0-computer/iroh-gossip/issues/91)) - ([464fe69](https://github.com/n0-computer/iroh-gossip/commit/464fe69789ae8c8fefd7734a2f44db5aa447db26))
|
||||
|
||||
## [0.91.0](https://github.com/n0-computer/iroh-gossip/compare/v0.90.0..0.91.0) - 2025-07-31
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Make GossipSender `Clone` and GossipTopic `Sync` ([#81](https://github.com/n0-computer/iroh-gossip/issues/81)) - ([f215aa1](https://github.com/n0-computer/iroh-gossip/commit/f215aa13806425491cf328e42c611a0002da4371))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Replace `iroh-net` mention in README ([#83](https://github.com/n0-computer/iroh-gossip/issues/83)) - ([e3df4ec](https://github.com/n0-computer/iroh-gossip/commit/e3df4ec7a56bcff0dafe6940d7d706ece5508891))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add patch for `iroh` dependencies ([#82](https://github.com/n0-computer/iroh-gossip/issues/82)) - ([2e82a68](https://github.com/n0-computer/iroh-gossip/commit/2e82a683f93aa1ae50929da8ce95b23f85b466f1))
|
||||
- [**breaking**] Prep for `v0.91.0` release ([#85](https://github.com/n0-computer/iroh-gossip/issues/85)) - ([d1fbfca](https://github.com/n0-computer/iroh-gossip/commit/d1fbfca15f484a41b8d6e8f771d14bf9fe5c7f81))
|
||||
|
||||
### Deps
|
||||
|
||||
- Update to irpc@main, iroh@main ([#84](https://github.com/n0-computer/iroh-gossip/issues/84)) - ([af7ae1f](https://github.com/n0-computer/iroh-gossip/commit/af7ae1f9bb9fa74aef97d510e062b09c03e96a87))
|
||||
|
||||
## [0.90.0](https://github.com/n0-computer/iroh-gossip/compare/v0.35.0..0.90.0) - 2025-06-27
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- *(net)* Add shutdown function ([#69](https://github.com/n0-computer/iroh-gossip/issues/69)) - ([3cf2cd2](https://github.com/n0-computer/iroh-gossip/commit/3cf2cd2f3af5c79832b335b525b34db4290d0332))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(hyparview)* [**breaking**] Only add peers to active view after receiving neighbor messages ([#56](https://github.com/n0-computer/iroh-gossip/issues/56)) - ([5a441e6](https://github.com/n0-computer/iroh-gossip/commit/5a441e6cf5589fc3c7cf3c290005b1094895038c))
|
||||
- *(hyparview)* Use shuffle replies as intended ([#57](https://github.com/n0-computer/iroh-gossip/issues/57)) - ([9632ced](https://github.com/n0-computer/iroh-gossip/commit/9632ced028ad7c211ac256b89d7ac0fcb32f55a6))
|
||||
- *(hyparview)* Don't emit PeerData event for empty PeerData - ([c345f0a](https://github.com/n0-computer/iroh-gossip/commit/c345f0a0a6a099643f13a7a6743b308548a1c40e))
|
||||
- *(plumtree)* Ensure eager relation is symmetrical - ([0abface](https://github.com/n0-computer/iroh-gossip/commit/0abface77c81dfde91c9f37da1f00bfee36f86d7))
|
||||
- *(plumtree)* Clear graft timer to allow retry on new ihaves - ([b65cdce](https://github.com/n0-computer/iroh-gossip/commit/b65cdcea36c8b30ac3ab6443645c6e69795f6ebf))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- *(hyparview)* Improve disconnect handling - ([5156d00](https://github.com/n0-computer/iroh-gossip/commit/5156d00f72be478872e7cecdf1b95d739b3b4fca))
|
||||
- *(hyparview)* Remove obsolete parameter in hyparview - ([d954aa6](https://github.com/n0-computer/iroh-gossip/commit/d954aa62272d7f781ce762b42b06d2521e7d1b30))
|
||||
- *(net)* [**breaking**] Remove `Joined` event, use `NeighborUp` ([#49](https://github.com/n0-computer/iroh-gossip/issues/49)) - ([c06f2ed](https://github.com/n0-computer/iroh-gossip/commit/c06f2ed64cb887d0714dfa1e75c0d66051c9d3e1))
|
||||
- [**breaking**] Port to irpc, flatten event enum, remove cli impl ([#67](https://github.com/n0-computer/iroh-gossip/issues/67)) - ([a8d5cd2](https://github.com/n0-computer/iroh-gossip/commit/a8d5cd2b4c749993dd99f9d5eead073fd4b2498d))
|
||||
- [**breaking**] Port to iroh@0.90 and n0-snafu ([#77](https://github.com/n0-computer/iroh-gossip/issues/77)) - ([1523227](https://github.com/n0-computer/iroh-gossip/commit/1523227c980c7d58efff805645aa50bea17402b0))
|
||||
- [**breaking**] Change wire protocol to use uni streams per topic ([#75](https://github.com/n0-computer/iroh-gossip/issues/75)) - ([db1a135](https://github.com/n0-computer/iroh-gossip/commit/db1a13550d7b014e959fe807b45c3614e26e7105))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Deny warnings for docs in CI ([#78](https://github.com/n0-computer/iroh-gossip/issues/78)) - ([b38b38f](https://github.com/n0-computer/iroh-gossip/commit/b38b38fc5970164a3c037b4d6306d8b7aee10f4f))
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- Improve simulator ([#52](https://github.com/n0-computer/iroh-gossip/issues/52)) - ([8c30674](https://github.com/n0-computer/iroh-gossip/commit/8c306742c5823f8a6655252b1dbbbfb021c3400d))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Update clippy ([#79](https://github.com/n0-computer/iroh-gossip/issues/79)) - ([07b7b77](https://github.com/n0-computer/iroh-gossip/commit/07b7b77a8ceacad8094ec83209aa3d701a63d5b4))
|
||||
- Upgrade to `iroh` at `0.90.0` and `irpc` at `0.5.0` ([#80](https://github.com/n0-computer/iroh-gossip/issues/80)) - ([0e613d8](https://github.com/n0-computer/iroh-gossip/commit/0e613d884e95203940d94b3b5363c972f4ef00d1))
|
||||
|
||||
### Change
|
||||
|
||||
- *(hyparview)* Send a ShuffleReply before disconnecting ([#59](https://github.com/n0-computer/iroh-gossip/issues/59)) - ([fd379fc](https://github.com/n0-computer/iroh-gossip/commit/fd379fc5f32ee52c2c7aad03c03c373c2ac69816))
|
||||
|
||||
## [0.35.0](https://github.com/n0-computer/iroh-gossip/compare/v0.34.1..0.35.0) - 2025-05-12
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Respect max message size when constructing IHave messages ([#63](https://github.com/n0-computer/iroh-gossip/issues/63)) - ([77c56f1](https://github.com/n0-computer/iroh-gossip/commit/77c56f1a769e561d1c8b91ebed6e02e7792bc2cb))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- [**breaking**] Use new iroh-metrics version, no more global tracking ([#58](https://github.com/n0-computer/iroh-gossip/issues/58)) - ([2a37214](https://github.com/n0-computer/iroh-gossip/commit/2a372144b08f6db43f67536e8694659b4b326698))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Update dependencies ([#66](https://github.com/n0-computer/iroh-gossip/issues/66)) - ([dbec9b0](https://github.com/n0-computer/iroh-gossip/commit/dbec9b033cded5aa3e09b0c80d52bed697dfe880))
|
||||
- Update to `iroh` v0.35 ([#68](https://github.com/n0-computer/iroh-gossip/issues/68)) - ([e6af27d](https://github.com/n0-computer/iroh-gossip/commit/e6af27d924db780e00b10017b18d4da3ef8db18a))
|
||||
|
||||
## [0.34.1](https://github.com/n0-computer/iroh-gossip/compare/v0.34.0..0.34.1) - 2025-03-24
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Allow instant reconnects, and always prefer newest connection ([#43](https://github.com/n0-computer/iroh-gossip/issues/43)) - ([ea1c773](https://github.com/n0-computer/iroh-gossip/commit/ea1c773659f88d7eed776b6b15cc0e559267afea))
|
||||
|
||||
## [0.34.0](https://github.com/n0-computer/iroh-gossip/compare/v0.33.0..0.34.0) - 2025-03-18
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Repo link for flaky tests ([#38](https://github.com/n0-computer/iroh-gossip/issues/38)) - ([0a03543](https://github.com/n0-computer/iroh-gossip/commit/0a03543db6aaedb7ac403e38360d5a1afc88b3f4))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Patch to use main branch of iroh dependencies ([#40](https://github.com/n0-computer/iroh-gossip/issues/40)) - ([d76305d](https://github.com/n0-computer/iroh-gossip/commit/d76305da7d75639638efcd537a1ffb13d07ef1ee))
|
||||
- Update to latest iroh ([#42](https://github.com/n0-computer/iroh-gossip/issues/42)) - ([129e2e8](https://github.com/n0-computer/iroh-gossip/commit/129e2e80ec7a6efd29606fcdaf0202791a25778f))
|
||||
|
||||
## [0.33.0](https://github.com/n0-computer/iroh-gossip/compare/v0.32.0..0.33.0) - 2025-02-25
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- Compile to wasm and run in browsers ([#37](https://github.com/n0-computer/iroh-gossip/issues/37)) - ([8f99f7d](https://github.com/n0-computer/iroh-gossip/commit/8f99f7d85fd8c410512b430a4ee2efd014828550))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Patch to use main branch of iroh dependencies ([#36](https://github.com/n0-computer/iroh-gossip/issues/36)) - ([7e16be8](https://github.com/n0-computer/iroh-gossip/commit/7e16be85dbf52af721aa8bb4c68723c029ce4bd2))
|
||||
- Upgrade to latest `iroh` and `quic-rpc` ([#39](https://github.com/n0-computer/iroh-gossip/issues/39)) - ([a2ef813](https://github.com/n0-computer/iroh-gossip/commit/a2ef813c6033f1683162bb09d50f1f988f774cbe))
|
||||
|
||||
## [0.32.0](https://github.com/n0-computer/iroh-gossip/compare/v0.31.0..0.32.0) - 2025-02-04
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- [**breaking**] Use explicit errors ([#34](https://github.com/n0-computer/iroh-gossip/issues/34)) - ([534f010](https://github.com/n0-computer/iroh-gossip/commit/534f01046332a21f6356d189c686f7c6c17af3c2))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Pin nextest version ([#29](https://github.com/n0-computer/iroh-gossip/issues/29)) - ([72b32d2](https://github.com/n0-computer/iroh-gossip/commit/72b32d25e8a810011456a2740581b3b3802f1cab))
|
||||
- Remove individual repo project tracking ([#31](https://github.com/n0-computer/iroh-gossip/issues/31)) - ([8a79db6](https://github.com/n0-computer/iroh-gossip/commit/8a79db65a928ae0610d85301b009d3ec13b0fbe1))
|
||||
- Update dependencies ([#35](https://github.com/n0-computer/iroh-gossip/issues/35)) - ([3c257a1](https://github.com/n0-computer/iroh-gossip/commit/3c257a1db9ea0ade0c35b060a28b1287321a532a))
|
||||
|
||||
## [0.31.0](https://github.com/n0-computer/iroh-gossip/compare/v0.30.1..0.31.0) - 2025-01-14
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Add project tracking ([#28](https://github.com/n0-computer/iroh-gossip/issues/28)) - ([bf89c85](https://github.com/n0-computer/iroh-gossip/commit/bf89c85c3ffa78fea462d5ad7c7bae10f828d7b0))
|
||||
- Upgrade to `iroh@v0.31.0` ([#30](https://github.com/n0-computer/iroh-gossip/issues/30)) - ([60f371e](https://github.com/n0-computer/iroh-gossip/commit/60f371ec61992889c390d64611e907a491812b96))
|
||||
|
||||
## [0.30.1](https://github.com/n0-computer/iroh-gossip/compare/v0.30.0..0.30.1) - 2024-12-20
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Add missing Sync bound to EventStream's inner - ([d7039c4](https://github.com/n0-computer/iroh-gossip/commit/d7039c4684e0072bce1c1fe4bce7d39ba42e8390))
|
||||
|
||||
## [0.30.0](https://github.com/n0-computer/iroh-gossip/compare/v0.29.0..0.30.0) - 2024-12-17
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- Remove rpc from default features - ([10e9b68](https://github.com/n0-computer/iroh-gossip/commit/10e9b685f6ede483ace4be4360466a111dfcfec4))
|
||||
- [**breaking**] Introduce builder pattern to construct Gossip ([#17](https://github.com/n0-computer/iroh-gossip/issues/17)) - ([0e6fd20](https://github.com/n0-computer/iroh-gossip/commit/0e6fd20203c6468af9d783f1e62379eca283188a))
|
||||
- Update to iroh 0.30 - ([b3a5a33](https://github.com/n0-computer/iroh-gossip/commit/b3a5a33351b57e01cba816826d642f3314f00e7d))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Improve connection handling ([#22](https://github.com/n0-computer/iroh-gossip/issues/22)) - ([61e64c7](https://github.com/n0-computer/iroh-gossip/commit/61e64c79961640cd2aa2412e607035cd7750f824))
|
||||
- Prevent task leak for rpc handler task ([#20](https://github.com/n0-computer/iroh-gossip/issues/20)) - ([03db85d](https://github.com/n0-computer/iroh-gossip/commit/03db85d218738df7b4c39cc2d178f2f90ba58ea3))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Adapt ProtocolHandler impl ([#16](https://github.com/n0-computer/iroh-gossip/issues/16)) - ([d5285e7](https://github.com/n0-computer/iroh-gossip/commit/d5285e7240da4e233be7c8f83099741f6f272bb0))
|
||||
- [**breaking**] Align api naming between RPC and direct calls - ([35d73db](https://github.com/n0-computer/iroh-gossip/commit/35d73db8a982d7bbe1eb3cba126ac25422f5c1b6))
|
||||
- Manually track dials, instead of using `iroh::dialer` ([#21](https://github.com/n0-computer/iroh-gossip/issues/21)) - ([2d90828](https://github.com/n0-computer/iroh-gossip/commit/2d90828a682574e382f5b0fbc43395ff698a63e2))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Add "Getting Started" to the README and add the readme to the docs ([#19](https://github.com/n0-computer/iroh-gossip/issues/19)) - ([1625123](https://github.com/n0-computer/iroh-gossip/commit/1625123a89278cb09827abe8e7ee2bf409cf2f20))
|
||||
|
||||
## [0.29.0](https://github.com/n0-computer/iroh-gossip/compare/v0.28.1..0.29.0) - 2024-12-04
|
||||
|
||||
### ⛰️ Features
|
||||
|
||||
- Add cli - ([16f3505](https://github.com/n0-computer/iroh-gossip/commit/16f35050fe47534052e79dcbca42da4212dc6256))
|
||||
- Update to latest iroh ([#11](https://github.com/n0-computer/iroh-gossip/issues/11)) - ([89e91a3](https://github.com/n0-computer/iroh-gossip/commit/89e91a34bd046fb7fbd504b2b8d0849e2865d410))
|
||||
- Reexport ALPN at top level - ([7a0ec63](https://github.com/n0-computer/iroh-gossip/commit/7a0ec63a0ab7f14d78c77f8c779b2abef956da40))
|
||||
- Update to iroh@0.29.0 - ([a28327c](https://github.com/n0-computer/iroh-gossip/commit/a28327ca512407a18a3802800c6712adc33acf84))
|
||||
|
||||
### 🚜 Refactor
|
||||
|
||||
- Use hex for debugging and display - ([b487112](https://github.com/n0-computer/iroh-gossip/commit/b4871121ed1862da4459353f63415d8ae4b3f8c5))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Fixup deny.toml - ([e614d86](https://github.com/n0-computer/iroh-gossip/commit/e614d86c0a690ac4acb6b4ef394a0bf55662dcc7))
|
||||
- Prune some deps ([#8](https://github.com/n0-computer/iroh-gossip/issues/8)) - ([ba0f6b0](https://github.com/n0-computer/iroh-gossip/commit/ba0f6b0f54a740d8eae7ee6683f4aa1d8d8c8eb2))
|
||||
- Init changelog - ([3eb675b](https://github.com/n0-computer/iroh-gossip/commit/3eb675b6a1ad51279ce225d0b36ef9957f17aa06))
|
||||
- Fix changelog generation - ([95a4611](https://github.com/n0-computer/iroh-gossip/commit/95a4611aafee248052d3dc9ef97c9bc8a26d4821))
|
||||
|
||||
## [0.28.1](https://github.com/n0-computer/iroh-gossip/compare/v0.28.0..v0.28.1) - 2024-11-04
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- Update to quic-rpc@0.14 - ([7b73408](https://github.com/n0-computer/iroh-gossip/commit/7b73408e80381b77534ae3721be0421da110de80))
|
||||
- Use correctly patched iroh-net - ([276e36a](https://github.com/n0-computer/iroh-gossip/commit/276e36aa1caff8d41f89d57d8aef229ffa9924cb))
|
||||
|
||||
### ⚙️ Miscellaneous Tasks
|
||||
|
||||
- Release iroh-gossip version 0.28.1 - ([efce3e1](https://github.com/n0-computer/iroh-gossip/commit/efce3e1dc991c15a7f1fc6f579f04876a22a7b1e))
|
||||
|
||||
|
||||
4965
third_party/iroh-org/iroh-gossip/Cargo.lock
generated
vendored
Normal file
4965
third_party/iroh-org/iroh-gossip/Cargo.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
172
third_party/iroh-org/iroh-gossip/Cargo.toml
vendored
Normal file
172
third_party/iroh-org/iroh-gossip/Cargo.toml
vendored
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
[package]
|
||||
name = "iroh-gossip"
|
||||
version = "0.96.0"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
description = "gossip messages over broadcast trees"
|
||||
license = "MIT/Apache-2.0"
|
||||
authors = ["n0 team"]
|
||||
repository = "https://github.com/n0-computer/iroh-gossip"
|
||||
resolver = "2"
|
||||
|
||||
# Sadly this also needs to be updated in .github/workflows/ci.yml
|
||||
rust-version = "1.89"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[lints.rust]
|
||||
missing_debug_implementations = "warn"
|
||||
|
||||
# We use this --cfg for documenting the cargo features on which an API
|
||||
# is available. To preview this locally use: RUSTFLAGS="--cfg
|
||||
# iroh_docsrs cargo +nightly doc --all-features". We use our own
|
||||
# iroh_docsrs instead of the common docsrs to avoid also enabling this
|
||||
# feature in any dependencies, because some indirect dependencies
|
||||
# require a feature enabled when using `--cfg docsrs` which we can not
|
||||
# do. To enable for a crate set `#![cfg_attr(iroh_docsrs,
|
||||
# feature(doc_cfg))]` in the crate.
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(iroh_docsrs)"] }
|
||||
|
||||
[lints.clippy]
|
||||
unused-async = "warn"
|
||||
|
||||
[dependencies]
|
||||
blake3 = "1.8"
|
||||
bytes = { version = "1.7", features = ["serde"] }
|
||||
data-encoding = "2.6.0"
|
||||
derive_more = { version = "2.0.1", features = [
|
||||
"add",
|
||||
"debug",
|
||||
"deref",
|
||||
"display",
|
||||
"from",
|
||||
"try_into",
|
||||
"into",
|
||||
] }
|
||||
ed25519-dalek = { version = "3.0.0-pre.1", features = ["serde", "rand_core"] }
|
||||
hex = "0.4.3"
|
||||
indexmap = "2.0"
|
||||
iroh-metrics = { version = "0.38", default-features = false }
|
||||
iroh-base = { version = "0.96", default-features = false, features = ["key"] }
|
||||
n0-future = "0.3"
|
||||
postcard = { version = "1", default-features = false, features = [
|
||||
"alloc",
|
||||
"use-std",
|
||||
"experimental-derive",
|
||||
] }
|
||||
rand = { version = "0.9.2", features = ["std_rng"] }
|
||||
serde = { version = "1.0.164", features = ["derive"] }
|
||||
|
||||
# net dependencies (optional)
|
||||
futures-lite = { version = "2.3", optional = true }
|
||||
futures-concurrency = { version = "7.6.1", optional = true }
|
||||
futures-util = { version = "0.3.30", optional = true }
|
||||
iroh = { version = "0.96", default-features = false, optional = true }
|
||||
tokio = { version = "1", optional = true, features = ["io-util", "sync"] }
|
||||
tokio-util = { version = "0.7.12", optional = true, features = ["codec"] }
|
||||
tracing = "0.1"
|
||||
irpc = { version = "0.12.0", optional = true, default-features = false, features = [
|
||||
"derive",
|
||||
"stream",
|
||||
"spans",
|
||||
] }
|
||||
n0-error = { version = "0.1", features = ["anyhow"] }
|
||||
|
||||
# rpc dependencies (optional)
|
||||
quinn = { package = "iroh-quinn", version = "0.16.0", optional = true }
|
||||
|
||||
# test-utils dependencies (optional)
|
||||
rand_chacha = { version = "0.9", optional = true }
|
||||
humantime-serde = { version = "1.1.1", optional = true }
|
||||
|
||||
# simulator dependencies (optional)
|
||||
clap = { version = "4", features = ["derive"], optional = true }
|
||||
toml = { version = "0.9.8", optional = true }
|
||||
tracing-subscriber = { version = "0.3", features = [
|
||||
"env-filter",
|
||||
], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
rayon = { version = "1.10.0", optional = true }
|
||||
comfy-table = { version = "7.1.4", optional = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = [
|
||||
"io-util",
|
||||
"sync",
|
||||
"rt",
|
||||
"macros",
|
||||
"net",
|
||||
"fs",
|
||||
] }
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
humantime-serde = { version = "1.1.1" }
|
||||
iroh = { version = "0.96", default-features = false, features = [
|
||||
"metrics",
|
||||
"test-utils",
|
||||
] }
|
||||
rand_chacha = "0.9"
|
||||
testresult = "0.4.1"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||
n0-tracing-test = "0.3"
|
||||
url = "2.4.0"
|
||||
serde-byte-array = "0.1.2"
|
||||
|
||||
[features]
|
||||
default = ["net", "metrics"]
|
||||
net = [
|
||||
"dep:irpc",
|
||||
"dep:futures-lite",
|
||||
"dep:iroh",
|
||||
"dep:tokio",
|
||||
"dep:tokio-util",
|
||||
"dep:futures-util",
|
||||
"dep:futures-concurrency",
|
||||
]
|
||||
rpc = [
|
||||
"dep:irpc",
|
||||
"dep:tokio",
|
||||
"dep:quinn",
|
||||
"irpc/rpc",
|
||||
"irpc/quinn_endpoint_setup",
|
||||
]
|
||||
test-utils = ["dep:rand_chacha", "dep:humantime-serde"]
|
||||
simulator = [
|
||||
"test-utils",
|
||||
"dep:tracing-subscriber",
|
||||
"dep:toml",
|
||||
"dep:clap",
|
||||
"dep:serde_json",
|
||||
"dep:rayon",
|
||||
"dep:comfy-table",
|
||||
]
|
||||
metrics = ["iroh-metrics/metrics"]
|
||||
examples = ["net"]
|
||||
|
||||
[[test]]
|
||||
name = "sim"
|
||||
path = "tests/sim.rs"
|
||||
required-features = ["test-utils"]
|
||||
|
||||
[[bin]]
|
||||
name = "sim"
|
||||
required-features = ["simulator"]
|
||||
|
||||
[[example]]
|
||||
name = "chat"
|
||||
required-features = ["examples"]
|
||||
|
||||
[[example]]
|
||||
name = "setup"
|
||||
required-features = ["examples"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "iroh_docsrs"]
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
201
third_party/iroh-org/iroh-gossip/LICENSE-APACHE
vendored
Normal file
201
third_party/iroh-org/iroh-gossip/LICENSE-APACHE
vendored
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2023] [N0, INC]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
25
third_party/iroh-org/iroh-gossip/LICENSE-MIT
vendored
Normal file
25
third_party/iroh-org/iroh-gossip/LICENSE-MIT
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
Copyright 2023 N0, INC.
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
28
third_party/iroh-org/iroh-gossip/Makefile.toml
vendored
Normal file
28
third_party/iroh-org/iroh-gossip/Makefile.toml
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# Use cargo-make to run tasks here: https://crates.io/crates/cargo-make
|
||||
|
||||
[tasks.format]
|
||||
workspace = false
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"--",
|
||||
"--config",
|
||||
"unstable_features=true",
|
||||
"--config",
|
||||
"imports_granularity=Crate,group_imports=StdExternalCrate,reorder_imports=true",
|
||||
]
|
||||
|
||||
[tasks.format-check]
|
||||
workspace = false
|
||||
command = "cargo"
|
||||
args = [
|
||||
"fmt",
|
||||
"--all",
|
||||
"--check",
|
||||
"--",
|
||||
"--config",
|
||||
"unstable_features=true",
|
||||
"--config",
|
||||
"imports_granularity=Crate,group_imports=StdExternalCrate,reorder_imports=true",
|
||||
]
|
||||
93
third_party/iroh-org/iroh-gossip/README.md
vendored
Normal file
93
third_party/iroh-org/iroh-gossip/README.md
vendored
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# iroh-gossip
|
||||
|
||||
This crate implements the `iroh-gossip` protocol.
|
||||
It is based on *epidemic broadcast trees* to disseminate messages among a swarm of peers interested in a *topic*.
|
||||
The implementation is based on the papers [HyParView](https://asc.di.fct.unl.pt/~jleitao/pdf/dsn07-leitao.pdf) and [PlumTree](https://asc.di.fct.unl.pt/~jleitao/pdf/srds07-leitao.pdf).
|
||||
|
||||
The crate is made up from two modules:
|
||||
The `proto` module is the protocol implementation, as a state machine without any IO.
|
||||
The `net` module implements networking logic for running `iroh-gossip` on `iroh` connections.
|
||||
|
||||
The `net` module is optional behind the `net` feature flag (enabled by default).
|
||||
|
||||
# Getting Started
|
||||
|
||||
The `iroh-gossip` protocol was designed to be used in conjunction with `iroh`. [Iroh](https://docs.rs/iroh) is a networking library for making direct connections, these connections are how gossip messages are sent.
|
||||
|
||||
Iroh provides a [`Router`](https://docs.rs/iroh/latest/iroh/protocol/struct.Router.html) that takes an [`Endpoint`](https://docs.rs/iroh/latest/iroh/endpoint/struct.Endpoint.html) and any protocols needed for the application. Similar to a router in webserver library, it runs a loop accepting incoming connections and routes them to the specific protocol handler, based on `ALPN`.
|
||||
|
||||
Here is a basic example of how to set up `iroh-gossip` with `iroh`:
|
||||
```rust,no_run
|
||||
use iroh::{protocol::Router, Endpoint, EndpointId};
|
||||
use iroh_gossip::{api::Event, Gossip, TopicId};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
use n0_future::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// create an iroh endpoint that includes the standard discovery mechanisms
|
||||
// we've built at number0
|
||||
let endpoint = Endpoint::bind().await?;
|
||||
|
||||
// build gossip protocol
|
||||
let gossip = Gossip::builder().spawn(endpoint.clone());
|
||||
|
||||
// setup router
|
||||
let router = Router::builder(endpoint)
|
||||
.accept(iroh_gossip::ALPN, gossip.clone())
|
||||
.spawn();
|
||||
|
||||
// gossip swarms are centered around a shared "topic id", which is a 32 byte identifier
|
||||
let topic_id = TopicId::from_bytes([23u8; 32]);
|
||||
// and you need some bootstrap peers to join the swarm
|
||||
let bootstrap_peers = bootstrap_peers();
|
||||
|
||||
// then, you can subscribe to the topic and join your initial peers
|
||||
let (sender, mut receiver) = gossip
|
||||
.subscribe(topic_id, bootstrap_peers)
|
||||
.await?
|
||||
.split();
|
||||
|
||||
// you might want to wait until you joined at least one other peer:
|
||||
receiver.joined().await?;
|
||||
|
||||
// then, you can broadcast messages to all other peers!
|
||||
sender.broadcast(b"hello world this is a gossip message".to_vec().into()).await?;
|
||||
|
||||
// and read messages from others!
|
||||
while let Some(event) = receiver.next().await {
|
||||
match event? {
|
||||
Event::Received(message) => {
|
||||
println!("received a message: {:?}", std::str::from_utf8(&message.content));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// clean shutdown makes sure that other peers are notified that you went offline
|
||||
router.shutdown().await.std_context("shutdown router")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bootstrap_peers() -> Vec<EndpointId> {
|
||||
// insert your bootstrap peers here, or get them from your environment
|
||||
vec![]
|
||||
}
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
<http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in this project by you, as defined in the Apache-2.0 license,
|
||||
shall be dual licensed as above, without any additional terms or conditions.
|
||||
64
third_party/iroh-org/iroh-gossip/cliff.toml
vendored
Normal file
64
third_party/iroh-org/iroh-gossip/cliff.toml
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
[changelog]
|
||||
# changelog header
|
||||
header = """
|
||||
# Changelog\n
|
||||
All notable changes to iroh-gossip will be documented in this file.\n
|
||||
"""
|
||||
|
||||
body = """
|
||||
{% if version %}\
|
||||
{% if previous.version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}](<REPO>/compare/{{ previous.version }}..{{ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% endif %}\
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
|
||||
{% macro commit(commit) -%}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }} - ([{{ commit.id | truncate(length=7, end="") }}](<REPO>/commit/{{ commit.id }}))\
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits
|
||||
| filter(attribute="scope")
|
||||
| sort(attribute="scope") %}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if not commit.scope -%}
|
||||
{{ self::commit(commit=commit) }}
|
||||
{% endif -%}
|
||||
{% endfor -%}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
|
||||
footer = ""
|
||||
postprocessors = [
|
||||
{ pattern = '<REPO>', replace = "https://github.com/n0-computer/iroh-gossip" },
|
||||
{ pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/n0-computer/iroh-gossip/issues/${1}))"}
|
||||
]
|
||||
|
||||
|
||||
[git]
|
||||
# regex for parsing and grouping commits
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->⛰️ Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^chore\\(release\\)", skip = true },
|
||||
{ message = "^chore\\(deps\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore|ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||
]
|
||||
13
third_party/iroh-org/iroh-gossip/code_of_conduct.md
vendored
Normal file
13
third_party/iroh-org/iroh-gossip/code_of_conduct.md
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Code of Conduct
|
||||
|
||||
Online or off, Number Zero is a harassment-free environment for everyone, regardless of gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, age or religion or technical skill level. We do not tolerate harassment of participants in any form.
|
||||
|
||||
Harassment includes verbal comments that reinforce social structures of domination related to gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, age, religion, sexual images in public spaces, deliberate intimidation, stalking, following, harassing photography or recording, sustained disruption of talks or other events, inappropriate physical contact, and unwelcome sexual attention. Participants asked to stop any harassing behavior are expected to comply immediately.
|
||||
|
||||
If a participant engages in harassing behaviour, the organizers may take any action they deem appropriate, including warning the offender or expulsion from events and online forums.
|
||||
|
||||
If you are being harassed, notice that someone else is being harassed, or have any other concerns, please contact a member of the organizing team immediately.
|
||||
|
||||
At offline events, organizers will identify themselves, and will help participants contact venue security or local law enforcement, provide escorts, or otherwise assist those experiencing harassment to feel safe for the duration of the event. We value your participation!
|
||||
|
||||
This document is based on a similar code from [EDGI](https://envirodatagov.org/) and [Civic Tech Toronto](http://civictech.ca/about-us/), itself derived from the [Recurse Center’s Social Rules](https://www.recurse.com/manual#sec-environment), and the [anti-harassment policy from the Geek Feminism Wiki](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
|
||||
41
third_party/iroh-org/iroh-gossip/deny.toml
vendored
Normal file
41
third_party/iroh-org/iroh-gossip/deny.toml
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[advisories]
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0436",
|
||||
"RUSTSEC-2023-0089",
|
||||
]
|
||||
|
||||
[bans]
|
||||
deny = [
|
||||
"aws-lc",
|
||||
"aws-lc-rs",
|
||||
"aws-lc-sys",
|
||||
"native-tls",
|
||||
"openssl",
|
||||
]
|
||||
multiple-versions = "allow"
|
||||
|
||||
[licenses]
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"Apache-2.0 WITH LLVM-exception",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"BSL-1.0",
|
||||
"CDLA-Permissive-2.0",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Zlib",
|
||||
"Unicode-3.0",
|
||||
"MPL-2.0",
|
||||
"Unlicense",
|
||||
]
|
||||
|
||||
[[licenses.clarify]]
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
name = "ring"
|
||||
|
||||
[[licenses.clarify.license-files]]
|
||||
hash = 3171872035
|
||||
path = "LICENSE"
|
||||
|
||||
[sources]
|
||||
319
third_party/iroh-org/iroh-gossip/examples/chat.rs
vendored
Normal file
319
third_party/iroh-org/iroh-gossip/examples/chat.rs
vendored
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use clap::Parser;
|
||||
use futures_lite::StreamExt;
|
||||
use iroh::{
|
||||
address_lookup::memory::MemoryLookup, Endpoint, EndpointAddr, PublicKey, RelayMode, RelayUrl,
|
||||
SecretKey,
|
||||
};
|
||||
use iroh_gossip::{
|
||||
api::{Event, GossipReceiver},
|
||||
net::{Gossip, GOSSIP_ALPN},
|
||||
proto::TopicId,
|
||||
};
|
||||
use n0_error::{bail_any, AnyError, Result, StdResultExt};
|
||||
use n0_future::task;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_byte_array::ByteArray;
|
||||
|
||||
/// Chat over iroh-gossip
|
||||
///
|
||||
/// This broadcasts signed messages over iroh-gossip and verifies signatures
|
||||
/// on received messages.
|
||||
///
|
||||
/// By default a new endpoint id is created when starting the example. To reuse your identity,
|
||||
/// set the `--secret-key` flag with the secret key printed on a previous invocation.
|
||||
///
|
||||
/// By default, the relay server run by n0 is used. To use a local relay server, run
|
||||
/// cargo run --bin iroh-relay --features iroh-relay -- --dev
|
||||
/// in another terminal and then set the `-d http://localhost:3340` flag on this example.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// secret key to derive our endpoint id from.
|
||||
#[clap(long)]
|
||||
secret_key: Option<String>,
|
||||
/// Set a custom relay server. By default, the relay server hosted by n0 will be used.
|
||||
#[clap(short, long)]
|
||||
relay: Option<RelayUrl>,
|
||||
/// Disable relay completely.
|
||||
#[clap(long)]
|
||||
no_relay: bool,
|
||||
/// Set your nickname.
|
||||
#[clap(short, long)]
|
||||
name: Option<String>,
|
||||
/// Set the bind port for our socket. By default, a random port will be used.
|
||||
#[clap(short, long, default_value = "0")]
|
||||
bind_port: u16,
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
enum Command {
|
||||
/// Open a chat room for a topic and print a ticket for others to join.
|
||||
///
|
||||
/// If no topic is provided, a new topic will be created.
|
||||
Open {
|
||||
/// Optionally set the topic id (64 bytes, as hex string).
|
||||
topic: Option<TopicId>,
|
||||
},
|
||||
/// Join a chat room from a ticket.
|
||||
Join {
|
||||
/// The ticket, as base32 string.
|
||||
ticket: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args = Args::parse();
|
||||
|
||||
// parse the cli command
|
||||
let (topic, peers) = match &args.command {
|
||||
Command::Open { topic } => {
|
||||
let topic = topic.unwrap_or_else(|| TopicId::from_bytes(rand::random()));
|
||||
println!("> opening chat room for topic {topic}");
|
||||
(topic, vec![])
|
||||
}
|
||||
Command::Join { ticket } => {
|
||||
let Ticket { topic, peers } = Ticket::from_str(ticket)?;
|
||||
println!("> joining chat room for topic {topic}");
|
||||
(topic, peers)
|
||||
}
|
||||
};
|
||||
|
||||
// parse or generate our secret key
|
||||
let secret_key = match args.secret_key {
|
||||
None => SecretKey::generate(&mut rand::rng()),
|
||||
Some(key) => key.parse()?,
|
||||
};
|
||||
println!(
|
||||
"> our secret key: {}",
|
||||
data_encoding::HEXLOWER.encode(&secret_key.to_bytes())
|
||||
);
|
||||
|
||||
// configure our relay map
|
||||
let relay_mode = match (args.no_relay, args.relay) {
|
||||
(false, None) => RelayMode::Default,
|
||||
(false, Some(url)) => RelayMode::Custom(url.into()),
|
||||
(true, None) => RelayMode::Disabled,
|
||||
(true, Some(_)) => bail_any!("You cannot set --no-relay and --relay at the same time"),
|
||||
};
|
||||
println!("> using relay servers: {}", fmt_relay_mode(&relay_mode));
|
||||
|
||||
// create a memory lookup to pass in endpoint addresses to
|
||||
let memory_lookup = MemoryLookup::new();
|
||||
|
||||
// build our magic endpoint
|
||||
let endpoint = Endpoint::builder()
|
||||
.secret_key(secret_key)
|
||||
.address_lookup(memory_lookup.clone())
|
||||
.relay_mode(relay_mode.clone())
|
||||
.bind_addr(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, args.bind_port))?
|
||||
.bind()
|
||||
.await?;
|
||||
println!("> our endpoint id: {}", endpoint.id());
|
||||
|
||||
// create the gossip protocol
|
||||
let gossip = Gossip::builder().spawn(endpoint.clone());
|
||||
|
||||
// print a ticket that includes our own endpoint id and endpoint addresses
|
||||
if !matches!(relay_mode, RelayMode::Disabled) {
|
||||
// if we are expecting a relay, wait until we get a home relay
|
||||
// before moving on
|
||||
endpoint.online().await;
|
||||
}
|
||||
let ticket = {
|
||||
let me = endpoint.addr();
|
||||
let peers = peers.iter().cloned().chain([me]).collect();
|
||||
Ticket { topic, peers }
|
||||
};
|
||||
println!("> ticket to join us: {ticket}");
|
||||
|
||||
// setup router
|
||||
let router = iroh::protocol::Router::builder(endpoint.clone())
|
||||
.accept(GOSSIP_ALPN, gossip.clone())
|
||||
.spawn();
|
||||
|
||||
// join the gossip topic by connecting to known peers, if any
|
||||
let peer_ids = peers.iter().map(|p| p.id).collect();
|
||||
if peers.is_empty() {
|
||||
println!("> waiting for peers to join us...");
|
||||
} else {
|
||||
println!("> trying to connect to {} peers...", peers.len());
|
||||
// add the peer addrs from the ticket to our endpoint's addressbook so that they can be dialed
|
||||
for peer in peers.into_iter() {
|
||||
memory_lookup.add_endpoint_info(peer);
|
||||
}
|
||||
};
|
||||
let (sender, receiver) = gossip.subscribe_and_join(topic, peer_ids).await?.split();
|
||||
println!("> connected!");
|
||||
|
||||
// broadcast our name, if set
|
||||
if let Some(name) = args.name {
|
||||
let message = Message::AboutMe { name };
|
||||
let encoded_message = SignedMessage::sign_and_encode(endpoint.secret_key(), &message)?;
|
||||
sender.broadcast(encoded_message).await?;
|
||||
}
|
||||
|
||||
// subscribe and print loop
|
||||
task::spawn(subscribe_loop(receiver));
|
||||
|
||||
// spawn an input thread that reads stdin
|
||||
// not using tokio here because they recommend this for "technical reasons"
|
||||
let (line_tx, mut line_rx) = tokio::sync::mpsc::channel(1);
|
||||
std::thread::spawn(move || input_loop(line_tx));
|
||||
|
||||
// broadcast each line we type
|
||||
println!("> type a message and hit enter to broadcast...");
|
||||
while let Some(text) = line_rx.recv().await {
|
||||
let message = Message::Message { text: text.clone() };
|
||||
let encoded_message = SignedMessage::sign_and_encode(endpoint.secret_key(), &message)?;
|
||||
sender.broadcast(encoded_message).await?;
|
||||
println!("> sent: {text}");
|
||||
}
|
||||
|
||||
// shutdown
|
||||
router.shutdown().await.anyerr()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_loop(mut receiver: GossipReceiver) -> Result<()> {
|
||||
// init a peerid -> name hashmap
|
||||
let mut names = HashMap::new();
|
||||
while let Some(event) = receiver.try_next().await? {
|
||||
if let Event::Received(msg) = event {
|
||||
let (from, message) = SignedMessage::verify_and_decode(&msg.content)?;
|
||||
match message {
|
||||
Message::AboutMe { name } => {
|
||||
names.insert(from, name.clone());
|
||||
println!("> {} is now known as {}", from.fmt_short(), name);
|
||||
}
|
||||
Message::Message { text } => {
|
||||
let name = names
|
||||
.get(&from)
|
||||
.map_or_else(|| from.fmt_short().to_string(), String::to_string);
|
||||
println!("{name}: {text}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn input_loop(line_tx: tokio::sync::mpsc::Sender<String>) -> Result<()> {
|
||||
let mut buffer = String::new();
|
||||
let stdin = std::io::stdin(); // We get `Stdin` here.
|
||||
loop {
|
||||
stdin.read_line(&mut buffer).anyerr()?;
|
||||
line_tx.blocking_send(buffer.clone()).anyerr()?;
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
|
||||
const SIGNATURE_LENGTH: usize = iroh::Signature::LENGTH;
|
||||
type Signature = ByteArray<SIGNATURE_LENGTH>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SignedMessage {
|
||||
from: PublicKey,
|
||||
data: Bytes,
|
||||
signature: Signature,
|
||||
}
|
||||
|
||||
impl SignedMessage {
|
||||
pub fn verify_and_decode(bytes: &[u8]) -> Result<(PublicKey, Message)> {
|
||||
let signed_message: Self =
|
||||
postcard::from_bytes(bytes).std_context("decode signed message")?;
|
||||
let key: PublicKey = signed_message.from;
|
||||
key.verify(
|
||||
&signed_message.data,
|
||||
&iroh::Signature::from_bytes(&signed_message.signature),
|
||||
)
|
||||
.std_context("verify signature")?;
|
||||
let message: Message =
|
||||
postcard::from_bytes(&signed_message.data).std_context("decode message")?;
|
||||
Ok((signed_message.from, message))
|
||||
}
|
||||
|
||||
pub fn sign_and_encode(secret_key: &SecretKey, message: &Message) -> Result<Bytes> {
|
||||
let data: Bytes = postcard::to_stdvec(&message)
|
||||
.std_context("encode message")?
|
||||
.into();
|
||||
let signature = secret_key.sign(&data);
|
||||
let from: PublicKey = secret_key.public();
|
||||
let signed_message = Self {
|
||||
from,
|
||||
data,
|
||||
signature: ByteArray::new(signature.to_bytes()),
|
||||
};
|
||||
let encoded = postcard::to_stdvec(&signed_message).std_context("encode signed message")?;
|
||||
Ok(encoded.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
enum Message {
|
||||
AboutMe { name: String },
|
||||
Message { text: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct Ticket {
|
||||
topic: TopicId,
|
||||
peers: Vec<EndpointAddr>,
|
||||
}
|
||||
impl Ticket {
|
||||
/// Deserializes from bytes.
|
||||
fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||
postcard::from_bytes(bytes).std_context("decode ticket")
|
||||
}
|
||||
/// Serializes to bytes.
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
postcard::to_stdvec(self).expect("postcard::to_stdvec is infallible")
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes to base32.
|
||||
impl fmt::Display for Ticket {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut text = data_encoding::BASE32_NOPAD.encode(&self.to_bytes()[..]);
|
||||
text.make_ascii_lowercase();
|
||||
write!(f, "{text}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes from base32.
|
||||
impl FromStr for Ticket {
|
||||
type Err = AnyError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let bytes = data_encoding::BASE32_NOPAD
|
||||
.decode(s.to_ascii_uppercase().as_bytes())
|
||||
.std_context("decode ticket base32")?;
|
||||
Self::from_bytes(&bytes)
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
fn fmt_relay_mode(relay_mode: &RelayMode) -> String {
|
||||
match relay_mode {
|
||||
RelayMode::Disabled => "None".to_string(),
|
||||
RelayMode::Default => "Default Relay (production) servers".to_string(),
|
||||
RelayMode::Staging => "Default Relay (staging) servers".to_string(),
|
||||
RelayMode::Custom(map) => map
|
||||
.urls::<Vec<_>>()
|
||||
.into_iter()
|
||||
.map(|url| url.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
}
|
||||
}
|
||||
21
third_party/iroh-org/iroh-gossip/examples/setup.rs
vendored
Normal file
21
third_party/iroh-org/iroh-gossip/examples/setup.rs
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use iroh::{protocol::Router, Endpoint};
|
||||
use iroh_gossip::{net::Gossip, ALPN};
|
||||
use n0_error::{Result, StdResultExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// create an iroh endpoint that includes the standard address lookup mechanisms
|
||||
// we've built at number0
|
||||
let endpoint = Endpoint::bind().await?;
|
||||
|
||||
// build gossip protocol
|
||||
let gossip = Gossip::builder().spawn(endpoint.clone());
|
||||
|
||||
// setup router
|
||||
let router = Router::builder(endpoint.clone())
|
||||
.accept(ALPN, gossip.clone())
|
||||
.spawn();
|
||||
// do fun stuff with the gossip protocol
|
||||
router.shutdown().await.std_context("shutdown router")?;
|
||||
Ok(())
|
||||
}
|
||||
1
third_party/iroh-org/iroh-gossip/release.toml
vendored
Normal file
1
third_party/iroh-org/iroh-gossip/release.toml
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pre-release-hook = ["git", "cliff", "--prepend", "CHANGELOG.md", "--tag", "{{version}}", "--unreleased" ]
|
||||
35
third_party/iroh-org/iroh-gossip/simulations/all.toml
vendored
Normal file
35
third_party/iroh-org/iroh-gossip/simulations/all.toml
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
seeds = [0, 1, 212312388123, 123]
|
||||
config.latency.dynamic = { min = "10ms", max = "50ms" }
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipSingle"
|
||||
nodes = 20
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipSingle"
|
||||
nodes = 100
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipSingle"
|
||||
nodes = 1000
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipMulti"
|
||||
nodes = 20
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipMulti"
|
||||
nodes = 100
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipMulti"
|
||||
nodes = 1000
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipAll"
|
||||
nodes = 20
|
||||
|
||||
[[scenario]]
|
||||
sim = "GossipAll"
|
||||
nodes = 100
|
||||
rounds = 5
|
||||
535
third_party/iroh-org/iroh-gossip/src/api.rs
vendored
Normal file
535
third_party/iroh-org/iroh-gossip/src/api.rs
vendored
Normal file
|
|
@ -0,0 +1,535 @@
|
|||
//! Public API for using iroh-gossip
|
||||
//!
|
||||
//! The API is usable both locally and over RPC.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use iroh_base::EndpointId;
|
||||
use irpc::{channel::mpsc, rpc_requests, Client};
|
||||
use n0_error::{e, stack_error};
|
||||
use n0_future::{Stream, StreamExt, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::proto::{DeliveryScope, TopicId};
|
||||
|
||||
/// Default channel capacity for topic subscription channels (one per topic)
|
||||
const TOPIC_EVENTS_DEFAULT_CAP: usize = 2048;
|
||||
/// Channel capacity for topic command send channels.
|
||||
const TOPIC_COMMANDS_CAP: usize = 64;
|
||||
|
||||
/// Input messages for the gossip actor.
|
||||
#[rpc_requests(message = RpcMessage, rpc_feature = "rpc")]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) enum Request {
|
||||
#[rpc(tx=mpsc::Sender<Event>, rx=mpsc::Receiver<Command>)]
|
||||
Join(JoinRequest),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct JoinRequest {
|
||||
pub topic_id: TopicId,
|
||||
pub bootstrap: BTreeSet<EndpointId>,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[stack_error(derive, add_meta, from_sources)]
|
||||
#[non_exhaustive]
|
||||
pub enum ApiError {
|
||||
#[error(transparent)]
|
||||
Rpc { source: irpc::Error },
|
||||
/// The gossip topic was closed.
|
||||
#[error("topic closed")]
|
||||
Closed,
|
||||
}
|
||||
|
||||
impl From<irpc::channel::SendError> for ApiError {
|
||||
fn from(value: irpc::channel::SendError) -> Self {
|
||||
irpc::Error::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<irpc::channel::mpsc::RecvError> for ApiError {
|
||||
fn from(value: irpc::channel::mpsc::RecvError) -> Self {
|
||||
irpc::Error::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<irpc::channel::oneshot::RecvError> for ApiError {
|
||||
fn from(value: irpc::channel::oneshot::RecvError) -> Self {
|
||||
irpc::Error::from(value).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// API to control a [`Gossip`] instance.
|
||||
///
|
||||
/// This has methods to subscribe and join gossip topics, which return handles to publish
|
||||
/// and receive messages on topics.
|
||||
///
|
||||
/// [`Gossip`] derefs to [`GossipApi`], so all functions on [`GossipApi`] are directly callable
|
||||
/// from [`Gossip`].
|
||||
///
|
||||
/// Additionally, a [`GossipApi`] can be created by connecting to an RPC server. See [`Gossip::listen`]
|
||||
/// and [`GossipApi::connect`] (*requires the `rpc` feature).
|
||||
///
|
||||
/// [`Gossip`]: crate::net::Gossip
|
||||
/// [`Gossip::listen`]: crate::net::Gossip::listen
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GossipApi {
|
||||
client: Client<Request>,
|
||||
}
|
||||
|
||||
impl GossipApi {
|
||||
#[cfg(feature = "net")]
|
||||
pub(crate) fn local(tx: tokio::sync::mpsc::Sender<RpcMessage>) -> Self {
|
||||
let local = irpc::LocalSender::<Request>::from(tx);
|
||||
Self {
|
||||
client: local.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to a remote as a RPC client.
|
||||
#[cfg(feature = "rpc")]
|
||||
pub fn connect(endpoint: quinn::Endpoint, addr: std::net::SocketAddr) -> Self {
|
||||
let inner = irpc::Client::quinn(endpoint, addr);
|
||||
Self { client: inner }
|
||||
}
|
||||
|
||||
/// Listen on a quinn endpoint for incoming RPC connections.
|
||||
#[cfg(all(feature = "rpc", feature = "net"))]
|
||||
pub(crate) async fn listen(&self, endpoint: quinn::Endpoint) {
|
||||
use irpc::rpc::{listen, RemoteService};
|
||||
|
||||
let local = self
|
||||
.client
|
||||
.as_local()
|
||||
.expect("cannot listen on remote client");
|
||||
let handler = Request::remote_handler(local);
|
||||
|
||||
listen::<Request>(endpoint, handler).await
|
||||
}
|
||||
|
||||
/// Join a gossip topic with options.
|
||||
///
|
||||
/// Returns a [`GossipTopic`] instantly. To wait for at least one connection to be established,
|
||||
/// you can await [`GossipTopic::joined`].
|
||||
///
|
||||
/// Messages will be queued until a first connection is available. If the internal channel becomes full,
|
||||
/// the oldest messages will be dropped from the channel.
|
||||
pub async fn subscribe_with_opts(
|
||||
&self,
|
||||
topic_id: TopicId,
|
||||
opts: JoinOptions,
|
||||
) -> Result<GossipTopic, ApiError> {
|
||||
let req = JoinRequest {
|
||||
topic_id,
|
||||
bootstrap: opts.bootstrap,
|
||||
};
|
||||
let (tx, rx) = self
|
||||
.client
|
||||
.bidi_streaming(req, TOPIC_COMMANDS_CAP, opts.subscription_capacity)
|
||||
.await?;
|
||||
Ok(GossipTopic::new(tx, rx))
|
||||
}
|
||||
|
||||
/// Join a gossip topic with the default options and wait for at least one active connection.
|
||||
pub async fn subscribe_and_join(
|
||||
&self,
|
||||
topic_id: TopicId,
|
||||
bootstrap: Vec<EndpointId>,
|
||||
) -> Result<GossipTopic, ApiError> {
|
||||
let mut sub = self
|
||||
.subscribe_with_opts(topic_id, JoinOptions::with_bootstrap(bootstrap))
|
||||
.await?;
|
||||
sub.joined().await?;
|
||||
Ok(sub)
|
||||
}
|
||||
|
||||
/// Join a gossip topic with the default options.
|
||||
///
|
||||
/// Note that this will not wait for any bootstrap endpoint to be available.
|
||||
/// To ensure the topic is connected to at least one endpoint, use [`GossipTopic::joined`]
|
||||
/// or [`Self::subscribe_and_join`]
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
topic_id: TopicId,
|
||||
bootstrap: Vec<EndpointId>,
|
||||
) -> Result<GossipTopic, ApiError> {
|
||||
let sub = self
|
||||
.subscribe_with_opts(topic_id, JoinOptions::with_bootstrap(bootstrap))
|
||||
.await?;
|
||||
|
||||
Ok(sub)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sender for a gossip topic.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GossipSender(mpsc::Sender<Command>);
|
||||
|
||||
impl GossipSender {
|
||||
pub(crate) fn new(sender: mpsc::Sender<Command>) -> Self {
|
||||
Self(sender)
|
||||
}
|
||||
|
||||
/// Broadcasts a message to all endpoints.
|
||||
pub async fn broadcast(&self, message: Bytes) -> Result<(), ApiError> {
|
||||
self.send(Command::Broadcast(message)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Broadcasts a message to our direct neighbors.
|
||||
pub async fn broadcast_neighbors(&self, message: Bytes) -> Result<(), ApiError> {
|
||||
self.send(Command::BroadcastNeighbors(message)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Joins a set of peers.
|
||||
pub async fn join_peers(&self, peers: Vec<EndpointId>) -> Result<(), ApiError> {
|
||||
self.send(Command::JoinPeers(peers)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send(&self, command: Command) -> Result<(), irpc::channel::SendError> {
|
||||
self.0.send(command).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribed gossip topic.
|
||||
///
|
||||
/// This handle is a [`Stream`] of [`Event`]s from the topic, and can be used to send messages.
|
||||
///
|
||||
/// Once the [`GossipTopic`] is dropped, the network actor will leave the gossip topic.
|
||||
///
|
||||
/// It may be split into sender and receiver parts with [`Self::split`]. In this case, the topic will
|
||||
/// be left once both the [`GossipSender`] and [`GossipReceiver`] halves are dropped.
|
||||
#[derive(Debug)]
|
||||
pub struct GossipTopic {
|
||||
sender: GossipSender,
|
||||
receiver: GossipReceiver,
|
||||
}
|
||||
|
||||
impl GossipTopic {
|
||||
pub(crate) fn new(sender: mpsc::Sender<Command>, receiver: mpsc::Receiver<Event>) -> Self {
|
||||
let sender = GossipSender::new(sender);
|
||||
Self {
|
||||
sender,
|
||||
receiver: GossipReceiver::new(receiver),
|
||||
}
|
||||
}
|
||||
|
||||
/// Splits `self` into [`GossipSender`] and [`GossipReceiver`] parts.
|
||||
pub fn split(self) -> (GossipSender, GossipReceiver) {
|
||||
(self.sender, self.receiver)
|
||||
}
|
||||
|
||||
/// Sends a message to all peers.
|
||||
pub async fn broadcast(&mut self, message: Bytes) -> Result<(), ApiError> {
|
||||
self.sender.broadcast(message).await
|
||||
}
|
||||
|
||||
/// Sends a message to our direct neighbors in the swarm.
|
||||
pub async fn broadcast_neighbors(&mut self, message: Bytes) -> Result<(), ApiError> {
|
||||
self.sender.broadcast_neighbors(message).await
|
||||
}
|
||||
|
||||
/// Lists our current direct neighbors.
|
||||
pub fn neighbors(&self) -> impl Iterator<Item = EndpointId> + '_ {
|
||||
self.receiver.neighbors()
|
||||
}
|
||||
|
||||
/// Waits until we are connected to at least one endpoint.
|
||||
///
|
||||
/// See [`GossipReceiver::joined`] for details.
|
||||
pub async fn joined(&mut self) -> Result<(), ApiError> {
|
||||
self.receiver.joined().await
|
||||
}
|
||||
|
||||
/// Returns `true` if we are connected to at least one endpoint.
|
||||
pub fn is_joined(&self) -> bool {
|
||||
self.receiver.is_joined()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for GossipTopic {
|
||||
type Item = Result<Event, ApiError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
Pin::new(&mut self.receiver).poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Receiver for gossip events on a topic.
|
||||
///
|
||||
/// This is a [`Stream`] of [`Event`]s emitted from the topic.
|
||||
#[derive(derive_more::Debug)]
|
||||
pub struct GossipReceiver {
|
||||
#[debug("BoxStream")]
|
||||
stream: Pin<Box<dyn Stream<Item = Result<Event, ApiError>> + Send + Sync + 'static>>,
|
||||
neighbors: HashSet<EndpointId>,
|
||||
}
|
||||
|
||||
impl GossipReceiver {
|
||||
pub(crate) fn new(events_rx: mpsc::Receiver<Event>) -> Self {
|
||||
let stream = events_rx.into_stream().map_err(ApiError::from);
|
||||
let stream = Box::pin(stream);
|
||||
Self {
|
||||
stream,
|
||||
neighbors: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Lists our current direct neighbors.
|
||||
pub fn neighbors(&self) -> impl Iterator<Item = EndpointId> + '_ {
|
||||
self.neighbors.iter().copied()
|
||||
}
|
||||
|
||||
/// Waits until we are connected to at least one endpoint.
|
||||
///
|
||||
/// Progresses the event stream to the first [`Event::NeighborUp`] event.
|
||||
///
|
||||
/// Note that this consumes this initial `NeighborUp` event. If you want to track
|
||||
/// neighbors, use [`Self::neighbors`] after awaiting [`Self::joined`], and then
|
||||
/// continue to track `NeighborUp` events on the event stream.
|
||||
pub async fn joined(&mut self) -> Result<(), ApiError> {
|
||||
while !self.is_joined() {
|
||||
let _event = self.next().await.ok_or(e!(ApiError::Closed))??;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` if we are connected to at least one endpoint.
|
||||
pub fn is_joined(&self) -> bool {
|
||||
!self.neighbors.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for GossipReceiver {
|
||||
type Item = Result<Event, ApiError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let item = std::task::ready!(Pin::new(&mut self.stream).poll_next(cx));
|
||||
if let Some(Ok(item)) = &item {
|
||||
match item {
|
||||
Event::NeighborUp(endpoint_id) => {
|
||||
self.neighbors.insert(*endpoint_id);
|
||||
}
|
||||
Event::NeighborDown(endpoint_id) => {
|
||||
self.neighbors.remove(endpoint_id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Poll::Ready(item)
|
||||
}
|
||||
}
|
||||
|
||||
/// Events emitted from a gossip topic.
|
||||
///
|
||||
/// These are the events emitted from a [`GossipReceiver`].
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
||||
pub enum Event {
|
||||
/// We have a new, direct neighbor in the swarm membership layer for this topic.
|
||||
NeighborUp(EndpointId),
|
||||
/// We dropped direct neighbor in the swarm membership layer for this topic.
|
||||
NeighborDown(EndpointId),
|
||||
/// We received a gossip message for this topic.
|
||||
Received(Message),
|
||||
/// We missed some messages because our [`GossipReceiver`] was not progressing fast enough.
|
||||
Lagged,
|
||||
}
|
||||
|
||||
impl From<crate::proto::Event<EndpointId>> for Event {
|
||||
fn from(event: crate::proto::Event<EndpointId>) -> Self {
|
||||
match event {
|
||||
crate::proto::Event::NeighborUp(endpoint_id) => Self::NeighborUp(endpoint_id),
|
||||
crate::proto::Event::NeighborDown(endpoint_id) => Self::NeighborDown(endpoint_id),
|
||||
crate::proto::Event::Received(message) => Self::Received(Message {
|
||||
content: message.content,
|
||||
scope: message.scope,
|
||||
delivered_from: message.delivered_from,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gossip message
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, derive_more::Debug, Serialize, Deserialize)]
|
||||
pub struct Message {
|
||||
/// The content of the message
|
||||
#[debug("Bytes({})", self.content.len())]
|
||||
pub content: Bytes,
|
||||
/// The scope of the message.
|
||||
/// This tells us if the message is from a direct neighbor or actual gossip.
|
||||
pub scope: DeliveryScope,
|
||||
/// The endpoint that delivered the message. This is not the same as the original author.
|
||||
pub delivered_from: EndpointId,
|
||||
}
|
||||
|
||||
/// Command for a gossip topic.
|
||||
#[derive(Serialize, Deserialize, derive_more::Debug, Clone)]
|
||||
pub enum Command {
|
||||
/// Broadcasts a message to all endpoints in the swarm.
|
||||
Broadcast(#[debug("Bytes({})", _0.len())] Bytes),
|
||||
/// Broadcasts a message to all direct neighbors.
|
||||
BroadcastNeighbors(#[debug("Bytes({})", _0.len())] Bytes),
|
||||
/// Connects to a set of peers.
|
||||
JoinPeers(Vec<EndpointId>),
|
||||
}
|
||||
|
||||
/// Options for joining a gossip topic.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct JoinOptions {
|
||||
/// The initial bootstrap endpoints.
|
||||
pub bootstrap: BTreeSet<EndpointId>,
|
||||
/// The maximum number of messages that can be buffered in a subscription.
|
||||
///
|
||||
/// If this limit is reached, the subscriber will receive a `Lagged` response,
|
||||
/// the message will be dropped, and the subscriber will be closed.
|
||||
///
|
||||
/// This is to prevent a single slow subscriber from blocking the dispatch loop.
|
||||
/// If a subscriber is lagging, it should be closed and re-opened.
|
||||
pub subscription_capacity: usize,
|
||||
}
|
||||
|
||||
impl JoinOptions {
|
||||
/// Creates [`JoinOptions`] with the provided bootstrap endpoints and the default subscription
|
||||
/// capacity.
|
||||
pub fn with_bootstrap(endpoints: impl IntoIterator<Item = EndpointId>) -> Self {
|
||||
Self {
|
||||
bootstrap: endpoints.into_iter().collect(),
|
||||
subscription_capacity: TOPIC_EVENTS_DEFAULT_CAP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::GossipTopic;
|
||||
|
||||
#[cfg(all(feature = "rpc", feature = "net"))]
|
||||
#[tokio::test]
|
||||
#[n0_tracing_test::traced_test]
|
||||
async fn test_rpc() -> n0_error::Result<()> {
|
||||
use iroh::{address_lookup::memory::MemoryLookup, protocol::Router, RelayMap};
|
||||
use n0_error::{AnyError, Result, StackResultExt, StdResultExt};
|
||||
use n0_future::{time::Duration, StreamExt};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
|
||||
use crate::{
|
||||
api::{Event, GossipApi},
|
||||
net::{test::create_endpoint, Gossip},
|
||||
proto::TopicId,
|
||||
ALPN,
|
||||
};
|
||||
|
||||
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(1);
|
||||
let (relay_map, _relay_url, _guard) = iroh::test_utils::run_relay_server().await.unwrap();
|
||||
|
||||
async fn create_gossip_endpoint(
|
||||
rng: &mut rand_chacha::ChaCha12Rng,
|
||||
relay_map: RelayMap,
|
||||
) -> Result<(Router, Gossip)> {
|
||||
let endpoint = create_endpoint(rng, relay_map, None).await?;
|
||||
let gossip = Gossip::builder().spawn(endpoint.clone());
|
||||
let router = Router::builder(endpoint)
|
||||
.accept(ALPN, gossip.clone())
|
||||
.spawn();
|
||||
Ok((router, gossip))
|
||||
}
|
||||
|
||||
let topic_id = TopicId::from_bytes([0u8; 32]);
|
||||
|
||||
// create our gossip endpoint
|
||||
let (router, gossip) = create_gossip_endpoint(&mut rng, relay_map.clone()).await?;
|
||||
|
||||
// create a second endpoint so that we can test actually joining
|
||||
let (endpoint2_id, endpoint2_addr, endpoint2_task) = {
|
||||
let (router, gossip) = create_gossip_endpoint(&mut rng, relay_map.clone()).await?;
|
||||
let endpoint_addr = router.endpoint().addr();
|
||||
let endpoint_id = router.endpoint().id();
|
||||
let task = tokio::task::spawn(async move {
|
||||
let mut topic = gossip.subscribe_and_join(topic_id, vec![]).await?;
|
||||
topic.broadcast(b"hello".to_vec().into()).await?;
|
||||
Ok::<_, AnyError>(router)
|
||||
});
|
||||
(endpoint_id, endpoint_addr, task)
|
||||
};
|
||||
|
||||
// create a memory lookup service to add endpoint addr manually
|
||||
let memory_lookup = MemoryLookup::new();
|
||||
memory_lookup.add_endpoint_info(endpoint2_addr);
|
||||
|
||||
router.endpoint().address_lookup().add(memory_lookup);
|
||||
|
||||
// expose the gossip endpoint over RPC
|
||||
let (rpc_server_endpoint, rpc_server_cert) =
|
||||
irpc::util::make_server_endpoint("127.0.0.1:0".parse().unwrap())
|
||||
.context("make server endpoint")?;
|
||||
let rpc_server_addr = rpc_server_endpoint
|
||||
.local_addr()
|
||||
.std_context("resolve server addr")?;
|
||||
let rpc_server_task = tokio::task::spawn(async move {
|
||||
gossip.listen(rpc_server_endpoint).await;
|
||||
});
|
||||
|
||||
// connect to the RPC endpoint with a new client
|
||||
let rpc_client_endpoint =
|
||||
irpc::util::make_client_endpoint("127.0.0.1:0".parse().unwrap(), &[&rpc_server_cert])
|
||||
.context("make client endpoint")?;
|
||||
let rpc_client = GossipApi::connect(rpc_client_endpoint, rpc_server_addr);
|
||||
|
||||
// join via RPC
|
||||
let recv = async move {
|
||||
let mut topic = rpc_client
|
||||
.subscribe_and_join(topic_id, vec![endpoint2_id])
|
||||
.await?;
|
||||
// wait for a message
|
||||
while let Some(event) = topic.try_next().await? {
|
||||
match event {
|
||||
Event::Received(message) => {
|
||||
assert_eq!(&message.content[..], b"hello");
|
||||
break;
|
||||
}
|
||||
Event::Lagged => panic!("unexpected lagged event"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok::<_, AnyError>(())
|
||||
};
|
||||
|
||||
// timeout to not hang in case of failure
|
||||
tokio::time::timeout(Duration::from_secs(10), recv)
|
||||
.await
|
||||
.std_context("rpc recv timeout")??;
|
||||
|
||||
// shutdown
|
||||
rpc_server_task.abort();
|
||||
router.shutdown().await.std_context("shutdown router")?;
|
||||
let router2 = endpoint2_task.await.std_context("join endpoint task")??;
|
||||
router2
|
||||
.shutdown()
|
||||
.await
|
||||
.std_context("shutdown second router")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_gossip_topic_is_sync() {
|
||||
#[allow(unused)]
|
||||
fn get() -> GossipTopic {
|
||||
unimplemented!()
|
||||
}
|
||||
#[allow(unused)]
|
||||
fn check(_t: impl Sync) {}
|
||||
#[allow(unused)]
|
||||
fn foo() {
|
||||
check(get());
|
||||
}
|
||||
}
|
||||
}
|
||||
418
third_party/iroh-org/iroh-gossip/src/bin/sim.rs
vendored
Normal file
418
third_party/iroh-org/iroh-gossip/src/bin/sim.rs
vendored
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use comfy_table::{presets::NOTHING, Cell, CellAlignment, Table};
|
||||
use iroh_gossip::proto::sim::{
|
||||
BootstrapMode, NetworkConfig, RoundStats, RoundStatsAvg, RoundStatsDiff, Simulator,
|
||||
SimulatorConfig,
|
||||
};
|
||||
use n0_error::{Result, StackResultExt, StdResultExt};
|
||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error_span, info, warn};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum Simulation {
|
||||
/// A single sender broadcasts a single message per round.
|
||||
GossipSingle,
|
||||
/// Each round a different sender is chosen at random, and broadcasts a single message
|
||||
GossipMulti,
|
||||
/// Each round, all peers broadcast a single message simultaneously.
|
||||
GossipAll,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct ScenarioDescription {
|
||||
sim: Simulation,
|
||||
nodes: u32,
|
||||
#[serde(default)]
|
||||
bootstrap: BootstrapMode,
|
||||
#[serde(default = "defaults::rounds")]
|
||||
rounds: u32,
|
||||
config: Option<NetworkConfig>,
|
||||
}
|
||||
|
||||
impl ScenarioDescription {
|
||||
pub fn label(&self) -> String {
|
||||
let &ScenarioDescription {
|
||||
sim,
|
||||
nodes,
|
||||
rounds,
|
||||
config: _,
|
||||
bootstrap: _,
|
||||
} = &self;
|
||||
format!("{sim:?}-n{nodes}-r{rounds}")
|
||||
}
|
||||
}
|
||||
|
||||
mod defaults {
|
||||
pub fn rounds() -> u32 {
|
||||
30
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct SimConfig {
|
||||
seeds: Vec<u64>,
|
||||
config: Option<NetworkConfig>,
|
||||
scenario: Vec<ScenarioDescription>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
enum Command {
|
||||
/// Run simulations
|
||||
Run {
|
||||
#[clap(short, long)]
|
||||
config_path: PathBuf,
|
||||
#[clap(short, long)]
|
||||
out_dir: Option<PathBuf>,
|
||||
#[clap(short, long)]
|
||||
baseline: Option<PathBuf>,
|
||||
#[clap(short, long)]
|
||||
single_threaded: bool,
|
||||
#[clap(short, long)]
|
||||
filter: Vec<String>,
|
||||
},
|
||||
/// Compare simulation runs
|
||||
Compare {
|
||||
baseline: PathBuf,
|
||||
current: PathBuf,
|
||||
#[clap(short, long)]
|
||||
filter: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let args: Cli = Cli::parse();
|
||||
match args.command {
|
||||
Command::Run {
|
||||
config_path,
|
||||
out_dir,
|
||||
baseline,
|
||||
single_threaded,
|
||||
filter,
|
||||
} => {
|
||||
let config_text = std::fs::read_to_string(&config_path)
|
||||
.with_std_context(|_| format!("read config {}", config_path.display()))?;
|
||||
let config: SimConfig = toml::from_str(&config_text).std_context("parse config")?;
|
||||
|
||||
let base_config = config.config.unwrap_or_default();
|
||||
info!("base config: {base_config:?}");
|
||||
let seeds = config.seeds;
|
||||
let mut scenarios = config.scenario;
|
||||
for scenario in scenarios.iter_mut() {
|
||||
scenario.config.get_or_insert_with(|| base_config.clone());
|
||||
}
|
||||
|
||||
if let Some(out_dir) = out_dir.as_ref() {
|
||||
std::fs::create_dir_all(out_dir)
|
||||
.with_std_context(|_| format!("create output dir {}", out_dir.display()))?;
|
||||
}
|
||||
|
||||
let filter_fn = |s: &ScenarioDescription| {
|
||||
let label = s.label();
|
||||
if filter.is_empty() {
|
||||
true
|
||||
} else {
|
||||
filter.iter().any(|x| x == &label)
|
||||
}
|
||||
};
|
||||
|
||||
let results: Result<Vec<_>> = if !single_threaded {
|
||||
scenarios
|
||||
.into_par_iter()
|
||||
.filter(filter_fn)
|
||||
.map(|scenario| run_and_save_simulation(scenario, &seeds, out_dir.as_ref()))
|
||||
.collect()
|
||||
} else {
|
||||
scenarios
|
||||
.into_iter()
|
||||
.filter(filter_fn)
|
||||
.map(|scenario| run_and_save_simulation(scenario, &seeds, out_dir.as_ref()))
|
||||
.collect()
|
||||
};
|
||||
let mut results = results?;
|
||||
results.sort_by_key(|a| a.scenario.label());
|
||||
for result in results {
|
||||
print_result(&result);
|
||||
}
|
||||
if let (Some(baseline), Some(out_dir)) = (baseline, out_dir) {
|
||||
compare_dirs(baseline, out_dir, filter)?;
|
||||
}
|
||||
}
|
||||
Command::Compare {
|
||||
baseline,
|
||||
current,
|
||||
filter,
|
||||
} => {
|
||||
compare_dirs(baseline, current, filter)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_and_save_simulation(
|
||||
scenario: ScenarioDescription,
|
||||
seeds: &[u64],
|
||||
out_dir: Option<impl AsRef<Path>>,
|
||||
) -> Result<SimulationResults> {
|
||||
let label = scenario.label();
|
||||
|
||||
if let Some(out_dir) = out_dir.as_ref() {
|
||||
let path = out_dir.as_ref().join(format!("{label}.config.toml"));
|
||||
let encoded = toml::to_string(&scenario).std_context("encode scenario")?;
|
||||
std::fs::write(&path, encoded)
|
||||
.with_std_context(|_| format!("write scenario {}", &path.display()))?;
|
||||
}
|
||||
|
||||
let result = run_simulation(seeds, scenario);
|
||||
|
||||
if let Some(out_dir) = out_dir.as_ref() {
|
||||
let path = out_dir.as_ref().join(format!("{label}.results.json"));
|
||||
let encoded = serde_json::to_string(&result).std_context("encode results")?;
|
||||
std::fs::write(&path, encoded)
|
||||
.with_std_context(|_| format!("write results {}", path.display()))?;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct SimulationResults {
|
||||
scenario: ScenarioDescription,
|
||||
/// Maps seeds to results
|
||||
results: HashMap<u64, RoundStatsAvg>,
|
||||
average: Option<RoundStatsAvg>,
|
||||
}
|
||||
|
||||
impl SimulationResults {
|
||||
fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
|
||||
let s = std::fs::read_to_string(path.as_ref())
|
||||
.with_std_context(|_| format!("read results {}", path.as_ref().display()))?;
|
||||
let out = serde_json::from_str(&s).std_context("decode results")?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
fn run_simulation(seeds: &[u64], scenario: ScenarioDescription) -> SimulationResults {
|
||||
let mut results = HashMap::new();
|
||||
let network_config = scenario.config.clone().unwrap_or_default();
|
||||
for &seed in seeds {
|
||||
let span = error_span!("sim", name=%scenario.label(), %seed);
|
||||
let _guard = span.enter();
|
||||
|
||||
let sim_config = SimulatorConfig {
|
||||
rng_seed: seed,
|
||||
peers: scenario.nodes as usize,
|
||||
..Default::default()
|
||||
};
|
||||
let bootstrap = scenario.bootstrap.clone();
|
||||
let mut simulator = Simulator::new(sim_config, network_config.clone());
|
||||
info!("start");
|
||||
let outcome = simulator.bootstrap(bootstrap);
|
||||
|
||||
if outcome.has_peers_with_no_neighbors() {
|
||||
warn!("not all nodes active after bootstrap: {outcome:?}");
|
||||
} else {
|
||||
info!("bootstrapped, all nodes active");
|
||||
}
|
||||
let result = match scenario.sim {
|
||||
Simulation::GossipSingle => BigSingle.run(simulator, scenario.rounds as usize),
|
||||
Simulation::GossipMulti => BigMulti.run(simulator, scenario.rounds as usize),
|
||||
Simulation::GossipAll => BigAll.run(simulator, scenario.rounds as usize),
|
||||
};
|
||||
info!("done");
|
||||
results.insert(seed, result);
|
||||
}
|
||||
|
||||
let stats: Vec<_> = results.values().cloned().collect();
|
||||
let average = if !stats.is_empty() {
|
||||
let avg = RoundStatsAvg::avg(&stats);
|
||||
Some(avg)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
SimulationResults {
|
||||
average,
|
||||
results,
|
||||
scenario,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result(r: &SimulationResults) {
|
||||
let seeds = r.results.len();
|
||||
println!("{} with {seeds} seeds", r.scenario.label());
|
||||
let Some(avg) = r.average.as_ref() else {
|
||||
println!("no results, simulation did not complete");
|
||||
return;
|
||||
};
|
||||
let mut table = Table::new();
|
||||
let header = ["", "RMR", "LDH", "missed", "duration"]
|
||||
.into_iter()
|
||||
.map(|s| Cell::new(s).set_alignment(CellAlignment::Right));
|
||||
table
|
||||
.load_preset(NOTHING)
|
||||
.set_header(header)
|
||||
.add_row(fmt_round("mean", &avg.mean))
|
||||
.add_row(fmt_round("max", &avg.max))
|
||||
.add_row(fmt_round("min", &avg.min));
|
||||
println!("{table}");
|
||||
if avg.max.missed > 0.0 {
|
||||
println!("WARN: Messages were missed!")
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
trait Scenario {
|
||||
fn run(self, sim: Simulator, rounds: usize) -> RoundStatsAvg;
|
||||
}
|
||||
|
||||
struct BigSingle;
|
||||
impl Scenario for BigSingle {
|
||||
fn run(self, mut simulator: Simulator, rounds: usize) -> RoundStatsAvg {
|
||||
let from = simulator.random_peer();
|
||||
for i in 0..rounds {
|
||||
let message = format!("m{i}").into_bytes().into();
|
||||
let messages = vec![(from, message)];
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
simulator.round_stats_average()
|
||||
}
|
||||
}
|
||||
|
||||
struct BigMulti;
|
||||
impl Scenario for BigMulti {
|
||||
fn run(self, mut simulator: Simulator, rounds: usize) -> RoundStatsAvg {
|
||||
for i in 0..rounds {
|
||||
let from = simulator.random_peer();
|
||||
let message = format!("m{i}").into_bytes().into();
|
||||
let messages = vec![(from, message)];
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
simulator.round_stats_average()
|
||||
}
|
||||
}
|
||||
|
||||
struct BigAll;
|
||||
impl Scenario for BigAll {
|
||||
fn run(self, mut simulator: Simulator, rounds: usize) -> RoundStatsAvg {
|
||||
let messages_per_peer = 1;
|
||||
for i in 0..rounds {
|
||||
let mut messages = vec![];
|
||||
for id in simulator.network.peer_ids() {
|
||||
for j in 0..messages_per_peer {
|
||||
let message: bytes::Bytes = format!("{i}:{j}.{id}").into_bytes().into();
|
||||
messages.push((id, message));
|
||||
}
|
||||
}
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
simulator.round_stats_average()
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_dirs(baseline_dir: PathBuf, current_path: PathBuf, filter: Vec<String>) -> Result<()> {
|
||||
let mut paths = vec![];
|
||||
for entry in std::fs::read_dir(¤t_path)
|
||||
.with_std_context(|_| format!("read directory {}", current_path.display()))?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|x| x.path().is_file())
|
||||
{
|
||||
let current_file = entry.path().to_owned();
|
||||
let Some(filename) = current_file.file_name().and_then(|s| s.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(basename) = filename.strip_suffix(".results.json") else {
|
||||
continue;
|
||||
};
|
||||
if !filter.is_empty() && !filter.iter().any(|x| x == basename) {
|
||||
continue;
|
||||
}
|
||||
let baseline_file = baseline_dir.join(filename);
|
||||
if !baseline_file.exists() {
|
||||
println!("skip {filename} (not in baseline)");
|
||||
}
|
||||
paths.push((basename.to_string(), baseline_file, current_file));
|
||||
}
|
||||
paths.sort();
|
||||
for (basename, baseline_file, current_file) in paths {
|
||||
println!("comparing {basename}");
|
||||
if let Err(err) = compare_files(&baseline_file, ¤t_file) {
|
||||
println!(" skip (reason: {err:#}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compare_files(baseline: impl AsRef<Path>, current: impl AsRef<Path>) -> Result<()> {
|
||||
let baseline =
|
||||
SimulationResults::load_from_file(baseline.as_ref()).context("failed to load baseline")?;
|
||||
let current =
|
||||
SimulationResults::load_from_file(current.as_ref()).context("failed to load current")?;
|
||||
compare_results(baseline, current);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compare_results(baseline: SimulationResults, current: SimulationResults) {
|
||||
match (baseline.average, current.average) {
|
||||
(None, Some(_avg)) => {
|
||||
println!("baseline run did not finish");
|
||||
}
|
||||
(Some(_avg), None) => {
|
||||
println!("current run did not finish");
|
||||
}
|
||||
(None, None) => println!("both runs did not finish"),
|
||||
(Some(baseline), Some(current)) => {
|
||||
let diff = baseline.diff(¤t);
|
||||
let mut table = Table::new();
|
||||
let header = ["", "RMR", "LDH", "missed", "duration"]
|
||||
.into_iter()
|
||||
.map(|s| Cell::new(s).set_alignment(CellAlignment::Right));
|
||||
table
|
||||
.load_preset(NOTHING)
|
||||
.set_header(header)
|
||||
.add_row(fmt_diff_round("mean", &diff.mean))
|
||||
.add_row(fmt_diff_round("max", &diff.max))
|
||||
.add_row(fmt_diff_round("min", &diff.min));
|
||||
println!("{table}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fmt_round(label: &str, round: &RoundStats) -> Vec<Cell> {
|
||||
[
|
||||
label.to_string(),
|
||||
format!("{:.2}", round.rmr),
|
||||
format!("{:.2}", round.ldh),
|
||||
format!("{:.2}", round.missed),
|
||||
format!("{}ms", round.duration.as_millis()),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|s| Cell::new(s).set_alignment(CellAlignment::Right))
|
||||
.collect()
|
||||
}
|
||||
fn fmt_diff_round(label: &str, round: &RoundStatsDiff) -> Vec<String> {
|
||||
vec![
|
||||
label.to_string(),
|
||||
fmt_percent(round.rmr),
|
||||
fmt_percent(round.ldh),
|
||||
fmt_percent(round.missed),
|
||||
fmt_percent(round.duration),
|
||||
]
|
||||
}
|
||||
|
||||
fn fmt_percent(diff: f32) -> String {
|
||||
format!("{:>+10.2}%", diff * 100.)
|
||||
}
|
||||
25
third_party/iroh-org/iroh-gossip/src/lib.rs
vendored
Normal file
25
third_party/iroh-org/iroh-gossip/src/lib.rs
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#![cfg_attr(feature = "net", doc = include_str!("../README.md"))]
|
||||
//! Broadcast messages to peers subscribed to a topic
|
||||
//!
|
||||
//! The crate is designed to be used from the [iroh] crate, which provides a
|
||||
//! [high level interface](https://docs.rs/iroh/latest/iroh/client/gossip/index.html),
|
||||
//! but can also be used standalone.
|
||||
//!
|
||||
//! [iroh]: https://docs.rs/iroh
|
||||
#![deny(missing_docs, rustdoc::broken_intra_doc_links)]
|
||||
#![cfg_attr(iroh_docsrs, feature(doc_cfg))]
|
||||
|
||||
#[cfg(feature = "net")]
|
||||
pub use net::Gossip;
|
||||
#[cfg(feature = "net")]
|
||||
#[doc(inline)]
|
||||
pub use net::GOSSIP_ALPN as ALPN;
|
||||
|
||||
#[cfg(any(feature = "net", feature = "rpc"))]
|
||||
pub mod api;
|
||||
pub mod metrics;
|
||||
#[cfg(feature = "net")]
|
||||
pub mod net;
|
||||
pub mod proto;
|
||||
|
||||
pub use proto::TopicId;
|
||||
45
third_party/iroh-org/iroh-gossip/src/metrics.rs
vendored
Normal file
45
third_party/iroh-org/iroh-gossip/src/metrics.rs
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
//! Metrics for iroh-gossip
|
||||
|
||||
use iroh_metrics::{Counter, MetricsGroup};
|
||||
|
||||
/// Enum of metrics for the module
|
||||
#[derive(Debug, Default, MetricsGroup)]
|
||||
#[metrics(name = "gossip")]
|
||||
pub struct Metrics {
|
||||
/// Number of control messages sent
|
||||
pub msgs_ctrl_sent: Counter,
|
||||
/// Number of control messages received
|
||||
pub msgs_ctrl_recv: Counter,
|
||||
/// Number of data messages sent
|
||||
pub msgs_data_sent: Counter,
|
||||
/// Number of data messages received
|
||||
pub msgs_data_recv: Counter,
|
||||
/// Total size of all data messages sent
|
||||
pub msgs_data_sent_size: Counter,
|
||||
/// Total size of all data messages received
|
||||
pub msgs_data_recv_size: Counter,
|
||||
/// Total size of all control messages sent
|
||||
pub msgs_ctrl_sent_size: Counter,
|
||||
/// Total size of all control messages received
|
||||
pub msgs_ctrl_recv_size: Counter,
|
||||
/// Number of times we connected to a peer
|
||||
pub neighbor_up: Counter,
|
||||
/// Number of times we disconnected from a peer
|
||||
pub neighbor_down: Counter,
|
||||
/// Number of times the main actor loop ticked
|
||||
pub actor_tick_main: Counter,
|
||||
/// Number of times the actor ticked for a message received
|
||||
pub actor_tick_rx: Counter,
|
||||
/// Number of times the actor ticked for an endpoint event
|
||||
pub actor_tick_endpoint: Counter,
|
||||
/// Number of times the actor ticked for a dialer event
|
||||
pub actor_tick_dialer: Counter,
|
||||
/// Number of times the actor ticked for a successful dialer event
|
||||
pub actor_tick_dialer_success: Counter,
|
||||
/// Number of times the actor ticked for a failed dialer event
|
||||
pub actor_tick_dialer_failure: Counter,
|
||||
/// Number of times the actor ticked for an incoming event
|
||||
pub actor_tick_in_event_rx: Counter,
|
||||
/// Number of times the actor ticked for a timer event
|
||||
pub actor_tick_timers: Counter,
|
||||
}
|
||||
1958
third_party/iroh-org/iroh-gossip/src/net.rs
vendored
Normal file
1958
third_party/iroh-org/iroh-gossip/src/net.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
175
third_party/iroh-org/iroh-gossip/src/net/address_lookup.rs
vendored
Normal file
175
third_party/iroh-org/iroh-gossip/src/net/address_lookup.rs
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
//! An address lookup service to gather addressing info collected from gossip Join and ForwardJoin messages.
|
||||
|
||||
use std::{
|
||||
collections::{btree_map::Entry, BTreeMap},
|
||||
sync::{Arc, RwLock},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use iroh::address_lookup::{self, AddressLookup, EndpointData, EndpointInfo};
|
||||
use iroh_base::EndpointId;
|
||||
use n0_future::{
|
||||
boxed::BoxStream,
|
||||
stream::{self, StreamExt},
|
||||
task::AbortOnDropHandle,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub(crate) struct RetentionOpts {
|
||||
/// How long to keep received endpoint info records alive before pruning them
|
||||
retention: Duration,
|
||||
/// How often to check for expired entries
|
||||
evict_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for RetentionOpts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
retention: Duration::from_secs(60 * 5),
|
||||
evict_interval: Duration::from_secs(30),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An address lookup service that expires endpoints after some time.
|
||||
///
|
||||
/// It is added to the endpoint when constructing a gossip instance, and the gossip actor
|
||||
/// then adds endpoint addresses as received with Join or ForwardJoin messages.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct GossipAddressLookup {
|
||||
endpoints: NodeMap,
|
||||
_task_handle: Arc<AbortOnDropHandle<()>>,
|
||||
}
|
||||
|
||||
type NodeMap = Arc<RwLock<BTreeMap<EndpointId, StoredEndpointInfo>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct StoredEndpointInfo {
|
||||
data: EndpointData,
|
||||
last_updated: SystemTime,
|
||||
}
|
||||
|
||||
impl Default for GossipAddressLookup {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl GossipAddressLookup {
|
||||
const PROVENANCE: &'static str = "gossip";
|
||||
|
||||
/// Creates a new gossip address lookup instance.
|
||||
pub(crate) fn new() -> Self {
|
||||
Self::with_opts(Default::default())
|
||||
}
|
||||
|
||||
pub(crate) fn with_opts(opts: RetentionOpts) -> Self {
|
||||
let endpoints: NodeMap = Default::default();
|
||||
let task = {
|
||||
let endpoints = Arc::downgrade(&endpoints);
|
||||
n0_future::task::spawn(async move {
|
||||
let mut interval = n0_future::time::interval(opts.evict_interval);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let Some(endpoints) = endpoints.upgrade() else {
|
||||
break;
|
||||
};
|
||||
let now = SystemTime::now();
|
||||
endpoints.write().expect("poisoned").retain(|_k, v| {
|
||||
let age = now.duration_since(v.last_updated).unwrap_or(Duration::MAX);
|
||||
age <= opts.retention
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
Self {
|
||||
endpoints,
|
||||
_task_handle: Arc::new(AbortOnDropHandle::new(task)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Augments endpoint addressing information for the given endpoint ID.
|
||||
///
|
||||
/// The provided addressing information is combined with the existing info in the in-memory
|
||||
/// lookup. Any new direct addresses are added to those already present while the
|
||||
/// relay URL is overwritten.
|
||||
pub(crate) fn add(&self, endpoint_info: impl Into<EndpointInfo>) {
|
||||
let last_updated = SystemTime::now();
|
||||
let EndpointInfo { endpoint_id, data } = endpoint_info.into();
|
||||
let mut guard = self.endpoints.write().expect("poisoned");
|
||||
match guard.entry(endpoint_id) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let existing = entry.get_mut();
|
||||
existing.data.add_addrs(data.addrs().cloned());
|
||||
existing.data.set_user_data(data.user_data().cloned());
|
||||
existing.last_updated = last_updated;
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(StoredEndpointInfo { data, last_updated });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressLookup for GossipAddressLookup {
|
||||
fn resolve(
|
||||
&self,
|
||||
endpoint_id: EndpointId,
|
||||
) -> Option<BoxStream<Result<address_lookup::Item, address_lookup::Error>>> {
|
||||
let guard = self.endpoints.read().expect("poisoned");
|
||||
let info = guard.get(&endpoint_id)?;
|
||||
let last_updated = info
|
||||
.last_updated
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("time drift")
|
||||
.as_micros() as u64;
|
||||
let item = address_lookup::Item::new(
|
||||
EndpointInfo::from_parts(endpoint_id, info.data.clone()),
|
||||
Self::PROVENANCE,
|
||||
Some(last_updated),
|
||||
);
|
||||
Some(stream::iter(Some(Ok(item))).boxed())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use iroh::{address_lookup::AddressLookup, EndpointAddr, SecretKey};
|
||||
use n0_future::StreamExt;
|
||||
use rand::SeedableRng;
|
||||
|
||||
use super::{GossipAddressLookup, RetentionOpts};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_retention() {
|
||||
let opts = RetentionOpts {
|
||||
evict_interval: Duration::from_millis(100),
|
||||
retention: Duration::from_millis(500),
|
||||
};
|
||||
let disco = GossipAddressLookup::with_opts(opts);
|
||||
|
||||
let rng = &mut rand_chacha::ChaCha12Rng::seed_from_u64(1);
|
||||
let k1 = SecretKey::generate(rng);
|
||||
let a1 = EndpointAddr::new(k1.public());
|
||||
|
||||
disco.add(a1);
|
||||
|
||||
assert!(matches!(
|
||||
disco.resolve(k1.public()).unwrap().next().await,
|
||||
Some(Ok(_))
|
||||
));
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
assert!(matches!(
|
||||
disco.resolve(k1.public()).unwrap().next().await,
|
||||
Some(Ok(_))
|
||||
));
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(700)).await;
|
||||
|
||||
assert!(disco.resolve(k1.public()).is_none());
|
||||
}
|
||||
}
|
||||
435
third_party/iroh-org/iroh-gossip/src/net/util.rs
vendored
Normal file
435
third_party/iroh-org/iroh-gossip/src/net/util.rs
vendored
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
//! Utilities for iroh-gossip networking
|
||||
|
||||
use std::{
|
||||
collections::{hash_map, HashMap},
|
||||
io,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use iroh::{
|
||||
endpoint::{Connection, RecvStream, SendStream},
|
||||
EndpointId,
|
||||
};
|
||||
use n0_error::{e, stack_error};
|
||||
use n0_future::{
|
||||
time::{sleep_until, Instant},
|
||||
FuturesUnordered, StreamExt,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
sync::mpsc,
|
||||
task::JoinSet,
|
||||
};
|
||||
use tracing::{debug, trace, Instrument};
|
||||
|
||||
use super::{InEvent, ProtoMessage};
|
||||
use crate::proto::{util::TimerMap, TopicId};
|
||||
|
||||
/// Errors related to message writing
|
||||
#[allow(missing_docs)]
|
||||
#[stack_error(derive, add_meta, from_sources)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) enum WriteError {
|
||||
/// Connection error
|
||||
#[error("Connection error")]
|
||||
Connection {
|
||||
#[error(std_err)]
|
||||
source: iroh::endpoint::ConnectionError,
|
||||
},
|
||||
/// Serialization failed
|
||||
#[error("Serialization failed")]
|
||||
Ser {
|
||||
#[error(std_err)]
|
||||
source: postcard::Error,
|
||||
},
|
||||
/// IO error
|
||||
#[error("IO error")]
|
||||
Io {
|
||||
#[error(std_err)]
|
||||
source: std::io::Error,
|
||||
},
|
||||
/// Message was larger than the configured maximum message size
|
||||
#[error("message too large")]
|
||||
TooLarge {},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct StreamHeader {
|
||||
pub(crate) topic_id: TopicId,
|
||||
}
|
||||
|
||||
impl StreamHeader {
|
||||
pub(crate) async fn read(
|
||||
stream: &mut RecvStream,
|
||||
buffer: &mut BytesMut,
|
||||
max_message_size: usize,
|
||||
) -> Result<Self, ReadError> {
|
||||
let header: Self = read_frame(stream, buffer, max_message_size)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ReadError::from(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"stream ended before header",
|
||||
))
|
||||
})?;
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
pub(crate) async fn write(
|
||||
self,
|
||||
stream: &mut SendStream,
|
||||
buffer: &mut Vec<u8>,
|
||||
max_message_size: usize,
|
||||
) -> Result<(), WriteError> {
|
||||
write_frame(stream, &self, buffer, max_message_size).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RecvLoop {
|
||||
remote_endpoint_id: EndpointId,
|
||||
conn: Connection,
|
||||
max_message_size: usize,
|
||||
in_event_tx: mpsc::Sender<InEvent>,
|
||||
}
|
||||
|
||||
impl RecvLoop {
|
||||
pub(crate) fn new(
|
||||
remote_endpoint_id: EndpointId,
|
||||
conn: Connection,
|
||||
in_event_tx: mpsc::Sender<InEvent>,
|
||||
max_message_size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
remote_endpoint_id,
|
||||
conn,
|
||||
max_message_size,
|
||||
in_event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) -> Result<(), ReadError> {
|
||||
let mut read_futures = FuturesUnordered::new();
|
||||
let mut conn_is_closed = false;
|
||||
let closed = self.conn.closed();
|
||||
tokio::pin!(closed);
|
||||
while !conn_is_closed || !read_futures.is_empty() {
|
||||
tokio::select! {
|
||||
_ = &mut closed, if !conn_is_closed => {
|
||||
conn_is_closed = true;
|
||||
}
|
||||
stream = self.conn.accept_uni(), if !conn_is_closed => {
|
||||
let stream = match stream {
|
||||
Ok(stream) => stream,
|
||||
Err(_) => {
|
||||
conn_is_closed = true;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let state = RecvStreamState::new(stream, self.max_message_size).await?;
|
||||
debug!(topic=%state.header.topic_id.fmt_short(), "stream opened");
|
||||
read_futures.push(state.next());
|
||||
}
|
||||
Some(res) = read_futures.next(), if !read_futures.is_empty() => {
|
||||
let (state, msg) = match res {
|
||||
Ok((state, msg)) => (state, msg),
|
||||
Err(err) => {
|
||||
debug!("recv stream closed with error: {err:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match msg {
|
||||
None => debug!(topic=%state.header.topic_id.fmt_short(), "stream closed"),
|
||||
Some(msg) => {
|
||||
if self.in_event_tx.send(InEvent::RecvMessage(self.remote_endpoint_id, msg)).await.is_err() {
|
||||
debug!("stop recv loop: actor closed");
|
||||
break;
|
||||
}
|
||||
read_futures.push(state.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("recv loop closed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecvStreamState {
|
||||
stream: RecvStream,
|
||||
header: StreamHeader,
|
||||
buffer: BytesMut,
|
||||
max_message_size: usize,
|
||||
}
|
||||
|
||||
impl RecvStreamState {
|
||||
async fn new(mut stream: RecvStream, max_message_size: usize) -> Result<Self, ReadError> {
|
||||
let mut buffer = BytesMut::new();
|
||||
let header = StreamHeader::read(&mut stream, &mut buffer, max_message_size).await?;
|
||||
Ok(Self {
|
||||
buffer: BytesMut::new(),
|
||||
max_message_size,
|
||||
stream,
|
||||
header,
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the next message from the stream.
|
||||
///
|
||||
/// Returns `self` and the next message, or `None` if the stream ended gracefully.
|
||||
///
|
||||
/// ## Cancellation safety
|
||||
///
|
||||
/// This function is not cancellation-safe.
|
||||
async fn next(mut self) -> Result<(Self, Option<ProtoMessage>), ReadError> {
|
||||
let msg = read_frame(&mut self.stream, &mut self.buffer, self.max_message_size).await?;
|
||||
let msg = msg.map(|msg| ProtoMessage {
|
||||
topic: self.header.topic_id,
|
||||
message: msg,
|
||||
});
|
||||
Ok((self, msg))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SendLoop {
|
||||
conn: Connection,
|
||||
streams: HashMap<TopicId, SendStream>,
|
||||
buffer: Vec<u8>,
|
||||
max_message_size: usize,
|
||||
finishing: JoinSet<()>,
|
||||
send_rx: mpsc::Receiver<ProtoMessage>,
|
||||
}
|
||||
|
||||
impl SendLoop {
|
||||
pub(crate) fn new(
|
||||
conn: Connection,
|
||||
send_rx: mpsc::Receiver<ProtoMessage>,
|
||||
max_message_size: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
max_message_size,
|
||||
buffer: Default::default(),
|
||||
streams: Default::default(),
|
||||
finishing: Default::default(),
|
||||
send_rx,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self, queue: Vec<ProtoMessage>) -> Result<(), WriteError> {
|
||||
for msg in queue {
|
||||
self.write_message(&msg).await?;
|
||||
}
|
||||
let conn_clone = self.conn.clone();
|
||||
let closed = conn_clone.closed();
|
||||
tokio::pin!(closed);
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = &mut closed => break,
|
||||
Some(msg) = self.send_rx.recv() => self.write_message(&msg).await?,
|
||||
_ = self.finishing.join_next(), if !self.finishing.is_empty() => {}
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
|
||||
// Close remaining streams.
|
||||
for (topic_id, mut stream) in self.streams.drain() {
|
||||
stream.finish().ok();
|
||||
self.finishing.spawn(
|
||||
async move {
|
||||
stream.stopped().await.ok();
|
||||
debug!(topic=%topic_id.fmt_short(), "stream closed");
|
||||
}
|
||||
.instrument(tracing::Span::current()),
|
||||
);
|
||||
}
|
||||
if !self.finishing.is_empty() {
|
||||
trace!(
|
||||
"send loop closing, waiting for {} send streams to finish",
|
||||
self.finishing.len()
|
||||
);
|
||||
// Wait for the remote to acknowledge all streams are finished.
|
||||
if let Err(_elapsed) = n0_future::time::timeout(Duration::from_secs(5), async move {
|
||||
while self.finishing.join_next().await.is_some() {}
|
||||
})
|
||||
.await
|
||||
{
|
||||
debug!("not all send streams finished within timeout, abort")
|
||||
}
|
||||
}
|
||||
debug!("send loop closed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write a [`ProtoMessage`] as a length-prefixed, postcard-encoded message on its stream.
|
||||
///
|
||||
/// If no stream is opened yet, this opens a new stream for the topic and writes the topic header.
|
||||
///
|
||||
/// This function is not cancellation-safe.
|
||||
pub async fn write_message(&mut self, message: &ProtoMessage) -> Result<(), WriteError> {
|
||||
let ProtoMessage { topic, message } = message;
|
||||
let topic_id = *topic;
|
||||
let is_last = message.is_disconnect();
|
||||
|
||||
let mut entry = match self.streams.entry(topic_id) {
|
||||
hash_map::Entry::Occupied(entry) => entry,
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let mut stream = self.conn.open_uni().await?;
|
||||
let header = StreamHeader { topic_id };
|
||||
header
|
||||
.write(&mut stream, &mut self.buffer, self.max_message_size)
|
||||
.await?;
|
||||
debug!(topic=%topic_id.fmt_short(), "stream opened");
|
||||
entry.insert_entry(stream)
|
||||
}
|
||||
};
|
||||
let stream = entry.get_mut();
|
||||
|
||||
write_frame(stream, message, &mut self.buffer, self.max_message_size).await?;
|
||||
|
||||
if is_last {
|
||||
trace!(topic=%topic_id.fmt_short(), "stream closing");
|
||||
let mut stream = entry.remove();
|
||||
if stream.finish().is_ok() {
|
||||
self.finishing.spawn(
|
||||
async move {
|
||||
stream.stopped().await.ok();
|
||||
debug!(topic=%topic_id.fmt_short(), "stream closed");
|
||||
}
|
||||
.instrument(tracing::Span::current()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors related to message reading
|
||||
#[allow(missing_docs)]
|
||||
#[stack_error(derive, add_meta, from_sources)]
|
||||
#[non_exhaustive]
|
||||
pub(crate) enum ReadError {
|
||||
/// Deserialization failed
|
||||
#[error("Deserialization failed")]
|
||||
De {
|
||||
#[error(std_err)]
|
||||
source: postcard::Error,
|
||||
},
|
||||
/// IO error
|
||||
#[error("IO error")]
|
||||
Io {
|
||||
#[error(std_err)]
|
||||
source: std::io::Error,
|
||||
},
|
||||
/// Message was larger than the configured maximum message size
|
||||
#[error("message too large")]
|
||||
TooLarge {},
|
||||
}
|
||||
|
||||
/// Read a length-prefixed frame and decode with postcard.
|
||||
pub async fn read_frame<T: DeserializeOwned>(
|
||||
reader: &mut RecvStream,
|
||||
buffer: &mut BytesMut,
|
||||
max_message_size: usize,
|
||||
) -> Result<Option<T>, ReadError> {
|
||||
match read_lp(reader, buffer, max_message_size).await? {
|
||||
None => Ok(None),
|
||||
Some(data) => {
|
||||
let message = postcard::from_bytes(&data)?;
|
||||
Ok(Some(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a length prefixed buffer.
|
||||
///
|
||||
/// Returns the frame as raw bytes. If the end of the stream is reached before
|
||||
/// the frame length starts, `None` is returned.
|
||||
pub async fn read_lp(
|
||||
reader: &mut RecvStream,
|
||||
buffer: &mut BytesMut,
|
||||
max_message_size: usize,
|
||||
) -> Result<Option<Bytes>, ReadError> {
|
||||
let size = match reader.read_u32().await {
|
||||
Ok(size) => size,
|
||||
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => return Ok(None),
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
let size = usize::try_from(size).map_err(|_| e!(ReadError::TooLarge))?;
|
||||
if size > max_message_size {
|
||||
return Err(e!(ReadError::TooLarge));
|
||||
}
|
||||
buffer.resize(size, 0u8);
|
||||
reader
|
||||
.read_exact(&mut buffer[..])
|
||||
.await
|
||||
.map_err(io::Error::other)?;
|
||||
Ok(Some(buffer.split_to(size).freeze()))
|
||||
}
|
||||
|
||||
/// Writes a length-prefixed frame.
|
||||
pub async fn write_frame<T: Serialize>(
|
||||
stream: &mut SendStream,
|
||||
message: &T,
|
||||
buffer: &mut Vec<u8>,
|
||||
max_message_size: usize,
|
||||
) -> Result<(), WriteError> {
|
||||
let len = postcard::experimental::serialized_size(&message)?;
|
||||
if len >= max_message_size {
|
||||
return Err(e!(WriteError::TooLarge));
|
||||
}
|
||||
buffer.clear();
|
||||
buffer.resize(len, 0u8);
|
||||
let slice = postcard::to_slice(&message, buffer)?;
|
||||
stream.write_u32(len as u32).await?;
|
||||
stream.write_all(slice).await.map_err(io::Error::other)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A [`TimerMap`] with an async method to wait for the next timer expiration.
|
||||
#[derive(Debug)]
|
||||
pub struct Timers<T> {
|
||||
map: TimerMap<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Timers<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: TimerMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Timers<T> {
|
||||
/// Creates a new timer map.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Inserts a new entry at the specified instant
|
||||
pub fn insert(&mut self, instant: Instant, item: T) {
|
||||
self.map.insert(instant, item);
|
||||
}
|
||||
|
||||
/// Waits for the next timer to elapse.
|
||||
pub async fn wait_next(&mut self) -> Instant {
|
||||
match self.map.first() {
|
||||
None => std::future::pending::<Instant>().await,
|
||||
Some(instant) => {
|
||||
sleep_until(*instant).await;
|
||||
*instant
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pops the earliest timer that expires at or before `now`.
|
||||
pub fn pop_before(&mut self, now: Instant) -> Option<(Instant, T)> {
|
||||
self.map.pop_before(now)
|
||||
}
|
||||
}
|
||||
344
third_party/iroh-org/iroh-gossip/src/proto.rs
vendored
Normal file
344
third_party/iroh-org/iroh-gossip/src/proto.rs
vendored
Normal file
|
|
@ -0,0 +1,344 @@
|
|||
//! Implementation of the iroh-gossip protocol, as an IO-less state machine
|
||||
//!
|
||||
//! This module implements the iroh-gossip protocol. The entry point is [`State`], which contains
|
||||
//! the protocol state for a node.
|
||||
//!
|
||||
//! The iroh-gossip protocol is made up from two parts: A swarm membership protocol, based on
|
||||
//! [HyParView][hyparview], and a gossip broadcasting protocol, based on [PlumTree][plumtree].
|
||||
//!
|
||||
//! For a full explanation it is recommended to read the two papers. What follows is a brief
|
||||
//! outline of the protocols.
|
||||
//!
|
||||
//! All protocol messages are namespaced by a [`TopicId`], a 32 byte identifier. Topics are
|
||||
//! separate swarms and broadcast scopes. The HyParView and PlumTree algorithms both work in the
|
||||
//! scope of a single topic. Thus, joining multiple topics increases the number of open connections
|
||||
//! to peers and the size of the local routing table.
|
||||
//!
|
||||
//! The **membership protocol** ([HyParView][hyparview]) is a cluster protocol where each peer
|
||||
//! maintains a partial view of all nodes in the swarm.
|
||||
//! A peer joins the swarm for a topic by connecting to any known peer that is a member of this
|
||||
//! topic's swarm. Obtaining this initial contact info happens out of band. The peer then sends
|
||||
//! a `Join` message to that initial peer. All peers maintain a list of
|
||||
//! `active` and `passive` peers. Active peers are those that you maintain active connections to.
|
||||
//! Passive peers is an addressbook of additional peers. If one of your active peers goes offline,
|
||||
//! its slot is filled with a random peer from the passive set. In the default configuration, the
|
||||
//! active view has a size of 5 and the passive view a size of 30.
|
||||
//! The HyParView protocol ensures that active connections are always bidirectional, and regularly
|
||||
//! exchanges nodes for the passive view in a `Shuffle` operation.
|
||||
//! Thus, this protocol exposes a high degree of reliability and auto-recovery in the case of node
|
||||
//! failures.
|
||||
//!
|
||||
//! The **gossip protocol** ([PlumTree][plumtree]) builds upon the membership protocol. It exposes
|
||||
//! a method to broadcast messages to all peers in the swarm. On each node, it maintains two sets
|
||||
//! of peers: An `eager` set and a `lazy` set. Both are subsets of the `active` view from the
|
||||
//! membership protocol. When broadcasting a message from the local node, or upon receiving a
|
||||
//! broadcast message, the message is pushed to all peers in the eager set. Additionally, the hash
|
||||
//! of the message (which uniquely identifies it), but not the message content, is lazily pushed
|
||||
//! to all peers in the `lazy` set. When receiving such lazy pushes (called `Ihaves`), those peers
|
||||
//! may request the message content after a timeout if they didn't receive the message by one of
|
||||
//! their eager peers before. When requesting a message from a currently-lazy peer, this peer is
|
||||
//! also upgraded to be an eager peer from that moment on. This strategy self-optimizes the
|
||||
//! messaging graph by latency. Note however that this optimization will work best if the messaging
|
||||
//! paths are stable, i.e. if it's always the same peer that broadcasts. If not, the relative
|
||||
//! message redundancy will grow and the ideal messaging graph might change frequently.
|
||||
//!
|
||||
//! [hyparview]: https://asc.di.fct.unl.pt/~jleitao/pdf/dsn07-leitao.pdf
|
||||
//! [plumtree]: https://asc.di.fct.unl.pt/~jleitao/pdf/srds07-leitao.pdf
|
||||
|
||||
use std::{fmt, hash::Hash};
|
||||
|
||||
use bytes::Bytes;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
mod hyparview;
|
||||
mod plumtree;
|
||||
pub mod state;
|
||||
pub mod topic;
|
||||
pub mod util;
|
||||
|
||||
#[cfg(any(test, feature = "test-utils"))]
|
||||
pub mod sim;
|
||||
|
||||
pub use hyparview::Config as HyparviewConfig;
|
||||
pub use plumtree::{Config as PlumtreeConfig, DeliveryScope, Scope};
|
||||
pub use state::{InEvent, Message, OutEvent, State, Timer, TopicId};
|
||||
pub use topic::{Command, Config, Event, IO};
|
||||
|
||||
/// The default maximum size in bytes for a gossip message.
|
||||
/// This is a sane but arbitrary default and can be changed in the [`Config`].
|
||||
pub const DEFAULT_MAX_MESSAGE_SIZE: usize = 4096;
|
||||
|
||||
/// The minimum allowed value for [`Config::max_message_size`].
|
||||
pub const MIN_MAX_MESSAGE_SIZE: usize = 512;
|
||||
|
||||
/// The identifier for a peer.
|
||||
///
|
||||
/// The protocol implementation is generic over this trait. When implementing the protocol,
|
||||
/// a concrete type must be chosen that will then be used throughout the implementation to identify
|
||||
/// and index individual peers.
|
||||
///
|
||||
/// Note that the concrete type will be used in protocol messages. Therefore, implementations of
|
||||
/// the protocol are only compatible if the same concrete type is supplied for this trait.
|
||||
///
|
||||
/// TODO: Rename to `PeerId`? It does not necessarily refer to a peer's address, as long as the
|
||||
/// networking layer can translate the value of its concrete type into an address.
|
||||
pub trait PeerIdentity: Hash + Eq + Ord + Copy + fmt::Debug + Serialize + DeserializeOwned {}
|
||||
impl<T> PeerIdentity for T where
|
||||
T: Hash + Eq + Ord + Copy + fmt::Debug + Serialize + DeserializeOwned
|
||||
{
|
||||
}
|
||||
|
||||
/// Opaque binary data that is transmitted on messages that introduce new peers.
|
||||
///
|
||||
/// Implementations may use these bytes to supply addresses or other information needed to connect
|
||||
/// to a peer that is not included in the peer's [`PeerIdentity`].
|
||||
#[derive(derive_more::Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
|
||||
#[debug("PeerData({}b)", self.0.len())]
|
||||
pub struct PeerData(Bytes);
|
||||
|
||||
impl PeerData {
|
||||
/// Create a new [`PeerData`] from a byte buffer.
|
||||
pub fn new(data: impl Into<Bytes>) -> Self {
|
||||
Self(data.into())
|
||||
}
|
||||
|
||||
/// Get a reference to the contained [`bytes::Bytes`].
|
||||
pub fn inner(&self) -> &bytes::Bytes {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Get the peer data as a byte slice.
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// PeerInfo contains a peer's identifier and the opaque peer data as provided by the implementer.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
struct PeerInfo<PI> {
|
||||
pub id: PI,
|
||||
pub data: Option<PeerData>,
|
||||
}
|
||||
|
||||
impl<PI> From<(PI, Option<PeerData>)> for PeerInfo<PI> {
|
||||
fn from((id, data): (PI, Option<PeerData>)) -> Self {
|
||||
Self { id, data }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{collections::HashSet, env, fmt, str::FromStr};
|
||||
|
||||
use n0_tracing_test::traced_test;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha12Rng;
|
||||
|
||||
use super::{Command, Config, Event};
|
||||
use crate::proto::{
|
||||
sim::{LatencyConfig, Network, NetworkConfig},
|
||||
Scope, TopicId,
|
||||
};
|
||||
|
||||
#[test]
|
||||
#[traced_test]
|
||||
fn hyparview_smoke() {
|
||||
// Create a network with 4 nodes and active_view_capacity 2
|
||||
let rng = ChaCha12Rng::seed_from_u64(read_var("SEED", 0));
|
||||
let mut config = Config::default();
|
||||
config.membership.active_view_capacity = 2;
|
||||
let network_config = NetworkConfig {
|
||||
proto: config,
|
||||
latency: LatencyConfig::default_static(),
|
||||
};
|
||||
let mut network = Network::new(network_config, rng);
|
||||
for i in 0..4 {
|
||||
network.insert(i);
|
||||
}
|
||||
|
||||
let t: TopicId = [0u8; 32].into();
|
||||
|
||||
// Do some joins between nodes 0,1,2
|
||||
network.command(0, t, Command::Join(vec![1, 2]));
|
||||
network.command(1, t, Command::Join(vec![2]));
|
||||
network.command(2, t, Command::Join(vec![]));
|
||||
|
||||
network.run_trips(3);
|
||||
|
||||
// Confirm emitted events
|
||||
let actual = network.events_sorted();
|
||||
let expected = sort(vec![
|
||||
(0, t, Event::NeighborUp(1)),
|
||||
(0, t, Event::NeighborUp(2)),
|
||||
(1, t, Event::NeighborUp(2)),
|
||||
(1, t, Event::NeighborUp(0)),
|
||||
(2, t, Event::NeighborUp(0)),
|
||||
(2, t, Event::NeighborUp(1)),
|
||||
]);
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
// Confirm active connections
|
||||
assert_eq!(network.conns(), vec![(0, 1), (0, 2), (1, 2)]);
|
||||
|
||||
// Now let node 3 join node 0.
|
||||
// Node 0 is full, so it will disconnect from either node 1 or node 2.
|
||||
network.command(3, t, Command::Join(vec![0]));
|
||||
|
||||
network.run_trips(2);
|
||||
|
||||
// Confirm emitted events. There's two options because whether node 0 disconnects from
|
||||
// node 1 or node 2 is random.
|
||||
let actual = network.events_sorted();
|
||||
eprintln!("actual {actual:#?}");
|
||||
let expected1 = sort(vec![
|
||||
(3, t, Event::NeighborUp(0)),
|
||||
(0, t, Event::NeighborUp(3)),
|
||||
(0, t, Event::NeighborDown(1)),
|
||||
(1, t, Event::NeighborDown(0)),
|
||||
]);
|
||||
let expected2 = sort(vec![
|
||||
(3, t, Event::NeighborUp(0)),
|
||||
(0, t, Event::NeighborUp(3)),
|
||||
(0, t, Event::NeighborDown(2)),
|
||||
(2, t, Event::NeighborDown(0)),
|
||||
]);
|
||||
assert!((actual == expected1) || (actual == expected2));
|
||||
|
||||
// Confirm active connections.
|
||||
if actual == expected1 {
|
||||
assert_eq!(network.conns(), vec![(0, 2), (0, 3), (1, 2)]);
|
||||
} else {
|
||||
assert_eq!(network.conns(), vec![(0, 1), (0, 3), (1, 2)]);
|
||||
}
|
||||
assert!(network.check_synchronicity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[traced_test]
|
||||
fn plumtree_smoke() {
|
||||
let rng = ChaCha12Rng::seed_from_u64(read_var("SEED", 0));
|
||||
let network_config = NetworkConfig {
|
||||
proto: Config::default(),
|
||||
latency: LatencyConfig::default_static(),
|
||||
};
|
||||
let mut network = Network::new(network_config, rng);
|
||||
// build a network with 6 nodes
|
||||
for i in 0..6 {
|
||||
network.insert(i);
|
||||
}
|
||||
|
||||
let t = [0u8; 32].into();
|
||||
|
||||
// let node 0 join the topic but do not connect to any peers
|
||||
network.command(0, t, Command::Join(vec![]));
|
||||
// connect nodes 1 and 2 to node 0
|
||||
(1..3).for_each(|i| network.command(i, t, Command::Join(vec![0])));
|
||||
// connect nodes 4 and 5 to node 3
|
||||
network.command(3, t, Command::Join(vec![]));
|
||||
(4..6).for_each(|i| network.command(i, t, Command::Join(vec![3])));
|
||||
// run ticks and drain events
|
||||
|
||||
network.run_trips(4);
|
||||
|
||||
let _ = network.events();
|
||||
assert!(network.check_synchronicity());
|
||||
|
||||
// now broadcast a first message
|
||||
network.command(
|
||||
1,
|
||||
t,
|
||||
Command::Broadcast(b"hi1".to_vec().into(), Scope::Swarm),
|
||||
);
|
||||
|
||||
network.run_trips(4);
|
||||
|
||||
let events = network.events();
|
||||
let received = events.filter(|x| matches!(x, (_, _, Event::Received(_))));
|
||||
// message should be received by two other nodes
|
||||
assert_eq!(received.count(), 2);
|
||||
assert!(network.check_synchronicity());
|
||||
|
||||
// now connect the two sections of the swarm
|
||||
network.command(2, t, Command::Join(vec![5]));
|
||||
network.run_trips(3);
|
||||
let _ = network.events();
|
||||
println!("{}", network.report());
|
||||
|
||||
// now broadcast again
|
||||
network.command(
|
||||
1,
|
||||
t,
|
||||
Command::Broadcast(b"hi2".to_vec().into(), Scope::Swarm),
|
||||
);
|
||||
network.run_trips(5);
|
||||
let events = network.events();
|
||||
let received = events.filter(|x| matches!(x, (_, _, Event::Received(_))));
|
||||
// message should be received by all 5 other nodes
|
||||
assert_eq!(received.count(), 5);
|
||||
assert!(network.check_synchronicity());
|
||||
println!("{}", network.report());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[traced_test]
|
||||
fn quit() {
|
||||
// Create a network with 4 nodes and active_view_capacity 2
|
||||
let rng = ChaCha12Rng::seed_from_u64(read_var("SEED", 0));
|
||||
let mut config = Config::default();
|
||||
config.membership.active_view_capacity = 2;
|
||||
let mut network = Network::new(config.into(), rng);
|
||||
let num = 4;
|
||||
for i in 0..num {
|
||||
network.insert(i);
|
||||
}
|
||||
|
||||
let t: TopicId = [0u8; 32].into();
|
||||
|
||||
// join all nodes
|
||||
network.command(0, t, Command::Join(vec![]));
|
||||
network.command(1, t, Command::Join(vec![0]));
|
||||
network.command(2, t, Command::Join(vec![1]));
|
||||
network.command(3, t, Command::Join(vec![2]));
|
||||
network.run_trips(2);
|
||||
|
||||
// assert all peers appear in the connections
|
||||
let all_conns: HashSet<u64> = HashSet::from_iter((0u64..4).flat_map(|p| {
|
||||
network
|
||||
.neighbors(&p, &t)
|
||||
.into_iter()
|
||||
.flat_map(|x| x.into_iter())
|
||||
}));
|
||||
assert_eq!(all_conns, HashSet::from_iter([0, 1, 2, 3]));
|
||||
assert!(network.check_synchronicity());
|
||||
|
||||
// let node 3 leave the swarm
|
||||
network.command(3, t, Command::Quit);
|
||||
network.run_trips(4);
|
||||
assert!(network.peer(&3).unwrap().state(&t).is_none());
|
||||
|
||||
// assert all peers without peer 3 appear in the connections
|
||||
let all_conns: HashSet<u64> = HashSet::from_iter((0..num).flat_map(|p| {
|
||||
network
|
||||
.neighbors(&p, &t)
|
||||
.into_iter()
|
||||
.flat_map(|x| x.into_iter())
|
||||
}));
|
||||
assert_eq!(all_conns, HashSet::from_iter([0, 1, 2]));
|
||||
assert!(network.check_synchronicity());
|
||||
}
|
||||
|
||||
fn read_var<T: FromStr<Err: fmt::Display + fmt::Debug>>(name: &str, default: T) -> T {
|
||||
env::var(name)
|
||||
.map(|x| {
|
||||
x.parse()
|
||||
.unwrap_or_else(|_| panic!("Failed to parse environment variable {name}"))
|
||||
})
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn sort<T: Ord + Clone>(items: Vec<T>) -> Vec<T> {
|
||||
let mut sorted = items;
|
||||
sorted.sort();
|
||||
sorted
|
||||
}
|
||||
}
|
||||
764
third_party/iroh-org/iroh-gossip/src/proto/hyparview.rs
vendored
Normal file
764
third_party/iroh-org/iroh-gossip/src/proto/hyparview.rs
vendored
Normal file
|
|
@ -0,0 +1,764 @@
|
|||
//! Implementation of the HyParView membership protocol
|
||||
//!
|
||||
//! The implementation is based on [this paper][paper] by Joao Leitao, Jose Pereira, Luıs Rodrigues
|
||||
//! and the [example implementation][impl] by Bartosz Sypytkowski
|
||||
//!
|
||||
//! [paper]: https://asc.di.fct.unl.pt/~jleitao/pdf/dsn07-leitao.pdf
|
||||
//! [impl]: https://gist.github.com/Horusiath/84fac596101b197da0546d1697580d99
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use derive_more::{From, Sub};
|
||||
use n0_future::time::Duration;
|
||||
use rand::{rngs::ThreadRng, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::debug;
|
||||
|
||||
use super::{util::IndexSet, PeerData, PeerIdentity, PeerInfo, IO};
|
||||
|
||||
/// Input event for HyParView
|
||||
#[derive(Debug)]
|
||||
pub enum InEvent<PI> {
|
||||
/// A [`Message`] was received from a peer.
|
||||
RecvMessage(PI, Message<PI>),
|
||||
/// A timer has expired.
|
||||
TimerExpired(Timer<PI>),
|
||||
/// A peer was disconnected on the IO layer.
|
||||
PeerDisconnected(PI),
|
||||
/// Send a join request to a peer.
|
||||
RequestJoin(PI),
|
||||
/// Update the peer data that is transmitted on join requests.
|
||||
UpdatePeerData(PeerData),
|
||||
/// Quit the swarm, informing peers about us leaving.
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Output event for HyParView
|
||||
#[derive(Debug)]
|
||||
pub enum OutEvent<PI> {
|
||||
/// Ask the IO layer to send a [`Message`] to peer `PI`.
|
||||
SendMessage(PI, Message<PI>),
|
||||
/// Schedule a [`Timer`].
|
||||
ScheduleTimer(Duration, Timer<PI>),
|
||||
/// Ask the IO layer to close the connection to peer `PI`.
|
||||
DisconnectPeer(PI),
|
||||
/// Emit an [`Event`] to the application.
|
||||
EmitEvent(Event<PI>),
|
||||
/// New [`PeerData`] was received for peer `PI`.
|
||||
PeerData(PI, PeerData),
|
||||
}
|
||||
|
||||
/// Event emitted by the [`State`] to the application.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Event<PI> {
|
||||
/// A peer was added to our set of active connections.
|
||||
NeighborUp(PI),
|
||||
/// A peer was removed from our set of active connections.
|
||||
NeighborDown(PI),
|
||||
}
|
||||
|
||||
/// Kinds of timers HyParView needs to schedule.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Timer<PI> {
|
||||
DoShuffle,
|
||||
PendingNeighborRequest(PI),
|
||||
}
|
||||
|
||||
/// Messages that we can send and receive from peers within the topic.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub enum Message<PI> {
|
||||
/// Sent to a peer if you want to join the swarm
|
||||
Join(Option<PeerData>),
|
||||
/// When receiving Join, ForwardJoin is forwarded to the peer's ActiveView to introduce the
|
||||
/// new member.
|
||||
ForwardJoin(ForwardJoin<PI>),
|
||||
/// A shuffle request is sent occasionally to re-shuffle the PassiveView with contacts from
|
||||
/// other peers.
|
||||
Shuffle(Shuffle<PI>),
|
||||
/// Peers reply to [`Message::Shuffle`] requests with a random peers from their active and
|
||||
/// passive views.
|
||||
ShuffleReply(ShuffleReply<PI>),
|
||||
/// Request to add sender to an active view of recipient. If [`Neighbor::priority`] is
|
||||
/// [`Priority::High`], the request cannot be denied.
|
||||
Neighbor(Neighbor),
|
||||
/// Request to disconnect from a peer.
|
||||
/// If [`Disconnect::alive`] is true, the other peer is not shutting down, so it should be
|
||||
/// added to the passive set.
|
||||
Disconnect(Disconnect),
|
||||
}
|
||||
|
||||
/// The time-to-live for this message.
|
||||
///
|
||||
/// Each time a message is forwarded, the `Ttl` is decreased by 1. If the `Ttl` reaches 0, it
|
||||
/// should not be forwarded further.
|
||||
#[derive(From, Sub, Eq, PartialEq, Clone, Debug, Copy, Serialize, Deserialize)]
|
||||
pub struct Ttl(pub u16);
|
||||
impl Ttl {
|
||||
pub fn expired(&self) -> bool {
|
||||
*self == Ttl(0)
|
||||
}
|
||||
pub fn next(&self) -> Ttl {
|
||||
Ttl(self.0.saturating_sub(1))
|
||||
}
|
||||
}
|
||||
|
||||
/// A message informing other peers that a new peer joined the swarm for this topic.
|
||||
///
|
||||
/// Will be forwarded in a random walk until `ttl` reaches 0.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ForwardJoin<PI> {
|
||||
/// The peer that newly joined the swarm
|
||||
peer: PeerInfo<PI>,
|
||||
/// The time-to-live for this message
|
||||
ttl: Ttl,
|
||||
}
|
||||
|
||||
/// Shuffle messages are sent occasionally to shuffle our passive view with peers from other peer's
|
||||
/// active and passive views.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Shuffle<PI> {
|
||||
/// The peer that initiated the shuffle request.
|
||||
origin: PI,
|
||||
/// A random subset of the active and passive peers of the `origin` peer.
|
||||
nodes: Vec<PeerInfo<PI>>,
|
||||
/// The time-to-live for this message.
|
||||
ttl: Ttl,
|
||||
}
|
||||
|
||||
/// Once a shuffle messages reaches a [`Ttl`] of 0, a peer replies with a `ShuffleReply`.
|
||||
///
|
||||
/// The reply is sent to the peer that initiated the shuffle and contains a subset of the active
|
||||
/// and passive views of the peer at the end of the random walk.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct ShuffleReply<PI> {
|
||||
/// A random subset of the active and passive peers of the peer sending the `ShuffleReply`.
|
||||
nodes: Vec<PeerInfo<PI>>,
|
||||
}
|
||||
|
||||
/// The priority of a `Join` message
|
||||
///
|
||||
/// This is `High` if the sender does not have any active peers, and `Low` otherwise.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub enum Priority {
|
||||
/// High priority join that may not be denied.
|
||||
///
|
||||
/// A peer may only send high priority joins if it doesn't have any active peers at the moment.
|
||||
High,
|
||||
/// Low priority join that can be denied.
|
||||
Low,
|
||||
}
|
||||
|
||||
/// A neighbor message is sent after adding a peer to our active view to inform them that we are
|
||||
/// now neighbors.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Neighbor {
|
||||
/// The priority of the `Join` or `ForwardJoin` message that triggered this neighbor request.
|
||||
priority: Priority,
|
||||
/// The user data of the peer sending this message.
|
||||
data: Option<PeerData>,
|
||||
}
|
||||
|
||||
/// Message sent when leaving the swarm or closing down to inform peers about us being gone.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Disconnect {
|
||||
/// Whether we are actually shutting down or closing the connection only because our limits are
|
||||
/// reached.
|
||||
alive: bool,
|
||||
/// Obsolete field (kept in the struct to maintain wire compatibility).
|
||||
_respond: bool,
|
||||
}
|
||||
|
||||
/// Configuration for the swarm membership layer
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// Number of peers to which active connections are maintained
|
||||
pub active_view_capacity: usize,
|
||||
/// Number of peers for which contact information is remembered,
|
||||
/// but to which we are not actively connected to.
|
||||
pub passive_view_capacity: usize,
|
||||
/// Number of hops a `ForwardJoin` message is propagated until the new peer's info
|
||||
/// is added to a peer's active view.
|
||||
pub active_random_walk_length: Ttl,
|
||||
/// Number of hops a `ForwardJoin` message is propagated until the new peer's info
|
||||
/// is added to a peer's passive view.
|
||||
pub passive_random_walk_length: Ttl,
|
||||
/// Number of hops a `Shuffle` message is propagated until a peer replies to it.
|
||||
pub shuffle_random_walk_length: Ttl,
|
||||
/// Number of active peers to be included in a `Shuffle` request.
|
||||
pub shuffle_active_view_count: usize,
|
||||
/// Number of passive peers to be included in a `Shuffle` request.
|
||||
pub shuffle_passive_view_count: usize,
|
||||
/// Interval duration for shuffle requests
|
||||
pub shuffle_interval: Duration,
|
||||
/// Timeout after which a `Neighbor` request is considered failed
|
||||
pub neighbor_request_timeout: Duration,
|
||||
}
|
||||
impl Default for Config {
|
||||
/// Default values for the HyParView layer
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// From the paper (p9)
|
||||
active_view_capacity: 5,
|
||||
// From the paper (p9)
|
||||
passive_view_capacity: 30,
|
||||
// From the paper (p9)
|
||||
active_random_walk_length: Ttl(6),
|
||||
// From the paper (p9)
|
||||
passive_random_walk_length: Ttl(3),
|
||||
// From the paper (p9)
|
||||
shuffle_random_walk_length: Ttl(6),
|
||||
// From the paper (p9)
|
||||
shuffle_active_view_count: 3,
|
||||
// From the paper (p9)
|
||||
shuffle_passive_view_count: 4,
|
||||
// Wild guess
|
||||
shuffle_interval: Duration::from_secs(60),
|
||||
// Wild guess
|
||||
neighbor_request_timeout: Duration::from_millis(500),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct Stats {
|
||||
total_connections: usize,
|
||||
}
|
||||
|
||||
/// The state of the HyParView protocol
|
||||
#[derive(Debug)]
|
||||
pub struct State<PI, RG = ThreadRng> {
|
||||
/// Our peer identity
|
||||
me: PI,
|
||||
/// Our opaque user data to transmit to peers on join messages
|
||||
me_data: Option<PeerData>,
|
||||
/// The active view, i.e. peers we are connected to
|
||||
pub(crate) active_view: IndexSet<PI>,
|
||||
/// The passive view, i.e. peers we know about but are not connected to at the moment
|
||||
pub(crate) passive_view: IndexSet<PI>,
|
||||
/// Protocol configuration (cannot change at runtime)
|
||||
config: Config,
|
||||
/// Whether a shuffle timer is currently scheduled
|
||||
shuffle_scheduled: bool,
|
||||
/// Random number generator
|
||||
rng: RG,
|
||||
/// Statistics
|
||||
pub(crate) stats: Stats,
|
||||
/// The set of neighbor requests we sent out but did not yet receive a reply for
|
||||
pending_neighbor_requests: HashSet<PI>,
|
||||
/// The opaque user peer data we received for other peers
|
||||
peer_data: HashMap<PI, PeerData>,
|
||||
/// List of peers that are disconnecting, but which we want to keep in the passive set once the connection closes
|
||||
alive_disconnect_peers: HashSet<PI>,
|
||||
}
|
||||
|
||||
impl<PI, RG> State<PI, RG>
|
||||
where
|
||||
PI: PeerIdentity,
|
||||
RG: Rng,
|
||||
{
|
||||
pub fn new(me: PI, me_data: Option<PeerData>, config: Config, rng: RG) -> Self {
|
||||
Self {
|
||||
me,
|
||||
me_data,
|
||||
active_view: IndexSet::new(),
|
||||
passive_view: IndexSet::new(),
|
||||
config,
|
||||
shuffle_scheduled: false,
|
||||
rng,
|
||||
stats: Stats::default(),
|
||||
pending_neighbor_requests: Default::default(),
|
||||
peer_data: Default::default(),
|
||||
alive_disconnect_peers: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, event: InEvent<PI>, io: &mut impl IO<PI>) {
|
||||
match event {
|
||||
InEvent::RecvMessage(from, message) => self.handle_message(from, message, io),
|
||||
InEvent::TimerExpired(timer) => match timer {
|
||||
Timer::DoShuffle => self.handle_shuffle_timer(io),
|
||||
Timer::PendingNeighborRequest(peer) => self.handle_pending_neighbor_timer(peer, io),
|
||||
},
|
||||
InEvent::PeerDisconnected(peer) => self.handle_connection_closed(peer, io),
|
||||
InEvent::RequestJoin(peer) => self.handle_join(peer, io),
|
||||
InEvent::UpdatePeerData(data) => {
|
||||
self.me_data = Some(data);
|
||||
}
|
||||
InEvent::Quit => self.handle_quit(io),
|
||||
}
|
||||
|
||||
// this will only happen on the first call
|
||||
if !self.shuffle_scheduled {
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.shuffle_interval,
|
||||
Timer::DoShuffle,
|
||||
));
|
||||
self.shuffle_scheduled = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_message(&mut self, from: PI, message: Message<PI>, io: &mut impl IO<PI>) {
|
||||
let is_disconnect = matches!(message, Message::Disconnect(Disconnect { .. }));
|
||||
if !is_disconnect && !self.active_view.contains(&from) {
|
||||
self.stats.total_connections += 1;
|
||||
}
|
||||
match message {
|
||||
Message::Join(data) => self.on_join(from, data, io),
|
||||
Message::ForwardJoin(details) => self.on_forward_join(from, details, io),
|
||||
Message::Shuffle(details) => self.on_shuffle(from, details, io),
|
||||
Message::ShuffleReply(details) => self.on_shuffle_reply(details, io),
|
||||
Message::Neighbor(details) => self.on_neighbor(from, details, io),
|
||||
Message::Disconnect(details) => self.on_disconnect(from, details, io),
|
||||
}
|
||||
|
||||
// Disconnect from passive nodes right after receiving a message.
|
||||
// TODO(frando): I'm not sure anymore that this is correct. Maybe remove?
|
||||
if !is_disconnect && !self.active_view.contains(&from) {
|
||||
io.push(OutEvent::DisconnectPeer(from));
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_join(&mut self, peer: PI, io: &mut impl IO<PI>) {
|
||||
io.push(OutEvent::SendMessage(
|
||||
peer,
|
||||
Message::Join(self.me_data.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
/// We received a disconnect message.
|
||||
fn on_disconnect(&mut self, peer: PI, details: Disconnect, io: &mut impl IO<PI>) {
|
||||
self.pending_neighbor_requests.remove(&peer);
|
||||
if self.active_view.contains(&peer) {
|
||||
self.remove_active(
|
||||
&peer,
|
||||
RemovalReason::DisconnectReceived {
|
||||
is_alive: details.alive,
|
||||
},
|
||||
io,
|
||||
);
|
||||
} else if details.alive && self.passive_view.contains(&peer) {
|
||||
self.alive_disconnect_peers.insert(peer);
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection was closed by the peer.
|
||||
fn handle_connection_closed(&mut self, peer: PI, io: &mut impl IO<PI>) {
|
||||
self.pending_neighbor_requests.remove(&peer);
|
||||
if self.active_view.contains(&peer) {
|
||||
self.remove_active(&peer, RemovalReason::ConnectionClosed, io);
|
||||
} else if !self.alive_disconnect_peers.remove(&peer) {
|
||||
self.passive_view.remove(&peer);
|
||||
self.peer_data.remove(&peer);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_quit(&mut self, io: &mut impl IO<PI>) {
|
||||
for peer in self.active_view.clone().into_iter() {
|
||||
self.active_view.remove(&peer);
|
||||
self.send_disconnect(peer, false, io);
|
||||
}
|
||||
}
|
||||
|
||||
fn send_disconnect(&mut self, peer: PI, alive: bool, io: &mut impl IO<PI>) {
|
||||
// Before disconnecting, send a `ShuffleReply` with some of our nodes to
|
||||
// prevent the other node from running out of connections. This is especially
|
||||
// relevant if the other node just joined the swarm.
|
||||
self.send_shuffle_reply(
|
||||
peer,
|
||||
self.config.shuffle_active_view_count + self.config.shuffle_passive_view_count,
|
||||
io,
|
||||
);
|
||||
let message = Message::Disconnect(Disconnect {
|
||||
alive,
|
||||
_respond: false,
|
||||
});
|
||||
io.push(OutEvent::SendMessage(peer, message));
|
||||
io.push(OutEvent::DisconnectPeer(peer));
|
||||
}
|
||||
|
||||
fn on_join(&mut self, peer: PI, data: Option<PeerData>, io: &mut impl IO<PI>) {
|
||||
// "A node that receives a join request will start by adding the new
|
||||
// node to its active view, even if it has to drop a random node from it. (6)"
|
||||
self.add_active(peer, data.clone(), Priority::High, true, io);
|
||||
|
||||
// "The contact node c will then send to all other nodes in its active view a ForwardJoin
|
||||
// request containing the new node identifier. Associated to the join procedure,
|
||||
// there are two configuration parameters, named Active Random Walk Length (ARWL),
|
||||
// that specifies the maximum number of hops a ForwardJoin request is propagated,
|
||||
// and Passive Random Walk Length (PRWL), that specifies at which point in the walk the node
|
||||
// is inserted in a passive view. To use these parameters, the ForwardJoin request carries
|
||||
// a “time to live” field that is initially set to ARWL and decreased at every hop. (7)"
|
||||
let ttl = self.config.active_random_walk_length;
|
||||
let peer_info = PeerInfo { id: peer, data };
|
||||
for node in self.active_view.iter_without(&peer) {
|
||||
let message = Message::ForwardJoin(ForwardJoin {
|
||||
peer: peer_info.clone(),
|
||||
ttl,
|
||||
});
|
||||
io.push(OutEvent::SendMessage(*node, message));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_forward_join(&mut self, sender: PI, message: ForwardJoin<PI>, io: &mut impl IO<PI>) {
|
||||
let peer_id = message.peer.id;
|
||||
// If the peer is already in our active view, we renew our neighbor relationship.
|
||||
if self.active_view.contains(&peer_id) {
|
||||
self.insert_peer_info(message.peer, io);
|
||||
self.send_neighbor(peer_id, Priority::High, io);
|
||||
}
|
||||
// "i) If the time to live is equal to zero or if the number of nodes in p’s active view is equal to one,
|
||||
// it will add the new node to its active view (7)"
|
||||
else if message.ttl.expired() || self.active_view.len() <= 1 {
|
||||
self.insert_peer_info(message.peer, io);
|
||||
// Modification from paper: Instead of adding the peer directly to our active view,
|
||||
// we only send the Neighbor message. We will add the peer to our active view once we receive a
|
||||
// reply from our neighbor.
|
||||
// This prevents us adding unreachable peers to our active view.
|
||||
self.send_neighbor(peer_id, Priority::High, io);
|
||||
} else {
|
||||
// "ii) If the time to live is equal to PRWL, p will insert the new node into its passive view"
|
||||
if message.ttl == self.config.passive_random_walk_length {
|
||||
self.add_passive(peer_id, message.peer.data.clone(), io);
|
||||
}
|
||||
// "iii) The time to live field is decremented."
|
||||
// "iv) If, at this point, n has not been inserted
|
||||
// in p’s active view, p will forward the request to a random node in its active view
|
||||
// (different from the one from which the request was received)."
|
||||
if !self.active_view.contains(&peer_id)
|
||||
&& !self.pending_neighbor_requests.contains(&peer_id)
|
||||
{
|
||||
match self
|
||||
.active_view
|
||||
.pick_random_without(&[&sender], &mut self.rng)
|
||||
{
|
||||
None => {
|
||||
unreachable!("if the peer was not added, there are at least two peers in our active view.");
|
||||
}
|
||||
Some(next) => {
|
||||
let message = Message::ForwardJoin(ForwardJoin {
|
||||
peer: message.peer,
|
||||
ttl: message.ttl.next(),
|
||||
});
|
||||
io.push(OutEvent::SendMessage(*next, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_neighbor(&mut self, from: PI, details: Neighbor, io: &mut impl IO<PI>) {
|
||||
let is_reply = self.pending_neighbor_requests.remove(&from);
|
||||
let do_reply = !is_reply;
|
||||
// "A node q that receives a high priority neighbor request will always accept the request, even
|
||||
// if it has to drop a random member from its active view (again, the member that is dropped will
|
||||
// receive a Disconnect notification). If a node q receives a low priority Neighbor request, it will
|
||||
// only accept the request if it has a free slot in its active view, otherwise it will refuse the request."
|
||||
if !self.add_active(from, details.data, details.priority, do_reply, io) {
|
||||
self.send_disconnect(from, true, io);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the peer [`PeerInfo`] for a peer.
|
||||
fn peer_info(&self, id: &PI) -> PeerInfo<PI> {
|
||||
let data = self.peer_data.get(id).cloned();
|
||||
PeerInfo { id: *id, data }
|
||||
}
|
||||
|
||||
fn insert_peer_info(&mut self, peer_info: PeerInfo<PI>, io: &mut impl IO<PI>) {
|
||||
if let Some(data) = peer_info.data {
|
||||
let old = self.peer_data.remove(&peer_info.id);
|
||||
let same = matches!(old, Some(old) if old == data);
|
||||
if !same && !data.0.is_empty() {
|
||||
io.push(OutEvent::PeerData(peer_info.id, data.clone()));
|
||||
}
|
||||
self.peer_data.insert(peer_info.id, data);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a [`Message::Shuffle`]
|
||||
///
|
||||
/// > A node q that receives a Shuffle request will first decrease its time to live. If the time
|
||||
/// > to live of the message is greater than zero and the number of nodes in q’s active view is
|
||||
/// > greater than 1, the node will select a random node from its active view, different from the
|
||||
/// > one he received this shuffle message from, and simply forwards the Shuffle request.
|
||||
/// > Otherwise, node q accepts the Shuffle request and send back (p.8)
|
||||
fn on_shuffle(&mut self, from: PI, shuffle: Shuffle<PI>, io: &mut impl IO<PI>) {
|
||||
if shuffle.ttl.expired() || self.active_view.len() <= 1 {
|
||||
let len = shuffle.nodes.len();
|
||||
for node in shuffle.nodes {
|
||||
self.add_passive(node.id, node.data, io);
|
||||
}
|
||||
self.send_shuffle_reply(shuffle.origin, len, io);
|
||||
} else if let Some(node) = self
|
||||
.active_view
|
||||
.pick_random_without(&[&shuffle.origin, &from], &mut self.rng)
|
||||
{
|
||||
let message = Message::Shuffle(Shuffle {
|
||||
origin: shuffle.origin,
|
||||
nodes: shuffle.nodes,
|
||||
ttl: shuffle.ttl.next(),
|
||||
});
|
||||
io.push(OutEvent::SendMessage(*node, message));
|
||||
}
|
||||
}
|
||||
|
||||
fn send_shuffle_reply(&mut self, to: PI, len: usize, io: &mut impl IO<PI>) {
|
||||
let mut nodes = self.passive_view.shuffled_and_capped(len, &mut self.rng);
|
||||
// If we don't have enough passive nodes for the expected length, we fill with
|
||||
// active nodes.
|
||||
if nodes.len() < len {
|
||||
nodes.extend(
|
||||
self.active_view
|
||||
.shuffled_and_capped(len - nodes.len(), &mut self.rng),
|
||||
);
|
||||
}
|
||||
let nodes = nodes.into_iter().map(|id| self.peer_info(&id));
|
||||
let message = Message::ShuffleReply(ShuffleReply {
|
||||
nodes: nodes.collect(),
|
||||
});
|
||||
io.push(OutEvent::SendMessage(to, message));
|
||||
}
|
||||
|
||||
fn on_shuffle_reply(&mut self, message: ShuffleReply<PI>, io: &mut impl IO<PI>) {
|
||||
for node in message.nodes {
|
||||
self.add_passive(node.id, node.data, io);
|
||||
}
|
||||
self.refill_active_from_passive(&[], io);
|
||||
}
|
||||
|
||||
fn handle_shuffle_timer(&mut self, io: &mut impl IO<PI>) {
|
||||
if let Some(node) = self.active_view.pick_random(&mut self.rng) {
|
||||
let active = self.active_view.shuffled_without_and_capped(
|
||||
&[node],
|
||||
self.config.shuffle_active_view_count,
|
||||
&mut self.rng,
|
||||
);
|
||||
let passive = self.passive_view.shuffled_without_and_capped(
|
||||
&[node],
|
||||
self.config.shuffle_passive_view_count,
|
||||
&mut self.rng,
|
||||
);
|
||||
let nodes = active
|
||||
.iter()
|
||||
.chain(passive.iter())
|
||||
.map(|id| self.peer_info(id));
|
||||
let me = PeerInfo {
|
||||
id: self.me,
|
||||
data: self.me_data.clone(),
|
||||
};
|
||||
let nodes = nodes.chain([me]);
|
||||
let message = Shuffle {
|
||||
origin: self.me,
|
||||
nodes: nodes.collect(),
|
||||
ttl: self.config.shuffle_random_walk_length,
|
||||
};
|
||||
io.push(OutEvent::SendMessage(*node, Message::Shuffle(message)));
|
||||
}
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.shuffle_interval,
|
||||
Timer::DoShuffle,
|
||||
));
|
||||
}
|
||||
|
||||
fn passive_is_full(&self) -> bool {
|
||||
self.passive_view.len() >= self.config.passive_view_capacity
|
||||
}
|
||||
|
||||
fn active_is_full(&self) -> bool {
|
||||
self.active_view.len() >= self.config.active_view_capacity
|
||||
}
|
||||
|
||||
/// Add a peer to the passive view.
|
||||
///
|
||||
/// If the passive view is full, it will first remove a random peer and then insert the new peer.
|
||||
/// If a peer is currently in the active view it will not be added.
|
||||
fn add_passive(&mut self, peer: PI, data: Option<PeerData>, io: &mut impl IO<PI>) {
|
||||
self.insert_peer_info((peer, data).into(), io);
|
||||
if self.active_view.contains(&peer) || self.passive_view.contains(&peer) || peer == self.me
|
||||
{
|
||||
return;
|
||||
}
|
||||
if self.passive_is_full() {
|
||||
self.passive_view.remove_random(&mut self.rng);
|
||||
}
|
||||
self.passive_view.insert(peer);
|
||||
}
|
||||
|
||||
/// Remove a peer from the active view.
|
||||
///
|
||||
/// If `reason` is [`RemovalReason::Random`], a [`Disconnect`] message will be sent to the peer.
|
||||
fn remove_active(&mut self, peer: &PI, reason: RemovalReason, io: &mut impl IO<PI>) {
|
||||
if let Some(idx) = self.active_view.get_index_of(peer) {
|
||||
let removed_peer = self.remove_active_by_index(idx, reason, io).unwrap();
|
||||
self.refill_active_from_passive(&[&removed_peer], io);
|
||||
}
|
||||
}
|
||||
|
||||
fn refill_active_from_passive(&mut self, skip_peers: &[&PI], io: &mut impl IO<PI>) {
|
||||
if self.active_view.len() + self.pending_neighbor_requests.len()
|
||||
>= self.config.active_view_capacity
|
||||
{
|
||||
return;
|
||||
}
|
||||
// "When a node p suspects that one of the nodes present in its active view has failed
|
||||
// (by either disconnecting or blocking), it selects a random node q from its passive view and
|
||||
// attempts to establish a TCP connection with q. If the connection fails to establish,
|
||||
// node q is considered failed and removed from p’s passive view; another node q′ is selected
|
||||
// at random and a new attempt is made. The procedure is repeated until a connection is established
|
||||
// with success." (p7)
|
||||
let mut skip_peers = skip_peers.to_vec();
|
||||
skip_peers.extend(self.pending_neighbor_requests.iter());
|
||||
|
||||
if let Some(node) = self
|
||||
.passive_view
|
||||
.pick_random_without(&skip_peers, &mut self.rng)
|
||||
.copied()
|
||||
{
|
||||
let priority = match self.active_view.is_empty() {
|
||||
true => Priority::High,
|
||||
false => Priority::Low,
|
||||
};
|
||||
self.send_neighbor(node, priority, io);
|
||||
// schedule a timer that checks if the node replied with a neighbor message,
|
||||
// otherwise try again with another passive node.
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.neighbor_request_timeout,
|
||||
Timer::PendingNeighborRequest(node),
|
||||
));
|
||||
};
|
||||
}
|
||||
|
||||
fn handle_pending_neighbor_timer(&mut self, peer: PI, io: &mut impl IO<PI>) {
|
||||
if self.pending_neighbor_requests.remove(&peer) {
|
||||
self.passive_view.remove(&peer);
|
||||
self.refill_active_from_passive(&[], io);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_active_by_index(
|
||||
&mut self,
|
||||
peer_index: usize,
|
||||
reason: RemovalReason,
|
||||
io: &mut impl IO<PI>,
|
||||
) -> Option<PI> {
|
||||
if let Some(peer) = self.active_view.remove_index(peer_index) {
|
||||
io.push(OutEvent::EmitEvent(Event::NeighborDown(peer)));
|
||||
|
||||
match reason {
|
||||
// send a disconnect message, then close connection.
|
||||
RemovalReason::Random => self.send_disconnect(peer, true, io),
|
||||
// close connection without sending anything further.
|
||||
RemovalReason::DisconnectReceived { is_alive: _ } => {
|
||||
io.push(OutEvent::DisconnectPeer(peer))
|
||||
}
|
||||
RemovalReason::ConnectionClosed => io.push(OutEvent::DisconnectPeer(peer)),
|
||||
}
|
||||
|
||||
let keep_as_passive = match reason {
|
||||
// keep alive if previously marked as alive.
|
||||
RemovalReason::ConnectionClosed => self.alive_disconnect_peers.remove(&peer),
|
||||
// keep alive if other peer said to be still alive.
|
||||
RemovalReason::DisconnectReceived { is_alive } => is_alive,
|
||||
// keep alive (only we are removing for now)
|
||||
RemovalReason::Random => true,
|
||||
};
|
||||
|
||||
if keep_as_passive {
|
||||
let data = self.peer_data.remove(&peer);
|
||||
self.add_passive(peer, data, io);
|
||||
// mark peer as alive, so it doesn't get removed from the passive view if the conn closes.
|
||||
if !matches!(reason, RemovalReason::ConnectionClosed) {
|
||||
self.alive_disconnect_peers.insert(peer);
|
||||
}
|
||||
}
|
||||
debug!(other = ?peer, "removed from active view, reason: {reason:?}");
|
||||
Some(peer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a random peer from the active view.
|
||||
fn free_random_slot_in_active_view(&mut self, io: &mut impl IO<PI>) {
|
||||
if let Some(index) = self.active_view.pick_random_index(&mut self.rng) {
|
||||
self.remove_active_by_index(index, RemovalReason::Random, io);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a peer to the active view.
|
||||
///
|
||||
/// If the active view is currently full, a random peer will be removed first.
|
||||
/// Sends a Neighbor message to the peer. If high_priority is true, the peer
|
||||
/// may not deny the Neighbor request.
|
||||
fn add_active(
|
||||
&mut self,
|
||||
peer: PI,
|
||||
data: Option<PeerData>,
|
||||
priority: Priority,
|
||||
reply: bool,
|
||||
io: &mut impl IO<PI>,
|
||||
) -> bool {
|
||||
if peer == self.me {
|
||||
return false;
|
||||
}
|
||||
self.insert_peer_info((peer, data).into(), io);
|
||||
if self.active_view.contains(&peer) {
|
||||
if reply {
|
||||
self.send_neighbor(peer, priority, io);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
match (priority, self.active_is_full()) {
|
||||
(Priority::High, is_full) => {
|
||||
if is_full {
|
||||
self.free_random_slot_in_active_view(io);
|
||||
}
|
||||
self.add_active_unchecked(peer, Priority::High, reply, io);
|
||||
true
|
||||
}
|
||||
(Priority::Low, false) => {
|
||||
self.add_active_unchecked(peer, Priority::Low, reply, io);
|
||||
true
|
||||
}
|
||||
(Priority::Low, true) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_active_unchecked(
|
||||
&mut self,
|
||||
peer: PI,
|
||||
priority: Priority,
|
||||
reply: bool,
|
||||
io: &mut impl IO<PI>,
|
||||
) {
|
||||
self.passive_view.remove(&peer);
|
||||
if self.active_view.insert(peer) {
|
||||
debug!(other = ?peer, "add to active view");
|
||||
io.push(OutEvent::EmitEvent(Event::NeighborUp(peer)));
|
||||
if reply {
|
||||
self.send_neighbor(peer, priority, io);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_neighbor(&mut self, peer: PI, priority: Priority, io: &mut impl IO<PI>) {
|
||||
if self.pending_neighbor_requests.insert(peer) {
|
||||
let message = Message::Neighbor(Neighbor {
|
||||
priority,
|
||||
data: self.me_data.clone(),
|
||||
});
|
||||
io.push(OutEvent::SendMessage(peer, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RemovalReason {
|
||||
/// A peer is removed because the connection was closed ungracefully.
|
||||
ConnectionClosed,
|
||||
/// A peer is removed because we received a disconnect message.
|
||||
DisconnectReceived { is_alive: bool },
|
||||
/// A peer is removed after random selection to make room for a newly joined peer.
|
||||
Random,
|
||||
}
|
||||
909
third_party/iroh-org/iroh-gossip/src/proto/plumtree.rs
vendored
Normal file
909
third_party/iroh-org/iroh-gossip/src/proto/plumtree.rs
vendored
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
//! Implementation of the Plumtree epidemic broadcast tree protocol
|
||||
//!
|
||||
//! The implementation is based on [this paper][paper] by Joao Leitao, Jose Pereira, Luıs Rodrigues
|
||||
//! and the [example implementation][impl] by Bartosz Sypytkowski
|
||||
//!
|
||||
//! [paper]: https://asc.di.fct.unl.pt/~jleitao/pdf/srds07-leitao.pdf
|
||||
//! [impl]: https://gist.github.com/Horusiath/84fac596101b197da0546d1697580d99
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use derive_more::{Add, From, Sub};
|
||||
use n0_future::time::{Duration, Instant};
|
||||
use postcard::experimental::max_size::MaxSize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
use super::{
|
||||
util::{idbytes_impls, TimeBoundCache},
|
||||
PeerIdentity, IO,
|
||||
};
|
||||
|
||||
/// A message identifier, which is the message content's blake3 hash.
|
||||
#[derive(Serialize, Deserialize, Clone, Hash, Copy, PartialEq, Eq, MaxSize)]
|
||||
pub struct MessageId([u8; 32]);
|
||||
idbytes_impls!(MessageId, "MessageId");
|
||||
|
||||
impl MessageId {
|
||||
/// Create a `[MessageId]` by hashing the message content.
|
||||
///
|
||||
/// This hashes the input with [`blake3::hash`].
|
||||
pub fn from_content(message: &[u8]) -> Self {
|
||||
Self::from(blake3::hash(message))
|
||||
}
|
||||
}
|
||||
|
||||
/// Events Plumtree is informed of from the peer sampling service and IO layer.
|
||||
#[derive(Debug)]
|
||||
pub enum InEvent<PI> {
|
||||
/// A [`Message`] was received from the peer.
|
||||
RecvMessage(PI, Message),
|
||||
/// Broadcast the contained payload to the given scope.
|
||||
Broadcast(Bytes, Scope),
|
||||
/// A timer has expired.
|
||||
TimerExpired(Timer),
|
||||
/// New member `PI` has joined the topic.
|
||||
NeighborUp(PI),
|
||||
/// Peer `PI` has disconnected from the topic.
|
||||
NeighborDown(PI),
|
||||
}
|
||||
|
||||
/// Events Plumtree emits.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum OutEvent<PI> {
|
||||
/// Ask the IO layer to send a [`Message`] to peer `PI`.
|
||||
SendMessage(PI, Message),
|
||||
/// Schedule a [`Timer`].
|
||||
ScheduleTimer(Duration, Timer),
|
||||
/// Emit an [`Event`] to the application.
|
||||
EmitEvent(Event<PI>),
|
||||
}
|
||||
|
||||
/// Kinds of timers Plumtree needs to schedule.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum Timer {
|
||||
/// Request the content for [`MessageId`] by sending [`Message::Graft`].
|
||||
///
|
||||
/// The message will be sent to a peer that sent us an [`Message::IHave`] for this [`MessageId`],
|
||||
/// which will send us the message content in reply and also move the peer into the eager set.
|
||||
/// Will be a no-op if the message for [`MessageId`] was already received from another peer by now.
|
||||
SendGraft(MessageId),
|
||||
/// Dispatch the [`Message::IHave`] in our lazy push queue.
|
||||
DispatchLazyPush,
|
||||
/// Evict the message cache
|
||||
EvictCache,
|
||||
}
|
||||
|
||||
/// Event emitted by the [`State`] to the application.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event<PI> {
|
||||
/// A new gossip message was received.
|
||||
Received(GossipEvent<PI>),
|
||||
}
|
||||
|
||||
#[derive(Clone, derive_more::Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)]
|
||||
pub struct GossipEvent<PI> {
|
||||
/// The content of the gossip message.
|
||||
#[debug("<{}b>", content.len())]
|
||||
pub content: Bytes,
|
||||
/// The peer that we received the gossip message from. Note that this is not the peer that
|
||||
/// originally broadcasted the message, but the peer before us in the gossiping path.
|
||||
pub delivered_from: PI,
|
||||
/// The broadcast scope of the message.
|
||||
pub scope: DeliveryScope,
|
||||
}
|
||||
|
||||
impl<PI> GossipEvent<PI> {
|
||||
fn from_message(message: &Gossip, from: PI) -> Self {
|
||||
Self {
|
||||
content: message.content.clone(),
|
||||
scope: message.scope,
|
||||
delivered_from: from,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of delivery hops a message has taken.
|
||||
#[derive(
|
||||
From,
|
||||
Add,
|
||||
Sub,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Eq,
|
||||
PartialEq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
Hash,
|
||||
MaxSize,
|
||||
)]
|
||||
pub struct Round(u16);
|
||||
|
||||
impl Round {
|
||||
pub fn next(&self) -> Round {
|
||||
Round(self.0 + 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Messages that we can send and receive from peers within the topic.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub enum Message {
|
||||
/// When receiving Gossip, emit as event and forward full message to eager peer and (after a
|
||||
/// delay) message IDs to lazy peers.
|
||||
Gossip(Gossip),
|
||||
/// When receiving Prune, move the peer from the eager to the lazy set.
|
||||
Prune,
|
||||
/// When receiving Graft, move the peer to the eager set and send the full content for the
|
||||
/// included message ID.
|
||||
Graft(Graft),
|
||||
/// When receiving IHave, do nothing initially, and request the messages for the included
|
||||
/// message IDs after some time if they aren't pushed eagerly to us.
|
||||
IHave(Vec<IHave>),
|
||||
}
|
||||
|
||||
/// Payload messages transmitted by the protocol.
|
||||
#[derive(Serialize, Deserialize, Clone, derive_more::Debug, PartialEq, Eq)]
|
||||
pub struct Gossip {
|
||||
/// Id of the message.
|
||||
id: MessageId,
|
||||
/// Message contents.
|
||||
#[debug("<{}b>", content.len())]
|
||||
content: Bytes,
|
||||
/// Scope to broadcast to.
|
||||
scope: DeliveryScope,
|
||||
}
|
||||
|
||||
impl Gossip {
|
||||
fn round(&self) -> Option<Round> {
|
||||
match self.scope {
|
||||
DeliveryScope::Swarm(round) => Some(round),
|
||||
DeliveryScope::Neighbors => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The scope to deliver the message to.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Copy)]
|
||||
pub enum DeliveryScope {
|
||||
/// This message was received from the swarm, with a distance (in hops) travelled from the
|
||||
/// original broadcaster.
|
||||
Swarm(Round),
|
||||
/// This message was received from a direct neighbor that broadcasted the message to neighbors
|
||||
/// only.
|
||||
Neighbors,
|
||||
}
|
||||
|
||||
impl DeliveryScope {
|
||||
/// Whether this message was directly received from its publisher.
|
||||
pub fn is_direct(&self) -> bool {
|
||||
matches!(self, Self::Neighbors | Self::Swarm(Round(0)))
|
||||
}
|
||||
}
|
||||
|
||||
/// The broadcast scope of a gossip message.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Copy)]
|
||||
pub enum Scope {
|
||||
/// The message is broadcast to all peers in the swarm.
|
||||
Swarm,
|
||||
/// The message is broadcast only to the immediate neighbors of a peer.
|
||||
Neighbors,
|
||||
}
|
||||
|
||||
impl Gossip {
|
||||
/// Get a clone of this `Gossip` message and increase the delivery round by 1.
|
||||
pub fn next_round(&self) -> Option<Gossip> {
|
||||
match self.scope {
|
||||
DeliveryScope::Neighbors => None,
|
||||
DeliveryScope::Swarm(round) => Some(Gossip {
|
||||
id: self.id,
|
||||
content: self.content.clone(),
|
||||
scope: DeliveryScope::Swarm(round.next()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the message id is the blake3 hash of the message content.
|
||||
pub fn validate(&self) -> bool {
|
||||
let expected = MessageId::from_content(&self.content);
|
||||
expected == self.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Control message to inform peers we have a message without transmitting the whole payload.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, MaxSize)]
|
||||
pub struct IHave {
|
||||
/// Id of the message.
|
||||
pub(crate) id: MessageId,
|
||||
/// Delivery round of the message.
|
||||
pub(crate) round: Round,
|
||||
}
|
||||
|
||||
/// Control message to signal a peer that they have been moved to the eager set, and to ask the
|
||||
/// peer to do the same with this node.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct Graft {
|
||||
/// Message id that triggers the graft, if any.
|
||||
/// On receiving a graft, the payload message must be sent in reply if a message id is set.
|
||||
id: Option<MessageId>,
|
||||
/// Delivery round of the [`Message::IHave`] that triggered this Graft message.
|
||||
round: Round,
|
||||
}
|
||||
|
||||
/// Configuration for the gossip broadcast layer.
|
||||
///
|
||||
/// Currently, the expectation is that the configuration is the same for all peers in the
|
||||
/// network (as recommended in the paper).
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// When receiving an `IHave` message, this timeout is registered. If the message for the
|
||||
/// `IHave` was not received once the timeout is expired, a `Graft` message is sent to the
|
||||
/// peer that sent us the `IHave` to request the message payload.
|
||||
///
|
||||
/// The plumtree paper notes:
|
||||
/// > The timeout value is a protocol parameter that should be configured considering the
|
||||
/// > diameter of the overlay and a target maximum recovery latency, defined by the application
|
||||
/// > requirements. (p.8)
|
||||
pub graft_timeout_1: Duration,
|
||||
/// This timeout is registered when sending a `Graft` message. If a reply has not been
|
||||
/// received once the timeout expires, we send another `Graft` message to the next peer that
|
||||
/// sent us an `IHave` for this message.
|
||||
///
|
||||
/// The plumtree paper notes:
|
||||
/// > This second timeout value should be smaller that the first, in the order of an average
|
||||
/// > round trip time to a neighbor.
|
||||
pub graft_timeout_2: Duration,
|
||||
/// Timeout after which `IHave` messages are pushed to peers.
|
||||
pub dispatch_timeout: Duration,
|
||||
/// The protocol performs a tree optimization, which promotes lazy peers to eager peers if the
|
||||
/// `Message::IHave` messages received from them have a lower number of hops from the
|
||||
/// message's origin as the `InEvent::Broadcast` messages received from our eager peers. This
|
||||
/// parameter is the number of hops that the lazy peers must be closer to the origin than our
|
||||
/// eager peers to be promoted to become an eager peer.
|
||||
pub optimization_threshold: Round,
|
||||
|
||||
/// Duration for which to keep gossip messages in the internal message cache.
|
||||
///
|
||||
/// Messages broadcast from this node or received from other nodes are kept in an internal
|
||||
/// cache for this duration before being evicted. If this is too low, other nodes will not be
|
||||
/// able to retrieve messages once they need them. If this is high, the cache will grow.
|
||||
///
|
||||
/// Should be at least around several round trip times to peers.
|
||||
pub message_cache_retention: Duration,
|
||||
|
||||
/// Duration for which to keep the `MessageId`s for received messages.
|
||||
///
|
||||
/// Should be at least as long as [`Self::message_cache_retention`], usually will be longer to
|
||||
/// not accidentally receive messages multiple times.
|
||||
pub message_id_retention: Duration,
|
||||
|
||||
/// How often the internal caches will be checked for expired items.
|
||||
pub cache_evict_interval: Duration,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
/// Sensible defaults for the plumtree configuration
|
||||
//
|
||||
// TODO: Find out what good defaults are for the three timeouts here. Current numbers are
|
||||
// guesses that need validation. The paper does not have concrete recommendations for these
|
||||
// numbers.
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Paper: "The timeout value is a protocol parameter that should be configured considering
|
||||
// the diameter of the overlay and a target maximum recovery latency, defined by the
|
||||
// application requirements. This is a parameter that should be statically configured
|
||||
// at deployment time." (p. 8)
|
||||
//
|
||||
// Earthstar has 5ms it seems, see https://github.com/earthstar-project/earthstar/blob/1523c640fedf106f598bf79b184fb0ada64b1cc0/src/syncer/plum_tree.ts#L75
|
||||
// However in the paper it is more like a few roundtrips if I read things correctly.
|
||||
graft_timeout_1: Duration::from_millis(80),
|
||||
|
||||
// Paper: "This second timeout value should be smaller that the first, in the order of an
|
||||
// average round trip time to a neighbor." (p. 9)
|
||||
//
|
||||
// Earthstar doesn't have this step from my reading.
|
||||
graft_timeout_2: Duration::from_millis(40),
|
||||
|
||||
// Again, paper does not tell a recommended number here. Likely should be quite small,
|
||||
// as to not delay messages without need. This would also be the time frame in which
|
||||
// `IHave`s are aggregated to save on packets.
|
||||
//
|
||||
// Eartstar dispatches immediately from my reading.
|
||||
dispatch_timeout: Duration::from_millis(5),
|
||||
|
||||
// This number comes from experiment settings the plumtree paper (p. 12)
|
||||
optimization_threshold: Round(7),
|
||||
|
||||
// This is a certainly-high-enough value for usual operation.
|
||||
message_cache_retention: Duration::from_secs(30),
|
||||
message_id_retention: Duration::from_secs(90),
|
||||
cache_evict_interval: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Stats about this topic's plumtree.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Stats {
|
||||
/// Number of payload messages received so far.
|
||||
///
|
||||
/// See [`Message::Gossip`].
|
||||
pub payload_messages_received: u64,
|
||||
/// Number of control messages received so far.
|
||||
///
|
||||
/// See [`Message::Prune`], [`Message::Graft`], [`Message::IHave`].
|
||||
pub control_messages_received: u64,
|
||||
/// Max round seen so far.
|
||||
pub max_last_delivery_hop: u16,
|
||||
}
|
||||
|
||||
/// State of the plumtree.
|
||||
#[derive(Debug)]
|
||||
pub struct State<PI> {
|
||||
/// Our address.
|
||||
me: PI,
|
||||
/// Configuration for this plumtree.
|
||||
config: Config,
|
||||
|
||||
/// Set of peers used for payload exchange.
|
||||
pub(crate) eager_push_peers: BTreeSet<PI>,
|
||||
/// Set of peers used for control message exchange.
|
||||
pub(crate) lazy_push_peers: BTreeSet<PI>,
|
||||
|
||||
lazy_push_queue: BTreeMap<PI, Vec<IHave>>,
|
||||
|
||||
/// Messages for which a [`MessageId`] has been seen via a [`Message::IHave`] but we have not
|
||||
/// yet received the full payload. For each, we store the peers that have claimed to have this
|
||||
/// message.
|
||||
missing_messages: HashMap<MessageId, VecDeque<(PI, Round)>>,
|
||||
/// Messages for which the full payload has been seen.
|
||||
received_messages: TimeBoundCache<MessageId, ()>,
|
||||
/// Payloads of received messages.
|
||||
cache: TimeBoundCache<MessageId, Gossip>,
|
||||
|
||||
/// Message ids for which a [`Timer::SendGraft`] has been scheduled.
|
||||
graft_timer_scheduled: HashSet<MessageId>,
|
||||
/// Whether a [`Timer::DispatchLazyPush`] has been scheduled.
|
||||
dispatch_timer_scheduled: bool,
|
||||
|
||||
/// Set to false after the first message is received. Used for initial timer scheduling.
|
||||
init: bool,
|
||||
|
||||
/// [`Stats`] of this plumtree.
|
||||
pub(crate) stats: Stats,
|
||||
|
||||
max_message_size: usize,
|
||||
}
|
||||
|
||||
impl<PI: PeerIdentity> State<PI> {
|
||||
/// Initialize the [`State`] of a plumtree.
|
||||
pub fn new(me: PI, config: Config, max_message_size: usize) -> Self {
|
||||
Self {
|
||||
me,
|
||||
eager_push_peers: Default::default(),
|
||||
lazy_push_peers: Default::default(),
|
||||
lazy_push_queue: Default::default(),
|
||||
config,
|
||||
missing_messages: Default::default(),
|
||||
received_messages: Default::default(),
|
||||
graft_timer_scheduled: Default::default(),
|
||||
dispatch_timer_scheduled: false,
|
||||
cache: Default::default(),
|
||||
init: false,
|
||||
stats: Default::default(),
|
||||
max_message_size,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an [`InEvent`].
|
||||
pub fn handle(&mut self, event: InEvent<PI>, now: Instant, io: &mut impl IO<PI>) {
|
||||
if !self.init {
|
||||
self.init = true;
|
||||
self.on_evict_cache_timer(now, io)
|
||||
}
|
||||
match event {
|
||||
InEvent::RecvMessage(from, message) => self.handle_message(from, message, now, io),
|
||||
InEvent::Broadcast(data, scope) => self.broadcast(data, scope, now, io),
|
||||
InEvent::NeighborUp(peer) => self.on_neighbor_up(peer),
|
||||
InEvent::NeighborDown(peer) => self.on_neighbor_down(peer),
|
||||
InEvent::TimerExpired(timer) => match timer {
|
||||
Timer::DispatchLazyPush => self.on_dispatch_timer(io),
|
||||
Timer::SendGraft(id) => {
|
||||
self.on_send_graft_timer(id, io);
|
||||
}
|
||||
Timer::EvictCache => self.on_evict_cache_timer(now, io),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get access to the [`Stats`] of the plumtree.
|
||||
pub fn stats(&self) -> &Stats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Handle receiving a [`Message`].
|
||||
fn handle_message(&mut self, sender: PI, message: Message, now: Instant, io: &mut impl IO<PI>) {
|
||||
if matches!(message, Message::Gossip(_)) {
|
||||
self.stats.payload_messages_received += 1;
|
||||
} else {
|
||||
self.stats.control_messages_received += 1;
|
||||
}
|
||||
match message {
|
||||
Message::Gossip(details) => self.on_gossip(sender, details, now, io),
|
||||
Message::Prune => self.on_prune(sender),
|
||||
Message::IHave(details) => self.on_ihave(sender, details, io),
|
||||
Message::Graft(details) => self.on_graft(sender, details, io),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches messages from lazy queue over to lazy peers.
|
||||
fn on_dispatch_timer(&mut self, io: &mut impl IO<PI>) {
|
||||
let chunk_size = self.max_message_size
|
||||
// Space for discriminator
|
||||
- 1
|
||||
// Space for length prefix
|
||||
- 2;
|
||||
let chunk_len = chunk_size / IHave::POSTCARD_MAX_SIZE;
|
||||
while let Some((peer, list)) = self.lazy_push_queue.pop_first() {
|
||||
for chunk in list.chunks(chunk_len) {
|
||||
io.push(OutEvent::SendMessage(peer, Message::IHave(chunk.to_vec())));
|
||||
}
|
||||
}
|
||||
|
||||
self.dispatch_timer_scheduled = false;
|
||||
}
|
||||
|
||||
/// Send a gossip message.
|
||||
///
|
||||
/// Will be pushed in full to eager peers.
|
||||
/// Pushing the message id to the lazy peers is delayed by a timer.
|
||||
fn broadcast(&mut self, content: Bytes, scope: Scope, now: Instant, io: &mut impl IO<PI>) {
|
||||
let id = MessageId::from_content(&content);
|
||||
let scope = match scope {
|
||||
Scope::Neighbors => DeliveryScope::Neighbors,
|
||||
Scope::Swarm => DeliveryScope::Swarm(Round(0)),
|
||||
};
|
||||
let message = Gossip { id, content, scope };
|
||||
let me = self.me;
|
||||
if let DeliveryScope::Swarm(_) = scope {
|
||||
self.received_messages
|
||||
.insert(id, (), now + self.config.message_id_retention);
|
||||
self.cache.insert(
|
||||
id,
|
||||
message.clone(),
|
||||
now + self.config.message_cache_retention,
|
||||
);
|
||||
self.lazy_push(message.clone(), &me, io);
|
||||
}
|
||||
|
||||
self.eager_push(message.clone(), &me, io);
|
||||
}
|
||||
|
||||
/// Handle receiving a [`Message::Gossip`].
|
||||
fn on_gossip(&mut self, sender: PI, message: Gossip, now: Instant, io: &mut impl IO<PI>) {
|
||||
// Validate that the message id is the blake3 hash of the message content.
|
||||
if !message.validate() {
|
||||
// TODO: Do we want to take any measures against the sender if we received a message
|
||||
// with a spoofed message id?
|
||||
warn!(
|
||||
peer = ?sender,
|
||||
"Received a message with spoofed message id ({})", message.id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// if we already received this message: move peer to lazy set
|
||||
// and notify peer about this.
|
||||
if self.received_messages.contains_key(&message.id) {
|
||||
self.add_lazy(sender);
|
||||
io.push(OutEvent::SendMessage(sender, Message::Prune));
|
||||
// otherwise store the message, emit to application and forward to peers
|
||||
} else {
|
||||
if let DeliveryScope::Swarm(prev_round) = message.scope {
|
||||
// insert the message in the list of received messages
|
||||
self.received_messages.insert(
|
||||
message.id,
|
||||
(),
|
||||
now + self.config.message_id_retention,
|
||||
);
|
||||
// increase the round for forwarding the message, and add to cache
|
||||
// to reply to Graft messages later
|
||||
// TODO: add callback/event to application to get missing messages that were received before?
|
||||
let message = message.next_round().expect("just checked");
|
||||
|
||||
self.cache.insert(
|
||||
message.id,
|
||||
message.clone(),
|
||||
now + self.config.message_cache_retention,
|
||||
);
|
||||
// push the message to our peers
|
||||
self.eager_push(message.clone(), &sender, io);
|
||||
self.lazy_push(message.clone(), &sender, io);
|
||||
// cleanup places where we track missing messages
|
||||
self.graft_timer_scheduled.remove(&message.id);
|
||||
let previous_ihaves = self.missing_messages.remove(&message.id);
|
||||
// do the optimization step from the paper
|
||||
if let Some(previous_ihaves) = previous_ihaves {
|
||||
self.optimize_tree(&sender, &message, previous_ihaves, io);
|
||||
}
|
||||
self.stats.max_last_delivery_hop =
|
||||
self.stats.max_last_delivery_hop.max(prev_round.0);
|
||||
}
|
||||
|
||||
// emit event to application
|
||||
io.push(OutEvent::EmitEvent(Event::Received(
|
||||
GossipEvent::from_message(&message, sender),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimize the tree by pruning the `sender` of a [`Message::Gossip`] if we previously
|
||||
/// received a [`Message::IHave`] for the same message with a much lower number of delivery
|
||||
/// hops from the original broadcaster of the message.
|
||||
///
|
||||
/// See [Config::optimization_threshold].
|
||||
fn optimize_tree(
|
||||
&mut self,
|
||||
gossip_sender: &PI,
|
||||
message: &Gossip,
|
||||
previous_ihaves: VecDeque<(PI, Round)>,
|
||||
io: &mut impl IO<PI>,
|
||||
) {
|
||||
let round = message.round().expect("only called for swarm messages");
|
||||
let best_ihave = previous_ihaves
|
||||
.iter()
|
||||
.min_by(|(_a_peer, a_round), (_b_peer, b_round)| a_round.cmp(b_round))
|
||||
.copied();
|
||||
|
||||
if let Some((ihave_peer, ihave_round)) = best_ihave {
|
||||
if (ihave_round < round) && (round - ihave_round) >= self.config.optimization_threshold
|
||||
{
|
||||
// Graft the sender of the IHave, but only if it's not already eager.
|
||||
if !self.eager_push_peers.contains(&ihave_peer) {
|
||||
let message = Message::Graft(Graft {
|
||||
id: None,
|
||||
round: ihave_round,
|
||||
});
|
||||
self.add_eager(ihave_peer);
|
||||
io.push(OutEvent::SendMessage(ihave_peer, message));
|
||||
}
|
||||
// Prune the sender of the Gossip.
|
||||
self.add_lazy(*gossip_sender);
|
||||
io.push(OutEvent::SendMessage(*gossip_sender, Message::Prune));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle receiving a [`Message::Prune`].
|
||||
fn on_prune(&mut self, sender: PI) {
|
||||
self.add_lazy(sender);
|
||||
}
|
||||
|
||||
/// Handle receiving a [`Message::IHave`].
|
||||
///
|
||||
/// > When a node receives a IHAVE message, it simply marks the corresponding message as
|
||||
/// > missing It then starts a timer, with a predefined timeout value, and waits for the missing
|
||||
/// > message to be received via eager push before the timer expires. The timeout value is a
|
||||
/// > protocol parameter that should be configured considering the diameter of the overlay and a
|
||||
/// > target maximum recovery latency, defined by the application requirements. This is a
|
||||
/// > parameter that should be statically configured at deployment time. (p8)
|
||||
fn on_ihave(&mut self, sender: PI, ihaves: Vec<IHave>, io: &mut impl IO<PI>) {
|
||||
for ihave in ihaves {
|
||||
if !self.received_messages.contains_key(&ihave.id) {
|
||||
self.missing_messages
|
||||
.entry(ihave.id)
|
||||
.or_default()
|
||||
.push_back((sender, ihave.round));
|
||||
|
||||
if !self.graft_timer_scheduled.contains(&ihave.id) {
|
||||
self.graft_timer_scheduled.insert(ihave.id);
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.graft_timeout_1,
|
||||
Timer::SendGraft(ihave.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A scheduled [`Timer::SendGraft`] has reached it's deadline.
|
||||
fn on_send_graft_timer(&mut self, id: MessageId, io: &mut impl IO<PI>) {
|
||||
self.graft_timer_scheduled.remove(&id);
|
||||
// if the message was received before the timer ran out, there is no need to request it
|
||||
// again
|
||||
if self.received_messages.contains_key(&id) {
|
||||
return;
|
||||
}
|
||||
// get the first peer that advertised this message
|
||||
let entry = self
|
||||
.missing_messages
|
||||
.get_mut(&id)
|
||||
.and_then(|entries| entries.pop_front());
|
||||
if let Some((peer, round)) = entry {
|
||||
self.add_eager(peer);
|
||||
let message = Message::Graft(Graft {
|
||||
id: Some(id),
|
||||
round,
|
||||
});
|
||||
io.push(OutEvent::SendMessage(peer, message));
|
||||
|
||||
// "when a GRAFT message is sent, another timer is started to expire after a certain timeout,
|
||||
// to ensure that the message will be requested to another neighbor if it is not received
|
||||
// meanwhile. This second timeout value should be smaller that the first, in the order of
|
||||
// an average round trip time to a neighbor." (p9)
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.graft_timeout_2,
|
||||
Timer::SendGraft(id),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle receiving a [`Message::Graft`].
|
||||
fn on_graft(&mut self, sender: PI, details: Graft, io: &mut impl IO<PI>) {
|
||||
self.add_eager(sender);
|
||||
if let Some(id) = details.id {
|
||||
if let Some(message) = self.cache.get(&id) {
|
||||
io.push(OutEvent::SendMessage(
|
||||
sender,
|
||||
Message::Gossip(message.clone()),
|
||||
));
|
||||
} else {
|
||||
debug!(?id, peer=?sender, "on_graft failed to graft: message not in cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a [`InEvent::NeighborUp`] when a peer joins the topic.
|
||||
fn on_neighbor_up(&mut self, peer: PI) {
|
||||
self.add_eager(peer);
|
||||
}
|
||||
|
||||
/// Handle a [`InEvent::NeighborDown`] when a peer leaves the topic.
|
||||
/// > When a neighbor is detected to leave the overlay, it is simple removed from the
|
||||
/// > membership. Furthermore, the record of IHAVE messages sent from failed members is deleted
|
||||
/// > from the missing history. (p9)
|
||||
fn on_neighbor_down(&mut self, peer: PI) {
|
||||
self.missing_messages.retain(|_message_id, ihaves| {
|
||||
ihaves.retain(|(ihave_peer, _round)| *ihave_peer != peer);
|
||||
!ihaves.is_empty()
|
||||
});
|
||||
self.eager_push_peers.remove(&peer);
|
||||
self.lazy_push_peers.remove(&peer);
|
||||
}
|
||||
|
||||
fn on_evict_cache_timer(&mut self, now: Instant, io: &mut impl IO<PI>) {
|
||||
self.cache.expire_until(now);
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.cache_evict_interval,
|
||||
Timer::EvictCache,
|
||||
));
|
||||
}
|
||||
|
||||
/// Moves peer into eager set.
|
||||
fn add_eager(&mut self, peer: PI) {
|
||||
self.lazy_push_peers.remove(&peer);
|
||||
self.eager_push_peers.insert(peer);
|
||||
}
|
||||
|
||||
/// Moves peer into lazy set.
|
||||
fn add_lazy(&mut self, peer: PI) {
|
||||
self.eager_push_peers.remove(&peer);
|
||||
self.lazy_push_peers.insert(peer);
|
||||
}
|
||||
|
||||
/// Immediately sends message to eager peers.
|
||||
fn eager_push(&mut self, gossip: Gossip, sender: &PI, io: &mut impl IO<PI>) {
|
||||
for peer in self
|
||||
.eager_push_peers
|
||||
.iter()
|
||||
.filter(|peer| **peer != self.me && *peer != sender)
|
||||
{
|
||||
io.push(OutEvent::SendMessage(
|
||||
*peer,
|
||||
Message::Gossip(gossip.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue lazy message announcements into the queue that will be sent out as batched
|
||||
/// [`Message::IHave`] messages once the [`Timer::DispatchLazyPush`] timer is triggered.
|
||||
fn lazy_push(&mut self, gossip: Gossip, sender: &PI, io: &mut impl IO<PI>) {
|
||||
let Some(round) = gossip.round() else {
|
||||
return;
|
||||
};
|
||||
for peer in self.lazy_push_peers.iter().filter(|x| *x != sender) {
|
||||
self.lazy_push_queue.entry(*peer).or_default().push(IHave {
|
||||
id: gossip.id,
|
||||
round,
|
||||
});
|
||||
}
|
||||
if !self.dispatch_timer_scheduled {
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
self.config.dispatch_timeout,
|
||||
Timer::DispatchLazyPush,
|
||||
));
|
||||
self.dispatch_timer_scheduled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn optimize_tree() {
|
||||
let mut io = VecDeque::new();
|
||||
let config: Config = Default::default();
|
||||
let mut state = State::new(1, config.clone(), 1024);
|
||||
let now = Instant::now();
|
||||
|
||||
// we receive an IHave message from peer 2
|
||||
// it has `round: 2` which means that the the peer that sent us the IHave was
|
||||
// two hops away from the original sender of the message
|
||||
let content: Bytes = b"hi".to_vec().into();
|
||||
let id = MessageId::from_content(&content);
|
||||
let event = InEvent::RecvMessage(
|
||||
2u32,
|
||||
Message::IHave(vec![IHave {
|
||||
id,
|
||||
round: Round(2),
|
||||
}]),
|
||||
);
|
||||
state.handle(event, now, &mut io);
|
||||
io.clear();
|
||||
// we then receive a `Gossip` message with the same `MessageId` from peer 3
|
||||
// the message has `round: 6`, which means it travelled 6 hops until it reached us
|
||||
// this is less hops than to peer 2, but not enough to trigger the optimization
|
||||
// because we use the default config which has `optimization_threshold: 7`
|
||||
let event = InEvent::RecvMessage(
|
||||
3,
|
||||
Message::Gossip(Gossip {
|
||||
id,
|
||||
content: content.clone(),
|
||||
scope: DeliveryScope::Swarm(Round(6)),
|
||||
}),
|
||||
);
|
||||
state.handle(event, now, &mut io);
|
||||
let expected = {
|
||||
// we expect a dispatch timer schedule and receive event, but no Graft or Prune
|
||||
// messages
|
||||
let mut io = VecDeque::new();
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
config.dispatch_timeout,
|
||||
Timer::DispatchLazyPush,
|
||||
));
|
||||
io.push(OutEvent::EmitEvent(Event::Received(GossipEvent {
|
||||
content,
|
||||
delivered_from: 3,
|
||||
scope: DeliveryScope::Swarm(Round(6)),
|
||||
})));
|
||||
io
|
||||
};
|
||||
assert_eq!(io, expected);
|
||||
io.clear();
|
||||
|
||||
// now we run the same flow again but this time peer 3 is 9 hops away from the message's
|
||||
// sender. message's sender. this will trigger the optimization:
|
||||
// peer 2 will be promoted to eager and peer 4 demoted to lazy
|
||||
|
||||
let content: Bytes = b"hi2".to_vec().into();
|
||||
let id = MessageId::from_content(&content);
|
||||
let event = InEvent::RecvMessage(
|
||||
2u32,
|
||||
Message::IHave(vec![IHave {
|
||||
id,
|
||||
round: Round(2),
|
||||
}]),
|
||||
);
|
||||
state.handle(event, now, &mut io);
|
||||
io.clear();
|
||||
|
||||
let event = InEvent::RecvMessage(
|
||||
3,
|
||||
Message::Gossip(Gossip {
|
||||
id,
|
||||
content: content.clone(),
|
||||
scope: DeliveryScope::Swarm(Round(9)),
|
||||
}),
|
||||
);
|
||||
state.handle(event, now, &mut io);
|
||||
let expected = {
|
||||
// this time we expect the Graft and Prune messages to be sent, performing the
|
||||
// optimization step
|
||||
let mut io = VecDeque::new();
|
||||
io.push(OutEvent::SendMessage(
|
||||
2,
|
||||
Message::Graft(Graft {
|
||||
id: None,
|
||||
round: Round(2),
|
||||
}),
|
||||
));
|
||||
io.push(OutEvent::SendMessage(3, Message::Prune));
|
||||
io.push(OutEvent::EmitEvent(Event::Received(GossipEvent {
|
||||
content,
|
||||
delivered_from: 3,
|
||||
scope: DeliveryScope::Swarm(Round(9)),
|
||||
})));
|
||||
io
|
||||
};
|
||||
assert_eq!(io, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spoofed_messages_are_ignored() {
|
||||
let config: Config = Default::default();
|
||||
let mut state = State::new(1, config.clone(), 1024);
|
||||
let now = Instant::now();
|
||||
|
||||
// we recv a correct gossip message and expect the Received event to be emitted
|
||||
let content: Bytes = b"hello1".to_vec().into();
|
||||
let message = Message::Gossip(Gossip {
|
||||
content: content.clone(),
|
||||
id: MessageId::from_content(&content),
|
||||
scope: DeliveryScope::Swarm(Round(1)),
|
||||
});
|
||||
let mut io = VecDeque::new();
|
||||
state.handle(InEvent::RecvMessage(2, message), now, &mut io);
|
||||
let expected = {
|
||||
let mut io = VecDeque::new();
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
config.cache_evict_interval,
|
||||
Timer::EvictCache,
|
||||
));
|
||||
io.push(OutEvent::ScheduleTimer(
|
||||
config.dispatch_timeout,
|
||||
Timer::DispatchLazyPush,
|
||||
));
|
||||
io.push(OutEvent::EmitEvent(Event::Received(GossipEvent {
|
||||
content,
|
||||
delivered_from: 2,
|
||||
scope: DeliveryScope::Swarm(Round(1)),
|
||||
})));
|
||||
io
|
||||
};
|
||||
assert_eq!(io, expected);
|
||||
|
||||
// now we recv with a spoofed id and expect no event to be emitted
|
||||
let content: Bytes = b"hello2".to_vec().into();
|
||||
let message = Message::Gossip(Gossip {
|
||||
content,
|
||||
id: MessageId::from_content(b"foo"),
|
||||
scope: DeliveryScope::Swarm(Round(1)),
|
||||
});
|
||||
let mut io = VecDeque::new();
|
||||
state.handle(InEvent::RecvMessage(2, message), now, &mut io);
|
||||
let expected = VecDeque::new();
|
||||
assert_eq!(io, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_is_evicted() {
|
||||
let config: Config = Default::default();
|
||||
let mut state = State::new(1, config.clone(), 1024);
|
||||
let now = Instant::now();
|
||||
let content: Bytes = b"hello1".to_vec().into();
|
||||
let message = Message::Gossip(Gossip {
|
||||
content: content.clone(),
|
||||
id: MessageId::from_content(&content),
|
||||
scope: DeliveryScope::Swarm(Round(1)),
|
||||
});
|
||||
let mut io = VecDeque::new();
|
||||
state.handle(InEvent::RecvMessage(2, message), now, &mut io);
|
||||
assert_eq!(state.cache.len(), 1);
|
||||
|
||||
let now = now + Duration::from_secs(1);
|
||||
state.handle(InEvent::TimerExpired(Timer::EvictCache), now, &mut io);
|
||||
assert_eq!(state.cache.len(), 1);
|
||||
|
||||
let now = now + config.message_cache_retention;
|
||||
state.handle(InEvent::TimerExpired(Timer::EvictCache), now, &mut io);
|
||||
assert_eq!(state.cache.len(), 0);
|
||||
}
|
||||
}
|
||||
1141
third_party/iroh-org/iroh-gossip/src/proto/sim.rs
vendored
Normal file
1141
third_party/iroh-org/iroh-gossip/src/proto/sim.rs
vendored
Normal file
File diff suppressed because it is too large
Load diff
381
third_party/iroh-org/iroh-gossip/src/proto/state.rs
vendored
Normal file
381
third_party/iroh-org/iroh-gossip/src/proto/state.rs
vendored
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
//! The protocol state of the `iroh-gossip` protocol.
|
||||
|
||||
use std::collections::{hash_map, HashMap, HashSet};
|
||||
|
||||
use n0_future::time::{Duration, Instant};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::trace;
|
||||
|
||||
use crate::{
|
||||
metrics::Metrics,
|
||||
proto::{
|
||||
topic::{self, Command},
|
||||
util::idbytes_impls,
|
||||
Config, PeerData, PeerIdentity, MIN_MAX_MESSAGE_SIZE,
|
||||
},
|
||||
};
|
||||
|
||||
/// The identifier for a topic
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Hash, Serialize, Ord, PartialOrd, Deserialize)]
|
||||
pub struct TopicId([u8; 32]);
|
||||
idbytes_impls!(TopicId, "TopicId");
|
||||
|
||||
impl TopicId {
|
||||
/// Convert to a hex string limited to the first 5 bytes for a friendly string
|
||||
/// representation of the key.
|
||||
pub fn fmt_short(&self) -> String {
|
||||
data_encoding::HEXLOWER.encode(&self.as_bytes()[..5])
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol wire message
|
||||
///
|
||||
/// This is the wire frame of the `iroh-gossip` protocol.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Message<PI> {
|
||||
pub(crate) topic: TopicId,
|
||||
pub(crate) message: topic::Message<PI>,
|
||||
}
|
||||
|
||||
impl<PI> Message<PI> {
|
||||
/// Get the kind of this message
|
||||
pub fn kind(&self) -> MessageKind {
|
||||
self.message.kind()
|
||||
}
|
||||
}
|
||||
|
||||
impl<PI: Serialize> Message<PI> {
|
||||
pub(crate) fn postcard_header_size() -> usize {
|
||||
// We create a message that has no payload (gossip::Message::Prune), calculate the encoded size,
|
||||
// and subtract 1 for the discriminator of the inner gossip::Message enum.
|
||||
let m = Self {
|
||||
topic: TopicId(Default::default()),
|
||||
message: topic::Message::<PI>::Gossip(super::plumtree::Message::Prune),
|
||||
};
|
||||
postcard::experimental::serialized_size(&m).unwrap() - 1
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this is a control or data message
|
||||
#[derive(Debug)]
|
||||
pub enum MessageKind {
|
||||
/// A data message.
|
||||
Data,
|
||||
/// A control message.
|
||||
Control,
|
||||
}
|
||||
|
||||
impl<PI: Serialize> Message<PI> {
|
||||
/// Get the encoded size of this message
|
||||
pub fn size(&self) -> postcard::Result<usize> {
|
||||
postcard::experimental::serialized_size(&self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer to be registered into the runtime
|
||||
///
|
||||
/// As the implementation of the protocol is an IO-less state machine, registering timers does not
|
||||
/// happen within the protocol implementation. Instead, these `Timer` structs are emitted as
|
||||
/// [`OutEvent`]s. The implementer must register the timer in its runtime to be emitted on the specified [`Instant`],
|
||||
/// and once triggered inject an [`InEvent::TimerExpired`] into the protocol state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Timer<PI> {
|
||||
topic: TopicId,
|
||||
timer: topic::Timer<PI>,
|
||||
}
|
||||
|
||||
/// Input event to the protocol state.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InEvent<PI> {
|
||||
/// Message received from the network.
|
||||
RecvMessage(PI, Message<PI>),
|
||||
/// Execute a command from the application.
|
||||
Command(TopicId, Command<PI>),
|
||||
/// Trigger a previously scheduled timer.
|
||||
TimerExpired(Timer<PI>),
|
||||
/// Peer disconnected on the network level.
|
||||
PeerDisconnected(PI),
|
||||
/// Update the opaque peer data about yourself.
|
||||
UpdatePeerData(PeerData),
|
||||
}
|
||||
|
||||
/// Output event from the protocol state.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OutEvent<PI> {
|
||||
/// Send a message on the network
|
||||
SendMessage(PI, Message<PI>),
|
||||
/// Emit an event to the application.
|
||||
EmitEvent(TopicId, topic::Event<PI>),
|
||||
/// Schedule a timer. The runtime is responsible for sending an [InEvent::TimerExpired]
|
||||
/// after the duration.
|
||||
ScheduleTimer(Duration, Timer<PI>),
|
||||
/// Close the connection to a peer on the network level.
|
||||
DisconnectPeer(PI),
|
||||
/// Updated peer data
|
||||
PeerData(PI, PeerData),
|
||||
}
|
||||
|
||||
type ConnsMap<PI> = HashMap<PI, HashSet<TopicId>>;
|
||||
type Outbox<PI> = Vec<OutEvent<PI>>;
|
||||
|
||||
enum InEventMapped<PI> {
|
||||
All(topic::InEvent<PI>),
|
||||
TopicEvent(TopicId, topic::InEvent<PI>),
|
||||
}
|
||||
|
||||
impl<PI> From<InEvent<PI>> for InEventMapped<PI> {
|
||||
fn from(event: InEvent<PI>) -> InEventMapped<PI> {
|
||||
match event {
|
||||
InEvent::RecvMessage(from, Message { topic, message }) => {
|
||||
Self::TopicEvent(topic, topic::InEvent::RecvMessage(from, message))
|
||||
}
|
||||
InEvent::Command(topic, command) => {
|
||||
Self::TopicEvent(topic, topic::InEvent::Command(command))
|
||||
}
|
||||
InEvent::TimerExpired(Timer { topic, timer }) => {
|
||||
Self::TopicEvent(topic, topic::InEvent::TimerExpired(timer))
|
||||
}
|
||||
InEvent::PeerDisconnected(peer) => Self::All(topic::InEvent::PeerDisconnected(peer)),
|
||||
InEvent::UpdatePeerData(data) => Self::All(topic::InEvent::UpdatePeerData(data)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the `iroh-gossip` protocol.
|
||||
///
|
||||
/// The implementation works as an IO-less state machine. The implementer injects events through
|
||||
/// [`Self::handle`], which returns an iterator of [`OutEvent`]s to be processed.
|
||||
///
|
||||
/// This struct contains a map of [`topic::State`] for each topic that was joined. It mostly acts as
|
||||
/// a forwarder of [`InEvent`]s to matching topic state. Each topic's state is completely
|
||||
/// independent; thus the actual protocol logic lives with [`topic::State`].
|
||||
#[derive(Debug)]
|
||||
pub struct State<PI, R> {
|
||||
me: PI,
|
||||
me_data: PeerData,
|
||||
config: Config,
|
||||
rng: R,
|
||||
states: HashMap<TopicId, topic::State<PI, R>>,
|
||||
outbox: Outbox<PI>,
|
||||
peer_topics: ConnsMap<PI>,
|
||||
}
|
||||
|
||||
impl<PI: PeerIdentity, R: Rng + Clone> State<PI, R> {
|
||||
/// Create a new protocol state instance.
|
||||
///
|
||||
/// `me` is the [`PeerIdentity`] of the local node, `peer_data` is the initial [`PeerData`]
|
||||
/// (which can be updated over time).
|
||||
/// For the protocol to perform as recommended in the papers, the [`Config`] should be
|
||||
/// identical for all nodes in the network.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if [`Config::max_message_size`] is below [`MIN_MAX_MESSAGE_SIZE`].
|
||||
pub fn new(me: PI, me_data: PeerData, config: Config, rng: R) -> Self {
|
||||
assert!(
|
||||
config.max_message_size >= MIN_MAX_MESSAGE_SIZE,
|
||||
"max_message_size must be at least {MIN_MAX_MESSAGE_SIZE}"
|
||||
);
|
||||
Self {
|
||||
me,
|
||||
me_data,
|
||||
config,
|
||||
rng,
|
||||
states: Default::default(),
|
||||
outbox: Default::default(),
|
||||
peer_topics: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the node's [`PeerIdentity`]
|
||||
pub fn me(&self) -> &PI {
|
||||
&self.me
|
||||
}
|
||||
|
||||
/// Get a reference to the protocol state for a topic.
|
||||
pub fn state(&self, topic: &TopicId) -> Option<&topic::State<PI, R>> {
|
||||
self.states.get(topic)
|
||||
}
|
||||
|
||||
/// Resets the tracked stats for a topic.
|
||||
pub fn reset_stats(&mut self, topic: &TopicId) {
|
||||
if let Some(state) = self.states.get_mut(topic) {
|
||||
state.reset_stats();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an iterator of all joined topics.
|
||||
pub fn topics(&self) -> impl Iterator<Item = &TopicId> {
|
||||
self.states.keys()
|
||||
}
|
||||
|
||||
/// Get an iterator for the states of all joined topics.
|
||||
pub fn states(&self) -> impl Iterator<Item = (&TopicId, &topic::State<PI, R>)> {
|
||||
self.states.iter()
|
||||
}
|
||||
|
||||
/// Check if a topic has any active (connected) peers.
|
||||
pub fn has_active_peers(&self, topic: &TopicId) -> bool {
|
||||
self.state(topic)
|
||||
.map(|s| s.has_active_peers())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns the maximum message size configured in the gossip protocol.
|
||||
pub fn max_message_size(&self) -> usize {
|
||||
self.config.max_message_size
|
||||
}
|
||||
|
||||
/// Handle an [`InEvent`]
|
||||
///
|
||||
/// This returns an iterator of [`OutEvent`]s that must be processed.
|
||||
pub fn handle(
|
||||
&mut self,
|
||||
event: InEvent<PI>,
|
||||
now: Instant,
|
||||
metrics: Option<&Metrics>,
|
||||
) -> impl Iterator<Item = OutEvent<PI>> + '_ + use<'_, PI, R> {
|
||||
trace!("in : {event:?}");
|
||||
if let Some(metrics) = &metrics {
|
||||
track_in_event(&event, metrics);
|
||||
}
|
||||
|
||||
let event: InEventMapped<PI> = event.into();
|
||||
|
||||
match event {
|
||||
InEventMapped::TopicEvent(topic, event) => {
|
||||
// when receiving a join command, initialize state if it doesn't exist
|
||||
if matches!(&event, topic::InEvent::Command(Command::Join(_peers))) {
|
||||
if let hash_map::Entry::Vacant(e) = self.states.entry(topic) {
|
||||
e.insert(topic::State::with_rng(
|
||||
self.me,
|
||||
Some(self.me_data.clone()),
|
||||
self.config.clone(),
|
||||
self.rng.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// when receiving a quit command, note this and drop the topic state after
|
||||
// processing this last event
|
||||
let quit = matches!(event, topic::InEvent::Command(Command::Quit));
|
||||
|
||||
// pass the event to the state handler
|
||||
if let Some(state) = self.states.get_mut(&topic) {
|
||||
// when receiving messages, update our conn map to take note that this topic state may want
|
||||
// to keep this connection
|
||||
if let topic::InEvent::RecvMessage(from, _message) = &event {
|
||||
self.peer_topics.entry(*from).or_default().insert(topic);
|
||||
}
|
||||
let out = state.handle(event, now);
|
||||
for event in out {
|
||||
handle_out_event(topic, event, &mut self.peer_topics, &mut self.outbox);
|
||||
}
|
||||
}
|
||||
|
||||
if quit {
|
||||
self.states.remove(&topic);
|
||||
}
|
||||
}
|
||||
// when a peer disconnected on the network level, forward event to all states
|
||||
InEventMapped::All(event) => {
|
||||
if let topic::InEvent::UpdatePeerData(data) = &event {
|
||||
self.me_data = data.clone();
|
||||
}
|
||||
for (topic, state) in self.states.iter_mut() {
|
||||
let out = state.handle(event.clone(), now);
|
||||
for event in out {
|
||||
handle_out_event(*topic, event, &mut self.peer_topics, &mut self.outbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// track metrics
|
||||
if let Some(metrics) = &metrics {
|
||||
track_out_events(&self.outbox, metrics);
|
||||
}
|
||||
|
||||
self.outbox.drain(..)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_out_event<PI: PeerIdentity>(
|
||||
topic: TopicId,
|
||||
event: topic::OutEvent<PI>,
|
||||
conns: &mut ConnsMap<PI>,
|
||||
outbox: &mut Outbox<PI>,
|
||||
) {
|
||||
trace!("out: {event:?}");
|
||||
match event {
|
||||
topic::OutEvent::SendMessage(to, message) => {
|
||||
outbox.push(OutEvent::SendMessage(to, Message { topic, message }))
|
||||
}
|
||||
topic::OutEvent::EmitEvent(event) => outbox.push(OutEvent::EmitEvent(topic, event)),
|
||||
topic::OutEvent::ScheduleTimer(delay, timer) => {
|
||||
outbox.push(OutEvent::ScheduleTimer(delay, Timer { topic, timer }))
|
||||
}
|
||||
topic::OutEvent::DisconnectPeer(peer) => {
|
||||
let empty = conns
|
||||
.get_mut(&peer)
|
||||
.map(|list| list.remove(&topic) || list.is_empty())
|
||||
.unwrap_or(false);
|
||||
if empty {
|
||||
conns.remove(&peer);
|
||||
outbox.push(OutEvent::DisconnectPeer(peer));
|
||||
}
|
||||
}
|
||||
topic::OutEvent::PeerData(peer, data) => outbox.push(OutEvent::PeerData(peer, data)),
|
||||
}
|
||||
}
|
||||
|
||||
fn track_out_events<PI: Serialize>(events: &[OutEvent<PI>], metrics: &Metrics) {
|
||||
for event in events {
|
||||
match event {
|
||||
OutEvent::SendMessage(_to, message) => match message.kind() {
|
||||
MessageKind::Data => {
|
||||
metrics.msgs_data_sent.inc();
|
||||
metrics
|
||||
.msgs_data_sent_size
|
||||
.inc_by(message.size().unwrap_or(0) as u64);
|
||||
}
|
||||
MessageKind::Control => {
|
||||
metrics.msgs_ctrl_sent.inc();
|
||||
metrics
|
||||
.msgs_ctrl_sent_size
|
||||
.inc_by(message.size().unwrap_or(0) as u64);
|
||||
}
|
||||
},
|
||||
OutEvent::EmitEvent(_topic, event) => match event {
|
||||
super::Event::NeighborUp(_peer) => {
|
||||
metrics.neighbor_up.inc();
|
||||
}
|
||||
super::Event::NeighborDown(_peer) => {
|
||||
metrics.neighbor_down.inc();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn track_in_event<PI: Serialize>(event: &InEvent<PI>, metrics: &Metrics) {
|
||||
if let InEvent::RecvMessage(_from, message) = event {
|
||||
match message.kind() {
|
||||
MessageKind::Data => {
|
||||
metrics.msgs_data_recv.inc();
|
||||
metrics
|
||||
.msgs_data_recv_size
|
||||
.inc_by(message.size().unwrap_or(0) as u64);
|
||||
}
|
||||
MessageKind::Control => {
|
||||
metrics.msgs_ctrl_recv.inc();
|
||||
metrics
|
||||
.msgs_ctrl_recv_size
|
||||
.inc_by(message.size().unwrap_or(0) as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
363
third_party/iroh-org/iroh-gossip/src/proto/topic.rs
vendored
Normal file
363
third_party/iroh-org/iroh-gossip/src/proto/topic.rs
vendored
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
//! This module contains the implementation of the gossiping protocol for an individual topic
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use bytes::Bytes;
|
||||
use derive_more::From;
|
||||
use n0_future::time::{Duration, Instant};
|
||||
use rand::Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{
|
||||
hyparview::{self, InEvent as SwarmIn},
|
||||
plumtree::{self, GossipEvent, InEvent as GossipIn, Scope},
|
||||
state::MessageKind,
|
||||
PeerData, PeerIdentity, DEFAULT_MAX_MESSAGE_SIZE,
|
||||
};
|
||||
use crate::proto::MIN_MAX_MESSAGE_SIZE;
|
||||
|
||||
/// Input event to the topic state handler.
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum InEvent<PI> {
|
||||
/// Message received from the network.
|
||||
RecvMessage(PI, Message<PI>),
|
||||
/// Execute a command from the application.
|
||||
Command(Command<PI>),
|
||||
/// Trigger a previously scheduled timer.
|
||||
TimerExpired(Timer<PI>),
|
||||
/// Peer disconnected on the network level.
|
||||
PeerDisconnected(PI),
|
||||
/// Update the opaque peer data about yourself.
|
||||
UpdatePeerData(PeerData),
|
||||
}
|
||||
|
||||
/// An output event from the state handler.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum OutEvent<PI> {
|
||||
/// Send a message on the network
|
||||
SendMessage(PI, Message<PI>),
|
||||
/// Emit an event to the application.
|
||||
EmitEvent(Event<PI>),
|
||||
/// Schedule a timer. The runtime is responsible for sending an [InEvent::TimerExpired]
|
||||
/// after the duration.
|
||||
ScheduleTimer(Duration, Timer<PI>),
|
||||
/// Close the connection to a peer on the network level.
|
||||
DisconnectPeer(PI),
|
||||
/// Emitted when new [`PeerData`] was received for a peer.
|
||||
PeerData(PI, PeerData),
|
||||
}
|
||||
|
||||
impl<PI> From<hyparview::OutEvent<PI>> for OutEvent<PI> {
|
||||
fn from(event: hyparview::OutEvent<PI>) -> Self {
|
||||
use hyparview::OutEvent::*;
|
||||
match event {
|
||||
SendMessage(to, message) => Self::SendMessage(to, message.into()),
|
||||
ScheduleTimer(delay, timer) => Self::ScheduleTimer(delay, timer.into()),
|
||||
DisconnectPeer(peer) => Self::DisconnectPeer(peer),
|
||||
EmitEvent(event) => Self::EmitEvent(event.into()),
|
||||
PeerData(peer, data) => Self::PeerData(peer, data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<PI> From<plumtree::OutEvent<PI>> for OutEvent<PI> {
|
||||
fn from(event: plumtree::OutEvent<PI>) -> Self {
|
||||
use plumtree::OutEvent::*;
|
||||
match event {
|
||||
SendMessage(to, message) => Self::SendMessage(to, message.into()),
|
||||
ScheduleTimer(delay, timer) => Self::ScheduleTimer(delay, timer.into()),
|
||||
EmitEvent(event) => Self::EmitEvent(event.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for a concrete type to push `OutEvent`s to.
|
||||
///
|
||||
/// The implementation is generic over this trait, which allows the upper layer to supply a
|
||||
/// container of their choice for `OutEvent`s emitted from the protocol state.
|
||||
pub trait IO<PI: Clone> {
|
||||
/// Push an event in the IO container
|
||||
fn push(&mut self, event: impl Into<OutEvent<PI>>);
|
||||
|
||||
/// Push all events from an iterator into the IO container
|
||||
fn push_from_iter(&mut self, iter: impl IntoIterator<Item = impl Into<OutEvent<PI>>>) {
|
||||
for event in iter.into_iter() {
|
||||
self.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol message for a particular topic
|
||||
#[derive(From, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub enum Message<PI> {
|
||||
/// A message of the swarm membership layer
|
||||
Swarm(hyparview::Message<PI>),
|
||||
/// A message of the gossip broadcast layer
|
||||
Gossip(plumtree::Message),
|
||||
}
|
||||
|
||||
impl<PI> Message<PI> {
|
||||
/// Get the kind of this message
|
||||
pub fn kind(&self) -> MessageKind {
|
||||
match self {
|
||||
Message::Swarm(_) => MessageKind::Control,
|
||||
Message::Gossip(message) => match message {
|
||||
plumtree::Message::Gossip(_) => MessageKind::Data,
|
||||
_ => MessageKind::Control,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a disconnect message (which is the last message sent to a peer per topic).
|
||||
pub fn is_disconnect(&self) -> bool {
|
||||
matches!(self, Message::Swarm(hyparview::Message::Disconnect(_)))
|
||||
}
|
||||
}
|
||||
|
||||
/// An event to be emitted to the application for a particular topic.
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
|
||||
pub enum Event<PI> {
|
||||
/// We have a new, direct neighbor in the swarm membership layer for this topic
|
||||
NeighborUp(PI),
|
||||
/// We dropped direct neighbor in the swarm membership layer for this topic
|
||||
NeighborDown(PI),
|
||||
/// A gossip message was received for this topic
|
||||
Received(GossipEvent<PI>),
|
||||
}
|
||||
|
||||
impl<PI> From<hyparview::Event<PI>> for Event<PI> {
|
||||
fn from(value: hyparview::Event<PI>) -> Self {
|
||||
match value {
|
||||
hyparview::Event::NeighborUp(peer) => Self::NeighborUp(peer),
|
||||
hyparview::Event::NeighborDown(peer) => Self::NeighborDown(peer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<PI> From<plumtree::Event<PI>> for Event<PI> {
|
||||
fn from(value: plumtree::Event<PI>) -> Self {
|
||||
match value {
|
||||
plumtree::Event::Received(event) => Self::Received(event),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A timer to be registered for a particular topic.
|
||||
///
|
||||
/// This should be treated as an opaque value by the implementer and, once emitted, simply returned
|
||||
/// to the protocol through [`InEvent::TimerExpired`].
|
||||
#[derive(Clone, From, Debug, PartialEq, Eq)]
|
||||
pub enum Timer<PI> {
|
||||
/// A timer for the swarm layer
|
||||
Swarm(hyparview::Timer<PI>),
|
||||
/// A timer for the gossip layer
|
||||
Gossip(plumtree::Timer),
|
||||
}
|
||||
|
||||
/// A command to the protocol state for a particular topic.
|
||||
#[derive(Clone, derive_more::Debug)]
|
||||
pub enum Command<PI> {
|
||||
/// Join this topic and connect to peers.
|
||||
///
|
||||
/// If the list of peers is empty, will prepare the state and accept incoming join requests,
|
||||
/// but only become operational after the first join request by another peer.
|
||||
Join(Vec<PI>),
|
||||
/// Broadcast a message for this topic.
|
||||
Broadcast(#[debug("<{}b>", _0.len())] Bytes, Scope),
|
||||
/// Leave this topic and drop all state.
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl<PI: Clone> IO<PI> for VecDeque<OutEvent<PI>> {
|
||||
fn push(&mut self, event: impl Into<OutEvent<PI>>) {
|
||||
self.push_back(event.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol configuration
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
/// Configuration for the swarm membership layer
|
||||
pub membership: hyparview::Config,
|
||||
/// Configuration for the gossip broadcast layer
|
||||
pub broadcast: plumtree::Config,
|
||||
/// Max message size in bytes.
|
||||
///
|
||||
/// This size should be the same across a network to ensure all nodes can transmit and read large messages.
|
||||
///
|
||||
/// At minimum, this size should be large enough to send gossip control messages. This can vary, depending on the size of the [`PeerIdentity`] you use and the size of the [`PeerData`] you transmit in your messages.
|
||||
///
|
||||
/// The default is [`DEFAULT_MAX_MESSAGE_SIZE`].
|
||||
pub max_message_size: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
membership: Default::default(),
|
||||
broadcast: Default::default(),
|
||||
max_message_size: DEFAULT_MAX_MESSAGE_SIZE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The topic state maintains the swarm membership and broadcast tree for a particular topic.
|
||||
#[derive(Debug)]
|
||||
pub struct State<PI, R> {
|
||||
me: PI,
|
||||
pub(crate) swarm: hyparview::State<PI, R>,
|
||||
pub(crate) gossip: plumtree::State<PI>,
|
||||
outbox: VecDeque<OutEvent<PI>>,
|
||||
stats: Stats,
|
||||
}
|
||||
|
||||
impl<PI: PeerIdentity> State<PI, rand::rngs::ThreadRng> {
|
||||
/// Initialize the local state with the default random number generator.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if [`Config::max_message_size`] is below [`MIN_MAX_MESSAGE_SIZE`].
|
||||
pub fn new(me: PI, me_data: Option<PeerData>, config: Config) -> Self {
|
||||
Self::with_rng(me, me_data, config, rand::rng())
|
||||
}
|
||||
}
|
||||
|
||||
impl<PI, R> State<PI, R> {
|
||||
/// The address of your local endpoint.
|
||||
pub fn endpoint(&self) -> &PI {
|
||||
&self.me
|
||||
}
|
||||
}
|
||||
|
||||
impl<PI: PeerIdentity, R: Rng> State<PI, R> {
|
||||
/// Initialize the local state with a custom random number generator.
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if [`Config::max_message_size`] is below [`MIN_MAX_MESSAGE_SIZE`].
|
||||
pub fn with_rng(me: PI, me_data: Option<PeerData>, config: Config, rng: R) -> Self {
|
||||
assert!(
|
||||
config.max_message_size >= MIN_MAX_MESSAGE_SIZE,
|
||||
"max_message_size must be at least {MIN_MAX_MESSAGE_SIZE}"
|
||||
);
|
||||
let max_payload_size =
|
||||
config.max_message_size - super::Message::<PI>::postcard_header_size();
|
||||
Self {
|
||||
swarm: hyparview::State::new(me, me_data, config.membership, rng),
|
||||
gossip: plumtree::State::new(me, config.broadcast, max_payload_size),
|
||||
me,
|
||||
outbox: VecDeque::new(),
|
||||
stats: Stats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an incoming event.
|
||||
///
|
||||
/// Returns an iterator of outgoing events that must be processed by the application.
|
||||
pub fn handle(
|
||||
&mut self,
|
||||
event: InEvent<PI>,
|
||||
now: Instant,
|
||||
) -> impl Iterator<Item = OutEvent<PI>> + '_ {
|
||||
let io = &mut self.outbox;
|
||||
// Process the event, store out events in outbox.
|
||||
match event {
|
||||
InEvent::Command(command) => match command {
|
||||
Command::Join(peers) => {
|
||||
for peer in peers {
|
||||
self.swarm.handle(SwarmIn::RequestJoin(peer), io);
|
||||
}
|
||||
}
|
||||
Command::Broadcast(data, scope) => {
|
||||
self.gossip
|
||||
.handle(GossipIn::Broadcast(data, scope), now, io)
|
||||
}
|
||||
Command::Quit => self.swarm.handle(SwarmIn::Quit, io),
|
||||
},
|
||||
InEvent::RecvMessage(from, message) => {
|
||||
self.stats.messages_received += 1;
|
||||
match message {
|
||||
Message::Swarm(message) => {
|
||||
self.swarm.handle(SwarmIn::RecvMessage(from, message), io)
|
||||
}
|
||||
Message::Gossip(message) => {
|
||||
self.gossip
|
||||
.handle(GossipIn::RecvMessage(from, message), now, io)
|
||||
}
|
||||
}
|
||||
}
|
||||
InEvent::TimerExpired(timer) => match timer {
|
||||
Timer::Swarm(timer) => self.swarm.handle(SwarmIn::TimerExpired(timer), io),
|
||||
Timer::Gossip(timer) => self.gossip.handle(GossipIn::TimerExpired(timer), now, io),
|
||||
},
|
||||
InEvent::PeerDisconnected(peer) => {
|
||||
self.swarm.handle(SwarmIn::PeerDisconnected(peer), io);
|
||||
self.gossip.handle(GossipIn::NeighborDown(peer), now, io);
|
||||
}
|
||||
InEvent::UpdatePeerData(data) => self.swarm.handle(SwarmIn::UpdatePeerData(data), io),
|
||||
}
|
||||
|
||||
// Forward NeighborUp and NeighborDown events from hyparview to plumtree
|
||||
let mut io = VecDeque::new();
|
||||
for event in self.outbox.iter() {
|
||||
match event {
|
||||
OutEvent::EmitEvent(Event::NeighborUp(peer)) => {
|
||||
self.gossip
|
||||
.handle(GossipIn::NeighborUp(*peer), now, &mut io)
|
||||
}
|
||||
OutEvent::EmitEvent(Event::NeighborDown(peer)) => {
|
||||
self.gossip
|
||||
.handle(GossipIn::NeighborDown(*peer), now, &mut io)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Note that this is a no-op because plumtree::handle(NeighborUp | NeighborDown)
|
||||
// above does not emit any OutEvents.
|
||||
self.outbox.extend(io.drain(..));
|
||||
|
||||
// Update sent message counter
|
||||
self.stats.messages_sent += self
|
||||
.outbox
|
||||
.iter()
|
||||
.filter(|event| matches!(event, OutEvent::SendMessage(_, _)))
|
||||
.count();
|
||||
|
||||
self.outbox.drain(..)
|
||||
}
|
||||
|
||||
/// Get stats on how many messages were sent and received.
|
||||
// TODO: Remove/replace with metrics?
|
||||
pub fn stats(&self) -> &Stats {
|
||||
&self.stats
|
||||
}
|
||||
|
||||
/// Reset all statistics.
|
||||
pub fn reset_stats(&mut self) {
|
||||
self.gossip.stats = Default::default();
|
||||
self.swarm.stats = Default::default();
|
||||
self.stats = Default::default();
|
||||
}
|
||||
|
||||
/// Get statistics for the gossip broadcast state
|
||||
///
|
||||
/// TODO: Remove/replace with metrics?
|
||||
pub fn gossip_stats(&self) -> &plumtree::Stats {
|
||||
self.gossip.stats()
|
||||
}
|
||||
|
||||
/// Check if this topic has any active (connected) peers.
|
||||
pub fn has_active_peers(&self) -> bool {
|
||||
!self.swarm.active_view.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Statistics for the protocol state of a topic
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Stats {
|
||||
/// Number of messages sent
|
||||
pub messages_sent: usize,
|
||||
/// Number of messages received
|
||||
pub messages_received: usize,
|
||||
}
|
||||
532
third_party/iroh-org/iroh-gossip/src/proto/util.rs
vendored
Normal file
532
third_party/iroh-org/iroh-gossip/src/proto/util.rs
vendored
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
//! Utilities used in the protocol implementation
|
||||
|
||||
use std::{
|
||||
collections::{hash_map, BinaryHeap, HashMap},
|
||||
hash::Hash,
|
||||
};
|
||||
|
||||
use n0_future::time::Instant;
|
||||
use rand::{
|
||||
seq::{IteratorRandom, SliceRandom},
|
||||
Rng,
|
||||
};
|
||||
|
||||
/// Implement methods, display, debug and conversion traits for 32 byte identifiers.
|
||||
macro_rules! idbytes_impls {
|
||||
($ty:ty, $name:expr) => {
|
||||
impl $ty {
|
||||
/// Create from a byte array.
|
||||
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
/// Get as byte slice.
|
||||
pub fn as_bytes(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ::std::convert::Into<[u8; 32]>> ::std::convert::From<T> for $ty {
|
||||
fn from(value: T) -> Self {
|
||||
Self::from_bytes(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::fmt::Display for $ty {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||
write!(f, "{}", ::hex::encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::fmt::Debug for $ty {
|
||||
fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
|
||||
write!(f, "{}({})", $name, ::hex::encode(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::str::FromStr for $ty {
|
||||
type Err = ::hex::FromHexError;
|
||||
fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
|
||||
let mut bytes = [0u8; 32];
|
||||
::hex::decode_to_slice(s, &mut bytes)?;
|
||||
Ok(Self::from_bytes(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::convert::AsRef<[u8]> for $ty {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::convert::AsRef<[u8; 32]> for $ty {
|
||||
fn as_ref(&self) -> &[u8; 32] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use idbytes_impls;
|
||||
|
||||
/// A hash set where the iteration order of the values is independent of their
|
||||
/// hash values.
|
||||
///
|
||||
/// This is wrapper around [indexmap::IndexSet] which couple of utility methods
|
||||
/// to randomly select elements from the set.
|
||||
#[derive(Default, Debug, Clone, derive_more::Deref)]
|
||||
pub(crate) struct IndexSet<T> {
|
||||
inner: indexmap::IndexSet<T>,
|
||||
}
|
||||
|
||||
impl<T: Hash + Eq> PartialEq for IndexSet<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Hash + Eq + PartialEq> IndexSet<T> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: indexmap::IndexSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, value: T) -> bool {
|
||||
self.inner.insert(value)
|
||||
}
|
||||
|
||||
/// Remove a random element from the set.
|
||||
pub fn remove_random<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Option<T> {
|
||||
self.pick_random_index(rng)
|
||||
.and_then(|idx| self.inner.shift_remove_index(idx))
|
||||
}
|
||||
|
||||
/// Pick a random element from the set.
|
||||
pub fn pick_random<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<&T> {
|
||||
self.pick_random_index(rng)
|
||||
.and_then(|idx| self.inner.get_index(idx))
|
||||
}
|
||||
|
||||
/// Pick a random element from the set, but not any of the elements in `without`.
|
||||
pub fn pick_random_without<R: Rng + ?Sized>(&self, without: &[&T], rng: &mut R) -> Option<&T> {
|
||||
self.iter().filter(|x| !without.contains(x)).choose(rng)
|
||||
}
|
||||
|
||||
/// Pick a random index for an element in the set.
|
||||
pub fn pick_random_index<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<usize> {
|
||||
if self.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(rng.random_range(0..self.inner.len()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove an element from the set.
|
||||
///
|
||||
/// NOTE: the value is removed by swapping it with the last element of the set and popping it off.
|
||||
/// **This modifies the order of element by moving the last element**
|
||||
pub fn remove(&mut self, value: &T) -> Option<T> {
|
||||
self.inner.swap_remove_full(value).map(|(_i, v)| v)
|
||||
}
|
||||
|
||||
/// Remove an element from the set by its index.
|
||||
///
|
||||
/// NOTE: the value is removed by swapping it with the last element of the set and popping it off.
|
||||
/// **This modifies the order of element by moving the last element**
|
||||
pub fn remove_index(&mut self, index: usize) -> Option<T> {
|
||||
self.inner.swap_remove_index(index)
|
||||
}
|
||||
|
||||
/// Create an iterator over the set in the order of insertion, while skipping the element in
|
||||
/// `without`.
|
||||
pub fn iter_without<'a>(&'a self, value: &'a T) -> impl Iterator<Item = &'a T> {
|
||||
self.iter().filter(move |x| *x != value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IndexSet<T>
|
||||
where
|
||||
T: Hash + Eq + Clone,
|
||||
{
|
||||
/// Create a vector of all elements in the set in random order.
|
||||
pub fn shuffled<R: Rng + ?Sized>(&self, rng: &mut R) -> Vec<T> {
|
||||
let mut items: Vec<_> = self.inner.iter().cloned().collect();
|
||||
items.shuffle(rng);
|
||||
items
|
||||
}
|
||||
|
||||
/// Create a vector of all elements in the set in random order, and shorten to
|
||||
/// the first `len` elements after shuffling.
|
||||
pub fn shuffled_and_capped<R: Rng + ?Sized>(&self, len: usize, rng: &mut R) -> Vec<T> {
|
||||
let mut items = self.shuffled(rng);
|
||||
items.truncate(len);
|
||||
items
|
||||
}
|
||||
|
||||
/// Create a vector of the elements in the set in random order while omitting
|
||||
/// the elements in `without`.
|
||||
pub fn shuffled_without<R: Rng + ?Sized>(&self, without: &[&T], rng: &mut R) -> Vec<T> {
|
||||
let mut items = self
|
||||
.inner
|
||||
.iter()
|
||||
.filter(|x| !without.contains(x))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
items.shuffle(rng);
|
||||
items
|
||||
}
|
||||
|
||||
/// Create a vector of the elements in the set in random order while omitting
|
||||
/// the elements in `without`, and shorten to the first `len` elements.
|
||||
pub fn shuffled_without_and_capped<R: Rng + ?Sized>(
|
||||
&self,
|
||||
without: &[&T],
|
||||
len: usize,
|
||||
rng: &mut R,
|
||||
) -> Vec<T> {
|
||||
let mut items = self.shuffled_without(without, rng);
|
||||
items.truncate(len);
|
||||
items
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> IntoIterator for IndexSet<T> {
|
||||
type Item = T;
|
||||
type IntoIter = <indexmap::IndexSet<T> as IntoIterator>::IntoIter;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromIterator<T> for IndexSet<T>
|
||||
where
|
||||
T: Hash + Eq,
|
||||
{
|
||||
fn from_iter<I: IntoIterator<Item = T>>(iterable: I) -> Self {
|
||||
IndexSet {
|
||||
inner: indexmap::IndexSet::from_iter(iterable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`BinaryHeap`] with entries sorted by [`Instant`]. Allows to process expired items.
|
||||
#[derive(Debug)]
|
||||
pub struct TimerMap<T> {
|
||||
heap: BinaryHeap<TimerMapEntry<T>>,
|
||||
seq: u64,
|
||||
}
|
||||
|
||||
// Can't derive default because we don't want a `T: Default` bound.
|
||||
impl<T> Default for TimerMap<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
heap: Default::default(),
|
||||
seq: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TimerMap<T> {
|
||||
/// Create a new, empty TimerMap.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Insert a new entry at the specified instant.
|
||||
pub fn insert(&mut self, instant: Instant, item: T) {
|
||||
let seq = self.seq;
|
||||
self.seq += 1;
|
||||
let entry = TimerMapEntry {
|
||||
seq,
|
||||
time: instant,
|
||||
item,
|
||||
};
|
||||
self.heap.push(entry);
|
||||
}
|
||||
|
||||
/// Remove and return all entries before and equal to `from`.
|
||||
pub fn drain_until(
|
||||
&mut self,
|
||||
from: &Instant,
|
||||
) -> impl Iterator<Item = (Instant, T)> + '_ + use<'_, T> {
|
||||
let from = *from;
|
||||
std::iter::from_fn(move || self.pop_before(from))
|
||||
}
|
||||
|
||||
/// Pop the first entry, if equal or before `limit`.
|
||||
pub fn pop_before(&mut self, limit: Instant) -> Option<(Instant, T)> {
|
||||
match self.heap.peek() {
|
||||
Some(item) if item.time <= limit => self.heap.pop().map(|item| (item.time, item.item)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the earliest entry in the `TimerMap`.
|
||||
pub fn first(&self) -> Option<&Instant> {
|
||||
self.heap.peek().map(|x| &x.time)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn to_vec(&self) -> Vec<(Instant, T)>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
self.heap
|
||||
.clone()
|
||||
.into_sorted_vec()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|x| (x.time, x.item))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TimerMapEntry<T> {
|
||||
time: Instant,
|
||||
seq: u64,
|
||||
item: T,
|
||||
}
|
||||
|
||||
impl<T> PartialEq for TimerMapEntry<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.time == other.time && self.seq == other.seq
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Eq for TimerMapEntry<T> {}
|
||||
|
||||
impl<T> PartialOrd for TimerMapEntry<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Ord for TimerMapEntry<T> {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.time
|
||||
.cmp(&other.time)
|
||||
.reverse()
|
||||
.then_with(|| self.seq.cmp(&other.seq).reverse())
|
||||
}
|
||||
}
|
||||
|
||||
/// A hash map where entries expire after a time
|
||||
#[derive(Debug)]
|
||||
pub struct TimeBoundCache<K, V> {
|
||||
map: HashMap<K, (Instant, V)>,
|
||||
expiry: TimerMap<K>,
|
||||
}
|
||||
|
||||
impl<K, V> Default for TimeBoundCache<K, V> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: Default::default(),
|
||||
expiry: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Hash + Eq + Clone, V> TimeBoundCache<K, V> {
|
||||
/// Insert an item into the cache, marked with an expiration time.
|
||||
pub fn insert(&mut self, key: K, value: V, expires: Instant) {
|
||||
self.map.insert(key.clone(), (expires, value));
|
||||
self.expiry.insert(expires, key);
|
||||
}
|
||||
|
||||
/// Returns `true` if the map contains a value for the specified key.
|
||||
pub fn contains_key(&self, key: &K) -> bool {
|
||||
self.map.contains_key(key)
|
||||
}
|
||||
|
||||
/// Get the number of entries in the cache.
|
||||
pub fn len(&self) -> usize {
|
||||
self.map.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if the map contains no elements.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.map.is_empty()
|
||||
}
|
||||
|
||||
/// Get an item from the cache.
|
||||
pub fn get(&self, key: &K) -> Option<&V> {
|
||||
self.map.get(key).map(|(_expires, value)| value)
|
||||
}
|
||||
|
||||
/// Get the expiration time for an item.
|
||||
pub fn expires(&self, key: &K) -> Option<&Instant> {
|
||||
self.map.get(key).map(|(expires, _value)| expires)
|
||||
}
|
||||
|
||||
/// Iterate over all items in the cache.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&K, &V, &Instant)> {
|
||||
self.map.iter().map(|(k, (expires, v))| (k, v, expires))
|
||||
}
|
||||
|
||||
/// Remove all entries with an expiry instant lower or equal to `instant`.
|
||||
///
|
||||
/// Returns the number of items that were removed.
|
||||
pub fn expire_until(&mut self, instant: Instant) -> usize {
|
||||
let drain = self.expiry.drain_until(&instant);
|
||||
let mut count = 0;
|
||||
for (time, key) in drain {
|
||||
match self.map.entry(key) {
|
||||
hash_map::Entry::Occupied(entry) if entry.get().0 == time => {
|
||||
// If the entry's time matches that of the item we are draining from the expiry list,
|
||||
// remove the entry from the map and increase the count of items we removed.
|
||||
entry.remove();
|
||||
count += 1;
|
||||
}
|
||||
hash_map::Entry::Occupied(_entry) => {
|
||||
// If the entry's time does not match the time of the item we are draining,
|
||||
// do not remove the entry: It means that it was re-added with a later time.
|
||||
}
|
||||
hash_map::Entry::Vacant(_) => {
|
||||
// If the entry is not in the map, it means that it was already removed,
|
||||
// which can happen if it was inserted multiple times.
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use n0_future::time::{Duration, Instant};
|
||||
use rand::SeedableRng;
|
||||
|
||||
use super::{IndexSet, TimeBoundCache, TimerMap};
|
||||
|
||||
fn test_rng() -> rand_chacha::ChaCha12Rng {
|
||||
rand_chacha::ChaCha12Rng::seed_from_u64(42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexset() {
|
||||
let elems = [1, 2, 3, 4];
|
||||
let set = IndexSet::from_iter(elems);
|
||||
let x = set.shuffled(&mut test_rng());
|
||||
assert_eq!(x, vec![2, 1, 4, 3]);
|
||||
let x = set.shuffled_and_capped(2, &mut test_rng());
|
||||
assert_eq!(x, vec![2, 1]);
|
||||
let x = set.shuffled_without(&[&1], &mut test_rng());
|
||||
assert_eq!(x, vec![3, 2, 4]);
|
||||
let x = set.shuffled_without_and_capped(&[&1], 2, &mut test_rng());
|
||||
assert_eq!(x, vec![3, 2]);
|
||||
|
||||
// recreate the rng - otherwise we get failures on some architectures when cross-compiling,
|
||||
// likely due to usize differences pulling different amounts of randomness.
|
||||
let x = set.pick_random(&mut test_rng());
|
||||
assert_eq!(x, Some(&1));
|
||||
let x = set.pick_random_without(&[&3], &mut test_rng());
|
||||
assert_eq!(x, Some(&4));
|
||||
|
||||
let mut set = set;
|
||||
set.remove_random(&mut test_rng());
|
||||
assert_eq!(set, IndexSet::from_iter([2, 3, 4]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_map() {
|
||||
let mut map = TimerMap::new();
|
||||
let now = Instant::now();
|
||||
|
||||
let times = [
|
||||
now - Duration::from_secs(1),
|
||||
now,
|
||||
now + Duration::from_secs(1),
|
||||
now + Duration::from_secs(2),
|
||||
];
|
||||
map.insert(times[0], -1);
|
||||
map.insert(times[0], -2);
|
||||
map.insert(times[1], 0);
|
||||
map.insert(times[2], 1);
|
||||
map.insert(times[3], 2);
|
||||
map.insert(times[3], 3);
|
||||
|
||||
assert_eq!(
|
||||
map.to_vec(),
|
||||
vec![
|
||||
(times[0], -1),
|
||||
(times[0], -2),
|
||||
(times[1], 0),
|
||||
(times[2], 1),
|
||||
(times[3], 2),
|
||||
(times[3], 3)
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(map.first(), Some(×[0]));
|
||||
|
||||
let drain = map.drain_until(&now);
|
||||
assert_eq!(
|
||||
drain.collect::<Vec<_>>(),
|
||||
vec![(times[0], -1), (times[0], -2), (times[1], 0),]
|
||||
);
|
||||
assert_eq!(
|
||||
map.to_vec(),
|
||||
vec![(times[2], 1), (times[3], 2), (times[3], 3)]
|
||||
);
|
||||
let drain = map.drain_until(&now);
|
||||
assert_eq!(drain.collect::<Vec<_>>(), vec![]);
|
||||
let drain = map.drain_until(&(now + Duration::from_secs(10)));
|
||||
assert_eq!(
|
||||
drain.collect::<Vec<_>>(),
|
||||
vec![(times[2], 1), (times[3], 2), (times[3], 3)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex() {
|
||||
#[derive(Eq, PartialEq)]
|
||||
struct Id([u8; 32]);
|
||||
idbytes_impls!(Id, "Id");
|
||||
let id: Id = [1u8; 32].into();
|
||||
assert_eq!(id, Id::from_str(&format!("{id}")).unwrap());
|
||||
assert_eq!(
|
||||
&format!("{id}"),
|
||||
"0101010101010101010101010101010101010101010101010101010101010101"
|
||||
);
|
||||
assert_eq!(
|
||||
&format!("{id:?}"),
|
||||
"Id(0101010101010101010101010101010101010101010101010101010101010101)"
|
||||
);
|
||||
assert_eq!(id.as_bytes(), &[1u8; 32]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_bound_cache() {
|
||||
let mut cache = TimeBoundCache::default();
|
||||
|
||||
let t0 = Instant::now();
|
||||
let t1 = t0 + Duration::from_secs(1);
|
||||
let t2 = t0 + Duration::from_secs(2);
|
||||
|
||||
cache.insert(1, 10, t0);
|
||||
cache.insert(2, 20, t1);
|
||||
cache.insert(3, 30, t1);
|
||||
cache.insert(4, 40, t2);
|
||||
|
||||
assert_eq!(cache.get(&2), Some(&20));
|
||||
assert_eq!(cache.len(), 4);
|
||||
let removed = cache.expire_until(t1);
|
||||
assert_eq!(removed, 3);
|
||||
assert_eq!(cache.len(), 1);
|
||||
assert_eq!(cache.get(&2), None);
|
||||
assert_eq!(cache.get(&4), Some(&40));
|
||||
|
||||
let t3 = t2 + Duration::from_secs(1);
|
||||
cache.insert(5, 50, t2);
|
||||
assert_eq!(cache.expires(&5), Some(&t2));
|
||||
cache.insert(5, 50, t3);
|
||||
assert_eq!(cache.expires(&5), Some(&t3));
|
||||
cache.expire_until(t2);
|
||||
assert_eq!(cache.get(&4), None);
|
||||
assert_eq!(cache.get(&5), Some(&50));
|
||||
}
|
||||
}
|
||||
134
third_party/iroh-org/iroh-gossip/tests/sim.rs
vendored
Normal file
134
third_party/iroh-org/iroh-gossip/tests/sim.rs
vendored
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//! Tests that use the [`iroh_gossip::proto::sim::Simulator`].
|
||||
|
||||
use std::{env, fmt, str::FromStr, time::Duration};
|
||||
|
||||
use iroh_gossip::proto::{
|
||||
sim::{BootstrapMode, LatencyConfig, NetworkConfig, Simulator, SimulatorConfig},
|
||||
Config,
|
||||
};
|
||||
|
||||
#[test]
|
||||
// #[traced_test]
|
||||
fn big_hyparview() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let mut proto = Config::default();
|
||||
proto.membership.shuffle_interval = Duration::from_secs(5);
|
||||
let config = SimulatorConfig::from_env();
|
||||
let bootstrap = BootstrapMode::default();
|
||||
let network_config = NetworkConfig {
|
||||
proto,
|
||||
latency: LatencyConfig::default(),
|
||||
};
|
||||
let mut simulator = Simulator::new(config, network_config);
|
||||
simulator.bootstrap(bootstrap);
|
||||
let state = simulator.report();
|
||||
println!("{state}");
|
||||
assert!(!state.has_peers_with_no_neighbors());
|
||||
}
|
||||
|
||||
#[test]
|
||||
// #[traced_test]
|
||||
fn big_multiple_sender() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
|
||||
let network_config = NetworkConfig::default();
|
||||
let config = SimulatorConfig::from_env();
|
||||
let bootstrap = BootstrapMode::default();
|
||||
let mut simulator = Simulator::new(config, network_config);
|
||||
|
||||
simulator.bootstrap(bootstrap);
|
||||
|
||||
let rounds = read_var("ROUNDS", 30);
|
||||
for i in 0..rounds {
|
||||
let from = simulator.random_peer();
|
||||
let message = format!("m{i}").into_bytes().into();
|
||||
let messages = vec![(from, message)];
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
let avg = simulator.round_stats_average().mean;
|
||||
println!(
|
||||
"average with {} peers after {} rounds:\n{}",
|
||||
simulator.peer_count(),
|
||||
rounds,
|
||||
avg
|
||||
);
|
||||
println!("{}", simulator.report());
|
||||
assert!(avg.ldh < 18.);
|
||||
assert!(avg.rmr < 1.);
|
||||
assert_eq!(avg.missed, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// #[traced_test]
|
||||
fn big_single_sender() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
|
||||
let network_config = NetworkConfig::default();
|
||||
|
||||
let config = SimulatorConfig::from_env();
|
||||
let bootstrap = BootstrapMode::default();
|
||||
let rounds = read_var("ROUNDS", 30);
|
||||
let mut simulator = Simulator::new(config, network_config);
|
||||
simulator.bootstrap(bootstrap);
|
||||
let from = simulator.random_peer();
|
||||
for i in 0..rounds {
|
||||
let message = format!("m{i}").into_bytes().into();
|
||||
let messages = vec![(from, message)];
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
let avg = simulator.round_stats_average().mean;
|
||||
println!(
|
||||
"average with {} peers after {} rounds:\n{}",
|
||||
simulator.peer_count(),
|
||||
rounds,
|
||||
avg
|
||||
);
|
||||
println!("{}", simulator.report());
|
||||
assert!(avg.ldh < 15.);
|
||||
assert!(avg.rmr < 0.2);
|
||||
assert_eq!(avg.missed, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
// #[traced_test]
|
||||
fn big_burst() {
|
||||
tracing_subscriber::fmt::try_init().ok();
|
||||
let network_config = NetworkConfig::default();
|
||||
let config = SimulatorConfig::from_env();
|
||||
let bootstrap = BootstrapMode::default();
|
||||
let rounds = read_var("ROUNDS", 5);
|
||||
|
||||
let mut simulator = Simulator::new(config, network_config);
|
||||
simulator.bootstrap(bootstrap);
|
||||
let messages_per_peer = read_var("MESSAGES_PER_PEER", 1);
|
||||
for i in 0..rounds {
|
||||
let mut messages = vec![];
|
||||
for id in simulator.network.peer_ids() {
|
||||
for j in 0..messages_per_peer {
|
||||
let message: bytes::Bytes = format!("{i}:{j}.{id}").into_bytes().into();
|
||||
messages.push((id, message));
|
||||
}
|
||||
}
|
||||
simulator.gossip_round(messages);
|
||||
}
|
||||
let avg = simulator.round_stats_average().mean;
|
||||
println!(
|
||||
"average with {} peers after {} rounds:\n{}",
|
||||
simulator.peer_count(),
|
||||
rounds,
|
||||
avg
|
||||
);
|
||||
println!("{}", simulator.report());
|
||||
assert!(avg.ldh < 30.);
|
||||
assert!(avg.rmr < 3.);
|
||||
assert_eq!(avg.missed, 0.0);
|
||||
}
|
||||
|
||||
fn read_var<T: FromStr<Err: fmt::Display + fmt::Debug>>(name: &str, default: T) -> T {
|
||||
env::var(name)
|
||||
.map(|x| {
|
||||
x.parse()
|
||||
.unwrap_or_else(|_| panic!("Failed to parse environment variable {name}"))
|
||||
})
|
||||
.unwrap_or(default)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue