mirror of
https://github.com/tower-rs/tower.git
synced 2025-09-30 22:41:17 +00:00
Compare commits
376 Commits
tower-buff
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1c277bc90 | ||
![]() |
cd2bfa0f58 | ||
![]() |
9e901d450d | ||
![]() |
d21cdbf044 | ||
![]() |
fcef5928a2 | ||
![]() |
fe3156587c | ||
![]() |
50d839d3b0 | ||
![]() |
9754acb5dc | ||
![]() |
b79c0c7497 | ||
![]() |
6a3ab07b4c | ||
![]() |
ec81e5797b | ||
![]() |
81658e65ad | ||
![]() |
abb375d08c | ||
![]() |
6c8d98b470 | ||
![]() |
fb646693bf | ||
![]() |
ee149f0170 | ||
![]() |
aade4e34ff | ||
![]() |
954e4c7e8d | ||
![]() |
34a6951a46 | ||
![]() |
7dc533ef86 | ||
![]() |
a09fd9742d | ||
![]() |
f57e31b0e6 | ||
![]() |
da24532017 | ||
![]() |
6283f3aff1 | ||
![]() |
71551010ac | ||
![]() |
b2c48b46a3 | ||
![]() |
fec9e559e2 | ||
![]() |
646804d77e | ||
![]() |
7202cfeecd | ||
![]() |
85080a5617 | ||
![]() |
88a7d3e01e | ||
![]() |
a6e98a7d69 | ||
![]() |
74e925d2c8 | ||
![]() |
89ac74f320 | ||
![]() |
032d17f689 | ||
![]() |
05a0a25dcc | ||
![]() |
7d723eb2fa | ||
![]() |
f286933bec | ||
![]() |
08917603c1 | ||
![]() |
39adf5c509 | ||
![]() |
a4c20a388b | ||
![]() |
bf4ea94834 | ||
![]() |
be1a4faf66 | ||
![]() |
0c3ae8856e | ||
![]() |
0604f20c48 | ||
![]() |
d2f1703c48 | ||
![]() |
664cb35abb | ||
![]() |
64182d8243 | ||
![]() |
74881d5311 | ||
![]() |
b01bb12ddd | ||
![]() |
6f3050614f | ||
![]() |
c34182d0b3 | ||
![]() |
387d2844b7 | ||
![]() |
787f5dd81b | ||
![]() |
c9d84cde0c | ||
![]() |
d27ba65891 | ||
![]() |
582a0e0c74 | ||
![]() |
c049ded33f | ||
![]() |
7d829f198e | ||
![]() |
87fa8ef782 | ||
![]() |
3f31ffd2cf | ||
![]() |
c5632a26aa | ||
![]() |
4362dfc70c | ||
![]() |
b12f14861f | ||
![]() |
b6b0f27197 | ||
![]() |
e0558266a3 | ||
![]() |
aec7b8f417 | ||
![]() |
6d34340f1e | ||
![]() |
06a0e597e5 | ||
![]() |
8805309486 | ||
![]() |
19c1a1dbb8 | ||
![]() |
ee826286fd | ||
![]() |
edd922d6d0 | ||
![]() |
22b6fc743b | ||
![]() |
45a13b128d | ||
![]() |
3c170aaf19 | ||
![]() |
8b522920f8 | ||
![]() |
5064987ffe | ||
![]() |
34d6e7befa | ||
![]() |
71292ee683 | ||
![]() |
cc9e5bea6f | ||
![]() |
9f86b8f3d8 | ||
![]() |
e4e440906d | ||
![]() |
d8c73fcb33 | ||
![]() |
04f0bd0cc3 | ||
![]() |
4bcfaeb33e | ||
![]() |
0e907963c3 | ||
![]() |
f0e999521d | ||
![]() |
522687c2cd | ||
![]() |
eee7d24e3a | ||
![]() |
12a06035eb | ||
![]() |
5e280fedca | ||
![]() |
9c184d81bc | ||
![]() |
7b6587e412 | ||
![]() |
386de64ab4 | ||
![]() |
db43c07e96 | ||
![]() |
665834c7a6 | ||
![]() |
2f50b49f5a | ||
![]() |
20d7b55556 | ||
![]() |
71a1578f27 | ||
![]() |
d0d8707ac0 | ||
![]() |
7f004da56f | ||
![]() |
373a010ff5 | ||
![]() |
b20b3cbd57 | ||
![]() |
2d167204dc | ||
![]() |
7674109b28 | ||
![]() |
4d80f7ed90 | ||
![]() |
48f8ae90a4 | ||
![]() |
973bf71583 | ||
![]() |
62df5e72b0 | ||
![]() |
0f16ea5652 | ||
![]() |
80c6e38f2f | ||
![]() |
d4865641e7 | ||
![]() |
1c9631d7b3 | ||
![]() |
62e09024aa | ||
![]() |
c4cb3b0788 | ||
![]() |
d91c0f5ba3 | ||
![]() |
3a134ba08a | ||
![]() |
ee131aaf46 | ||
![]() |
77760198f1 | ||
![]() |
31dbc90c45 | ||
![]() |
b5d2c8f1d3 | ||
![]() |
74f9047f30 | ||
![]() |
71ece8ea5a | ||
![]() |
ce2f031523 | ||
![]() |
31d5a6652a | ||
![]() |
5e0c8da260 | ||
![]() |
53ec99eb8f | ||
![]() |
7b1528815e | ||
![]() |
58544d65d9 | ||
![]() |
8ceee45fdf | ||
![]() |
7b4ebc6c3e | ||
![]() |
1467321fab | ||
![]() |
9b4261fe8a | ||
![]() |
e1760d385d | ||
![]() |
f2505d2ade | ||
![]() |
208160e6bb | ||
![]() |
7461abc6b5 | ||
![]() |
9948f2561d | ||
![]() |
b538246296 | ||
![]() |
db0b2c5885 | ||
![]() |
a63e3cf07a | ||
![]() |
a8884ae150 | ||
![]() |
04369f3b8f | ||
![]() |
ba3d431c04 | ||
![]() |
ec547b329b | ||
![]() |
f90f518f9f | ||
![]() |
e49700a79d | ||
![]() |
ccfaffc1f4 | ||
![]() |
52fab7c446 | ||
![]() |
0226ef0f4c | ||
![]() |
02b6731350 | ||
![]() |
d80044f825 | ||
![]() |
776f215135 | ||
![]() |
3d9efce2d3 | ||
![]() |
886f72a53e | ||
![]() |
2683ab6231 | ||
![]() |
f2bb123928 | ||
![]() |
1cd65c104c | ||
![]() |
f62c5b5350 | ||
![]() |
487e531c8b | ||
![]() |
78a32743bd | ||
![]() |
2af77e811e | ||
![]() |
af03a45ced | ||
![]() |
5ad1757367 | ||
![]() |
359db0ee3f | ||
![]() |
1270d691d2 | ||
![]() |
b8927bc32a | ||
![]() |
aa29693bd1 | ||
![]() |
00377d1f80 | ||
![]() |
ec497f7dd8 | ||
![]() |
0ad132bdbe | ||
![]() |
b4169c7753 | ||
![]() |
7aecf78ac0 | ||
![]() |
0e818a467e | ||
![]() |
e988f87824 | ||
![]() |
7d3443e6f0 | ||
![]() |
d25589d3a6 | ||
![]() |
b4d3844693 | ||
![]() |
b447771855 | ||
![]() |
44ad621c56 | ||
![]() |
ca685ae943 | ||
![]() |
992702fd20 | ||
![]() |
bebe677074 | ||
![]() |
f171390703 | ||
![]() |
3b7c91ed58 | ||
![]() |
2a7d47adda | ||
![]() |
b7a7c280ee | ||
![]() |
3bdaae9284 | ||
![]() |
fdd66e5305 | ||
![]() |
bef0ade3cb | ||
![]() |
d0fde833b1 | ||
![]() |
878b10f57d | ||
![]() |
0100800dda | ||
![]() |
6be2ff68dd | ||
![]() |
0ede7c335e | ||
![]() |
f76fe9f38f | ||
![]() |
704bfa819e | ||
![]() |
ff85e3ade8 | ||
![]() |
4f65e3017b | ||
![]() |
45974d018d | ||
![]() |
124816a40e | ||
![]() |
3a8d31c60f | ||
![]() |
d4d1c67c6a | ||
![]() |
5e1e077448 | ||
![]() |
450fa3d2be | ||
![]() |
43c44922af | ||
![]() |
069c9085b1 | ||
![]() |
ddc64e8d4d | ||
![]() |
1a84543317 | ||
![]() |
ad348d8ee5 | ||
![]() |
ab7518ef13 | ||
![]() |
4316894422 | ||
![]() |
b12a3e3ae9 | ||
![]() |
007b648ea9 | ||
![]() |
98e0e41db1 | ||
![]() |
a0a66b10a2 | ||
![]() |
1c2d50680a | ||
![]() |
6a25d322b5 | ||
![]() |
8752a38117 | ||
![]() |
82e578b5b0 | ||
![]() |
39112cb0ba | ||
![]() |
05b165056b | ||
![]() |
c87fdd9c1e | ||
![]() |
5947e2e145 | ||
![]() |
85b657bf93 | ||
![]() |
5e1788f494 | ||
![]() |
cd7dd12315 | ||
![]() |
8a73440c1a | ||
![]() |
d34019045f | ||
![]() |
0520a6a467 | ||
![]() |
81cfbab19e | ||
![]() |
9dd2314048 | ||
![]() |
2e06782241 | ||
![]() |
c4d70b535b | ||
![]() |
8df2a3e410 | ||
![]() |
0f9eb648a5 | ||
![]() |
378433fc75 | ||
![]() |
b575175210 | ||
![]() |
52fde9767c | ||
![]() |
b6f5f586c5 | ||
![]() |
52d9e95a38 | ||
![]() |
ba1fdd755b | ||
![]() |
414e3b0809 | ||
![]() |
be156e733d | ||
![]() |
1a67100aab | ||
![]() |
ae34c9b4a1 | ||
![]() |
96529148d8 | ||
![]() |
650e5be58e | ||
![]() |
47c3a14560 | ||
![]() |
ccfe7da592 | ||
![]() |
7e35b758be | ||
![]() |
40103d84ce | ||
![]() |
7b48479bd2 | ||
![]() |
d63665515c | ||
![]() |
fe7919b1a4 | ||
![]() |
86eef82d2f | ||
![]() |
1e87d7ca8b | ||
![]() |
2fede40bdb | ||
![]() |
2dc9a72bea | ||
![]() |
1863304331 | ||
![]() |
2e9e2d1813 | ||
![]() |
fd2d034e97 | ||
![]() |
f6650b90c7 | ||
![]() |
f130e5e113 | ||
![]() |
1843416dfe | ||
![]() |
423ecee7e9 | ||
![]() |
fdc7460f5a | ||
![]() |
e2f1a49cf3 | ||
![]() |
0d2a3778ad | ||
![]() |
54dd475ec0 | ||
![]() |
15c58e8842 | ||
![]() |
877c194b1b | ||
![]() |
ec6215fb2f | ||
![]() |
45e311c2f2 | ||
![]() |
b6c67182cb | ||
![]() |
c3c6780d31 | ||
![]() |
a4cb384751 | ||
![]() |
bb5c02ca58 | ||
![]() |
a62fe875c4 | ||
![]() |
a4c02f5d9c | ||
![]() |
186a0fb4a3 | ||
![]() |
51a374c564 | ||
![]() |
87ad2e1cc8 | ||
![]() |
7e55b7fa0b | ||
![]() |
2d24d84e7c | ||
![]() |
4a4593d522 | ||
![]() |
52dbdda23d | ||
![]() |
fac5c361a4 | ||
![]() |
e414b2b7d3 | ||
![]() |
30f11bfaa2 | ||
![]() |
abe5b78542 | ||
![]() |
3bff86e28e | ||
![]() |
7fa1054892 | ||
![]() |
2653f70884 | ||
![]() |
03dc7069aa | ||
![]() |
d5b36b54a5 | ||
![]() |
6baf381879 | ||
![]() |
5a561b7776 | ||
![]() |
55b5150a89 | ||
![]() |
52075f3c6f | ||
![]() |
b86d7fb6e4 | ||
![]() |
8509ab879d | ||
![]() |
f4a81d2c7d | ||
![]() |
ca951d56f4 | ||
![]() |
206f3d9941 | ||
![]() |
167f791a9f | ||
![]() |
e8cef688a0 | ||
![]() |
42376d484b | ||
![]() |
fc211c3b7c | ||
![]() |
7bb3a646a7 | ||
![]() |
2f6c0afde3 | ||
![]() |
67a9e27177 | ||
![]() |
bd62f64d6c | ||
![]() |
48f97c3dce | ||
![]() |
768528e737 | ||
![]() |
c40185901e | ||
![]() |
589eb44377 | ||
![]() |
a3c16e85f9 | ||
![]() |
a8def9349f | ||
![]() |
90ef2a64b4 | ||
![]() |
04fd0c5898 | ||
![]() |
395889c763 | ||
![]() |
6c68c56dc4 | ||
![]() |
9b8db5b393 | ||
![]() |
97a2bc18b9 | ||
![]() |
83752ab6c2 | ||
![]() |
3d642f5ca0 | ||
![]() |
fb124a14f0 | ||
![]() |
921325ac2d | ||
![]() |
65e07064db | ||
![]() |
d333e9f32f | ||
![]() |
4c1df65a75 | ||
![]() |
87976ae418 | ||
![]() |
1ca999fde1 | ||
![]() |
0802ca2bce | ||
![]() |
9691d0d379 | ||
![]() |
adca66cf74 | ||
![]() |
eac0ea30c3 | ||
![]() |
4eb47b01dc | ||
![]() |
233aab1988 | ||
![]() |
4f71951221 | ||
![]() |
154bd69b9f | ||
![]() |
390e124525 | ||
![]() |
693965fa4a | ||
![]() |
f8097a60f6 | ||
![]() |
db116d1937 | ||
![]() |
5ad02b73d9 | ||
![]() |
7ae5967e7a | ||
![]() |
ae611db665 | ||
![]() |
84da777ad1 | ||
![]() |
6cb9c1099e | ||
![]() |
652137aaa3 | ||
![]() |
793e2e8e94 | ||
![]() |
1351aaa9d8 | ||
![]() |
fe9cef6006 | ||
![]() |
b7faef31e9 | ||
![]() |
168539ed9e | ||
![]() |
26d096bd99 | ||
![]() |
72219ce862 | ||
![]() |
40fbb85c4b | ||
![]() |
491dfbe634 | ||
![]() |
b39a4881d8 | ||
![]() |
18b30eb70e | ||
![]() |
67a11f27ff | ||
![]() |
03ec4aafa8 | ||
![]() |
313530c875 | ||
![]() |
82e7b8a27b | ||
![]() |
a496fbf72c | ||
![]() |
42f4b7781e | ||
![]() |
a611a14096 | ||
![]() |
9b27863a61 | ||
![]() |
b9c2fea0fc | ||
![]() |
4d6d2c8572 | ||
![]() |
8a646dd25c | ||
![]() |
73c74252e6 |
121
.github/workflows/CI.yml
vendored
Normal file
121
.github/workflows/CI.yml
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request: {}
|
||||
|
||||
env:
|
||||
MSRV: 1.64.0
|
||||
|
||||
jobs:
|
||||
check-stable:
|
||||
# Run `cargo check` first to ensure that the pushed code at least compiles.
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: Check
|
||||
run: cargo check --workspace --all-features --all-targets
|
||||
|
||||
check-docs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: cargo doc
|
||||
working-directory: ${{ matrix.subcrate }}
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D rustdoc::broken_intra_doc_links"
|
||||
run: cargo doc --all-features --no-deps
|
||||
|
||||
check-msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "install Rust ${{ env.MSRV }}"
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
- name: "install Rust nightly"
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Select minimal versions
|
||||
run: |
|
||||
cargo update -Z minimal-versions
|
||||
cargo update -p lazy_static --precise 1.5.0
|
||||
- name: Check
|
||||
run: |
|
||||
rustup default ${{ env.MSRV }}
|
||||
cargo check --all --all-targets --all-features --locked
|
||||
|
||||
cargo-hack:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- name: install cargo-hack
|
||||
uses: taiki-e/install-action@cargo-hack
|
||||
- name: cargo hack check
|
||||
working-directory: ${{ matrix.subcrate }}
|
||||
run: cargo hack check --each-feature --no-dev-deps --workspace
|
||||
|
||||
test-versions:
|
||||
# Test against the stable, beta, and nightly Rust toolchains on ubuntu-latest.
|
||||
needs: check-stable
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
# Disable fail-fast. If the test run for a particular Rust version fails,
|
||||
# don't cancel the other test runs, so that we can determine whether a
|
||||
# failure only occurs on a particular version.
|
||||
fail-fast: false
|
||||
matrix:
|
||||
rust: [stable, beta, nightly]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "install Rust ${{ matrix.rust }}"
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --all-features
|
||||
|
||||
test-msrv:
|
||||
needs: check-msrv
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "install Rust ${{ env.MSRV }}"
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ env.MSRV }}
|
||||
- name: "install Rust nightly"
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Select minimal versions
|
||||
run: |
|
||||
cargo update -Z minimal-versions
|
||||
cargo update -p lazy_static --precise 1.5.0
|
||||
- name: test
|
||||
run: |
|
||||
rustup default ${{ env.MSRV }}
|
||||
cargo check --workspace --all-features --locked
|
||||
|
||||
style:
|
||||
needs: check-stable
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt
|
||||
- name: rustfmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
deny-check:
|
||||
name: cargo-deny check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
with:
|
||||
command: check
|
7
.github/workflows/patch.toml
vendored
Normal file
7
.github/workflows/patch.toml
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
# Patch dependencies to run all tests against versions of the crate in the
|
||||
# repository.
|
||||
[patch.crates-io]
|
||||
tower = { path = "tower" }
|
||||
tower-layer = { path = "tower-layer" }
|
||||
tower-service = { path = "tower-service" }
|
||||
tower-test = { path = "tower-test" }
|
32
.github/workflows/publish.yml
vendored
Normal file
32
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: Deploy API Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Install nightly Rust
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: nightly
|
||||
- name: Generate documentation
|
||||
run: cargo doc --workspace --no-deps --all-features
|
||||
env:
|
||||
# Enable the RustDoc `#[doc(cfg(...))]` attribute.
|
||||
RUSTDOCFLAGS: --cfg docsrs
|
||||
- name: Deploy documentation
|
||||
if: success()
|
||||
uses: crazy-max/ghaction-github-pages@v1
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: target/doc
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
24
.github/workflows/release.yml
vendored
Normal file
24
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: create github release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- tower-[0-9]+.*
|
||||
- tower-[a-z]+-[0-9]+.*
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create GitHub release
|
||||
# only publish from the origin repository
|
||||
if: github.repository_owner == 'tower-rs'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: taiki-e/create-gh-release-action@v1.3.0
|
||||
with:
|
||||
prefix: "(tower)|(tower-[a-z]+)"
|
||||
changelog: "$prefix/CHANGELOG.md"
|
||||
title: "$prefix $version"
|
||||
branch: "(master)|(v[0-9]+.[0-9]+.x)"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
29
Cargo.toml
29
Cargo.toml
@ -2,17 +2,26 @@
|
||||
|
||||
members = [
|
||||
"tower",
|
||||
"tower-balance",
|
||||
"tower-buffer",
|
||||
"tower-discover",
|
||||
"tower-filter",
|
||||
"tower-layer",
|
||||
"tower-limit",
|
||||
"tower-load-shed",
|
||||
"tower-reconnect",
|
||||
"tower-retry",
|
||||
"tower-service",
|
||||
"tower-test",
|
||||
"tower-timeout",
|
||||
"tower-util",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
futures = "0.3.22"
|
||||
futures-core = "0.3.22"
|
||||
futures-util = { version = "0.3.22", default-features = false }
|
||||
hdrhistogram = { version = "7.0", default-features = false }
|
||||
http = "1"
|
||||
indexmap = "2.0.2"
|
||||
pin-project-lite = "0.2.7"
|
||||
quickcheck = "1"
|
||||
rand = "0.9"
|
||||
slab = "0.4.9"
|
||||
sync_wrapper = "1"
|
||||
tokio = "1.6.2"
|
||||
tokio-stream = "0.1.1"
|
||||
tokio-test = "0.4"
|
||||
tokio-util = { version = "0.7.0", default-features = false }
|
||||
tracing = { version = "0.1.2", default-features = false }
|
||||
tracing-subscriber = { version = "0.3", default-features = false }
|
||||
|
102
README.md
102
README.md
@ -3,13 +3,25 @@
|
||||
Tower is a library of modular and reusable components for building robust
|
||||
networking clients and servers.
|
||||
|
||||
[![Build Status][azure-badge]][azure-url]
|
||||
[![Gitter][gitter-badge]][gitter-url]
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![Documentation][docs-badge]][docs-url]
|
||||
[![Documentation (master)][docs-master-badge]][docs-master-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[azure-badge]: https://dev.azure.com/tower-rs/Tower/_apis/build/status/tower-rs.tower?branchName=master
|
||||
[azure-url]: https://dev.azure.com/tower-rs/Tower/_build/latest?definitionId=1&branchName=master
|
||||
[gitter-badge]: https://badges.gitter.im/tower-rs/tower.svg
|
||||
[gitter-url]: https://gitter.im/tower-rs/tower
|
||||
[crates-badge]: https://img.shields.io/crates/v/tower.svg
|
||||
[crates-url]: https://crates.io/crates/tower
|
||||
[docs-badge]: https://docs.rs/tower/badge.svg
|
||||
[docs-url]: https://docs.rs/tower
|
||||
[docs-master-badge]: https://img.shields.io/badge/docs-master-blue
|
||||
[docs-master-url]: https://tower-rs.github.io/tower/tower
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: LICENSE
|
||||
[actions-badge]: https://github.com/tower-rs/tower/workflows/CI/badge.svg
|
||||
[actions-url]:https://github.com/tower-rs/tower/actions?query=workflow%3ACI
|
||||
[discord-badge]: https://img.shields.io/discord/500028886025895936?logo=discord&label=discord&logoColor=white
|
||||
[discord-url]: https://discord.gg/EeF3cQw
|
||||
|
||||
## Overview
|
||||
|
||||
@ -17,52 +29,20 @@ Tower aims to make it as easy as possible to build robust networking clients and
|
||||
servers. It is protocol agnostic, but is designed around a request / response
|
||||
pattern. If your protocol is entirely stream based, Tower may not be a good fit.
|
||||
|
||||
## Project Layout
|
||||
## Supported Rust Versions
|
||||
|
||||
Tower consists of a number of components, each of which live in their own sub
|
||||
crates.
|
||||
Tower will keep a rolling MSRV (minimum supported Rust version) policy of **at
|
||||
least** 6 months. When increasing the MSRV, the new Rust version must have been
|
||||
released at least six months ago. The current MSRV is 1.64.0.
|
||||
|
||||
* [`tower`]: The main user facing crate that provides batteries included tower services ([docs][t-docs]).
|
||||
## `no_std`
|
||||
|
||||
* [`tower-service`]: The foundational traits upon which Tower is built
|
||||
([docs][ts-docs]).
|
||||
`tower` itself is _not_ `no_std` compatible, but `tower-layer` and `tower-service` are.
|
||||
|
||||
* [`tower-layer`]: The foundational trait to compose services together
|
||||
([docs][tl-docs]).
|
||||
## Getting Started
|
||||
|
||||
* [`tower-balance`]: A load balancer. Load is balanced across a number of
|
||||
services ([docs][tb-docs]).
|
||||
|
||||
* [`tower-buffer`]: A buffering middleware. If the inner service is not ready to
|
||||
handle the next request, `tower-buffer` stores the request in an internal
|
||||
queue ([docs][tbuf-docs]).
|
||||
|
||||
* [`tower-discover`]: Service discovery abstraction ([docs][td-docs]).
|
||||
|
||||
* [`tower-filter`]: Middleware that conditionally dispatch requests to the inner
|
||||
service based on a predicate ([docs][tf-docs]).
|
||||
|
||||
* [`tower-limit`]: Middleware limiting the number of requests that are
|
||||
processed ([docs][tlim-docs]).
|
||||
|
||||
* [`tower-reconnect`]: Middleware that automatically reconnects the inner
|
||||
service when it becomes degraded ([docs][tre-docs]).
|
||||
|
||||
* [`tower-retry`]: Middleware that retries requests based on a given `Policy`
|
||||
([docs][tretry-docs]).
|
||||
|
||||
* [`tower-test`]: Testing utilies ([docs][ttst-docs]).
|
||||
|
||||
* [`tower-timeout`]: Middleware that applies a timeout to requests
|
||||
([docs][tt-docs]).
|
||||
|
||||
* [`tower-util`]: Miscellaneous additional utilities for Tower
|
||||
([docs][tu-docs]).
|
||||
|
||||
## Status
|
||||
|
||||
Currently, only [`tower-service`], the foundational trait, has been released to
|
||||
crates.io. The rest of the library will be following shortly.
|
||||
If you're brand new to Tower and want to start with the basics we recommend you
|
||||
check out some of our [guides].
|
||||
|
||||
## License
|
||||
|
||||
@ -74,30 +54,4 @@ Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
[`tower`]: tower
|
||||
[t-docs]: https://tower-rs.github.io/tower/doc/tower/index.html
|
||||
[`tower-service`]: tower-service
|
||||
[ts-docs]: https://docs.rs/tower-service/
|
||||
[`tower-layer`]: tower-layer
|
||||
[tl-docs]: https://docs.rs/tower-layer/
|
||||
[`tower-balance`]: tower-balance
|
||||
[tb-docs]: https://tower-rs.github.io/tower/doc/tower_balance/index.html
|
||||
[`tower-buffer`]: tower-buffer
|
||||
[tbuf-docs]: https://tower-rs.github.io/tower/doc/tower_buffer/index.html
|
||||
[`tower-discover`]: tower-discover
|
||||
[td-docs]: https://tower-rs.github.io/tower/doc/tower_discover/index.html
|
||||
[`tower-filter`]: tower-filter
|
||||
[tf-docs]: https://tower-rs.github.io/tower/doc/tower_filter/index.html
|
||||
[`tower-limit`]: tower-limit
|
||||
[tlim-docs]: https://tower-rs.github.io/tower/doc/tower_limit/index.html
|
||||
[`tower-reconnect`]: tower-reconnect
|
||||
[tre-docs]: https://tower-rs.github.io/tower/doc/tower_reconnect/index.html
|
||||
[`tower-retry`]: tower-retry
|
||||
[tretry-docs]: https://tower-rs.github.io/tower/doc/tower_retry/index.html
|
||||
[`tower-timeout`]: tower-timeout
|
||||
[`tower-test`]: tower-test
|
||||
[ttst-docs]: https://tower-rs.github.io/tower/doc/tower_test/index.html
|
||||
[`tower-rate-limit`]: tower-rate-limit
|
||||
[tt-docs]: https://tower-rs.github.io/tower/doc/tower_timeout/index.html
|
||||
[`tower-util`]: tower-util
|
||||
[tu-docs]: https://tower-rs.github.io/tower/doc/tower_util/index.html
|
||||
[guides]: https://github.com/tower-rs/tower/tree/master/guides
|
||||
|
@ -1,35 +0,0 @@
|
||||
trigger: ["master"]
|
||||
pr: ["master"]
|
||||
|
||||
jobs:
|
||||
- template: ci/azure-rustfmt.yml
|
||||
parameters:
|
||||
name: rustfmt
|
||||
|
||||
# Basic test run on all platforms
|
||||
- template: ci/azure-test-stable.yml
|
||||
parameters:
|
||||
name: Linux_Stable
|
||||
displayName: Test
|
||||
vmImage: ubuntu-16.04
|
||||
crates:
|
||||
- tower-balance
|
||||
- tower-buffer
|
||||
- tower-discover
|
||||
- tower-filter
|
||||
- tower-layer
|
||||
- tower-limit
|
||||
- tower-load-shed
|
||||
- tower-reconnect
|
||||
- tower-retry
|
||||
- tower-service
|
||||
- tower-test
|
||||
- tower-timeout
|
||||
- tower-util
|
||||
- tower
|
||||
|
||||
- template: ci/azure-deploy-docs.yml
|
||||
parameters:
|
||||
dependsOn:
|
||||
- rustfmt
|
||||
- Linux_Stable
|
@ -1,39 +0,0 @@
|
||||
parameters:
|
||||
dependsOn: []
|
||||
|
||||
jobs:
|
||||
- job: documentation
|
||||
displayName: 'Deploy API Documentation'
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||
pool:
|
||||
vmImage: 'Ubuntu 16.04'
|
||||
dependsOn:
|
||||
- ${{ parameters.dependsOn }}
|
||||
steps:
|
||||
- template: azure-install-rust.yml
|
||||
parameters:
|
||||
platform: ${{parameters.name}}
|
||||
rust_version: stable
|
||||
- script: |
|
||||
cargo doc --all --no-deps
|
||||
cp -R target/doc '$(Build.BinariesDirectory)'
|
||||
displayName: 'Generate Documentation'
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
git --version
|
||||
ls -la
|
||||
git init
|
||||
git config user.name 'Deployment Bot (from Azure Pipelines)'
|
||||
git config user.email 'deploy@tower-rs.com'
|
||||
git config --global credential.helper 'store --file ~/.my-credentials'
|
||||
printf "protocol=https\nhost=github.com\nusername=carllerche\npassword=%s\n\n" "$GITHUB_TOKEN" | git credential-store --file ~/.my-credentials store
|
||||
git remote add origin https://github.com/tower-rs/tower
|
||||
git checkout -b gh-pages
|
||||
git add .
|
||||
git commit -m 'Deploy Tower API documentation'
|
||||
git push -f origin gh-pages
|
||||
env:
|
||||
GITHUB_TOKEN: $(githubPersonalToken)
|
||||
workingDirectory: '$(Build.BinariesDirectory)'
|
||||
displayName: 'Deploy Documentation'
|
@ -1,28 +0,0 @@
|
||||
steps:
|
||||
# Linux and macOS.
|
||||
- script: |
|
||||
set -e
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUSTUP_TOOLCHAIN
|
||||
echo "##vso[task.setvariable variable=PATH;]$PATH:$HOME/.cargo/bin"
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
|
||||
displayName: "Install rust (*nix)"
|
||||
condition: not(eq(variables['Agent.OS'], 'Windows_NT'))
|
||||
|
||||
# Windows.
|
||||
- script: |
|
||||
echo "windows"
|
||||
curl -sSf -o rustup-init.exe https://win.rustup.rs
|
||||
rustup-init.exe -y --default-toolchain %RUSTUP_TOOLCHAIN%
|
||||
set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||
echo "##vso[task.setvariable variable=PATH;]%PATH%;%USERPROFILE%\.cargo\bin"
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: ${{parameters.rust_version}}
|
||||
displayName: Install rust (windows)
|
||||
condition: eq(variables['Agent.OS'], 'Windows_NT')
|
||||
|
||||
# All platforms.
|
||||
- script: |
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
displayName: Query rust and cargo versions
|
@ -1,9 +0,0 @@
|
||||
steps:
|
||||
- bash: |
|
||||
set -e
|
||||
|
||||
if git log --no-merges -1 --format='%s' | grep -q '[ci-release]'; then
|
||||
echo "##vso[task.setvariable variable=isRelease]true"
|
||||
fi
|
||||
failOnStderr: true
|
||||
displayName: Check if release commit
|
@ -1,17 +0,0 @@
|
||||
steps:
|
||||
- script: |
|
||||
set -e
|
||||
|
||||
# Remove any existing patch statements
|
||||
mv Cargo.toml Cargo.toml.bck
|
||||
sed -n '/\[patch.crates-io\]/q;p' Cargo.toml.bck > Cargo.toml
|
||||
|
||||
# Patch all crates
|
||||
cat ci/patch.toml >> Cargo.toml
|
||||
|
||||
# Print `Cargo.toml` for debugging
|
||||
echo "~~~~ Cargo.toml ~~~~"
|
||||
cat Cargo.toml
|
||||
echo "~~~~~~~~~~~~~~~~~~~~"
|
||||
displayName: Patch Cargo.toml
|
||||
|
@ -1,16 +0,0 @@
|
||||
jobs:
|
||||
# Check formatting
|
||||
- job: ${{ parameters.name }}
|
||||
displayName: Check rustfmt
|
||||
pool:
|
||||
vmImage: ubuntu-16.04
|
||||
steps:
|
||||
- template: azure-install-rust.yml
|
||||
parameters:
|
||||
rust_version: stable
|
||||
- bash: |
|
||||
rustup component add rustfmt
|
||||
displayName: Install rustfmt
|
||||
- bash: |
|
||||
cargo fmt --all -- --check
|
||||
displayName: Check formatting
|
@ -1,31 +0,0 @@
|
||||
parameters:
|
||||
crates: []
|
||||
|
||||
jobs:
|
||||
- job: ${{ parameters.name }}
|
||||
displayName: ${{ parameters.displayName }}
|
||||
pool:
|
||||
vmImage: ${{ parameters.vmImage }}
|
||||
steps:
|
||||
- template: azure-install-rust.yml
|
||||
parameters:
|
||||
rust_version: stable
|
||||
|
||||
- template: azure-is-release.yml
|
||||
|
||||
- ${{ each crate in parameters.crates }}:
|
||||
- script: cargo test
|
||||
env:
|
||||
CI: 'True'
|
||||
displayName: cargo test -p ${{ crate }}
|
||||
workingDirectory: $(Build.SourcesDirectory)/${{ crate }}
|
||||
condition: and(succeeded(), not(variables['isRelease']))
|
||||
|
||||
- template: azure-patch-crates.yml
|
||||
|
||||
- ${{ each crate in parameters.crates }}:
|
||||
- script: cargo test
|
||||
env:
|
||||
CI: 'True'
|
||||
displayName: cargo test -p ${{ crate }}
|
||||
workingDirectory: $(Build.SourcesDirectory)/${{ crate }}
|
@ -1,17 +0,0 @@
|
||||
# Patch dependencies to run all tests against versions of the crate in the
|
||||
# repository.
|
||||
[patch.crates-io]
|
||||
tower = { path = "tower" }
|
||||
tower-balance = { path = "tower-balance" }
|
||||
tower-buffer = { path = "tower-buffer" }
|
||||
tower-discover = { path = "tower-discover" }
|
||||
tower-filter = { path = "tower-filter" }
|
||||
tower-layer = { path = "tower-layer" }
|
||||
tower-limit = { path = "tower-limit" }
|
||||
tower-load-shed = { path = "tower-load-shed" }
|
||||
tower-reconnect = { path = "tower-reconnect" }
|
||||
tower-retry = { path = "tower-retry" }
|
||||
tower-service = { path = "tower-service" }
|
||||
tower-test = { path = "tower-test" }
|
||||
tower-timeout = { path = "tower-timeout" }
|
||||
tower-util = { path = "tower-util" }
|
22
deny.toml
Normal file
22
deny.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[advisories]
|
||||
vulnerability = "deny"
|
||||
unmaintained = "warn"
|
||||
notice = "warn"
|
||||
|
||||
[licenses]
|
||||
unlicensed = "deny"
|
||||
allow = []
|
||||
deny = []
|
||||
copyleft = "warn"
|
||||
allow-osi-fsf-free = "either"
|
||||
confidence-threshold = 0.8
|
||||
|
||||
[bans]
|
||||
multiple-versions = "deny"
|
||||
highlight = "all"
|
||||
skip = []
|
||||
|
||||
[sources]
|
||||
unknown-registry = "warn"
|
||||
unknown-git = "warn"
|
||||
allow-git = []
|
22
examples/Cargo.toml
Normal file
22
examples/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "examples"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2018"
|
||||
|
||||
# If you copy one of the examples into a new project, you should be using
|
||||
# [dependencies] instead.
|
||||
[dev-dependencies]
|
||||
tower = { version = "0.4", path = "../tower", features = ["full"] }
|
||||
tower-service = "0.3"
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
rand = "0.9"
|
||||
pin-project = "1.0"
|
||||
futures = "0.3.22"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.2"
|
||||
hdrhistogram = "7"
|
||||
|
||||
[[example]]
|
||||
name = "balance"
|
||||
path = "balance.rs"
|
27
guides/README.md
Normal file
27
guides/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Tower Guides
|
||||
|
||||
These guides are meant to be an introduction to Tower. At least basic Rust
|
||||
experience is assumed. Some experience with asynchronous Rust is also
|
||||
recommended. If you're brand new to async Rust, we recommend the [Asynchronous
|
||||
Programming in Rust][async-book] book or the [Tokio tutorial][tokio-tutorial].
|
||||
|
||||
Additionally, some of these guides explain Tower from the perspective of HTTP
|
||||
servers and clients. However, Tower is useful for any network protocol that
|
||||
follows an async request/response pattern. HTTP is used here because it is a
|
||||
widely known protocol, and one of Tower's more common use-cases.
|
||||
|
||||
## Guides
|
||||
|
||||
- ["Inventing the `Service` trait"][invent] walks through how Tower's
|
||||
fundamental [`Service`] trait could be designed from scratch. If you have no
|
||||
experience with Tower and want to learn the absolute basics, this is where you
|
||||
should start.
|
||||
- ["Building a middleware from scratch"][build] walks through how to build the
|
||||
[`Timeout`] middleware as it exists in Tower today, without taking any shortcuts.
|
||||
|
||||
[async-book]: https://rust-lang.github.io/async-book/
|
||||
[tokio-tutorial]: https://tokio.rs/tokio/tutorial
|
||||
[invent]: https://tokio.rs/blog/2021-05-14-inventing-the-service-trait
|
||||
[build]: https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md
|
||||
[`Service`]: https://docs.rs/tower/latest/tower/trait.Service.html
|
||||
[`Timeout`]: https://docs.rs/tower/latest/tower/timeout/struct.Timeout.html
|
674
guides/building-a-middleware-from-scratch.md
Normal file
674
guides/building-a-middleware-from-scratch.md
Normal file
@ -0,0 +1,674 @@
|
||||
# Building a middleware from scratch
|
||||
|
||||
In ["Inventing the `Service` trait"][invent] we learned all the motivations
|
||||
behind [`Service`] and why its designed the way it is. We also built a few
|
||||
smaller middleware ourselves but we took a few shortcuts in our implementation.
|
||||
In this guide we're going to build the `Timeout` middleware as it exists in
|
||||
Tower today without taking any shortcuts.
|
||||
|
||||
Writing a robust middleware requires working with async Rust at a slightly lower
|
||||
level than you might be used to. The goal of this guide is to demystify the
|
||||
concepts and patterns so you can start writing your own middleware and maybe
|
||||
even contribute back to the Tower ecosystem!
|
||||
|
||||
## Getting started
|
||||
|
||||
The middleware we're going to build is [`tower::timeout::Timeout`]. It will set
|
||||
a limit on the maximum duration its inner `Service`'s response future is allowed
|
||||
to take. If it doesn't produce a response within some amount of time, an error
|
||||
is returned. This allows the client to retry that request or report an error to
|
||||
the user, rather than waiting forever.
|
||||
|
||||
Lets start by writing a `Timeout` struct that holds the `Service` its wrapping
|
||||
and the duration of the timeout:
|
||||
|
||||
```rust
|
||||
use std::time::Duration;
|
||||
|
||||
struct Timeout<S> {
|
||||
inner: S,
|
||||
timeout: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
As we learned in ["Inventing the `Service` trait"][invent] its important for
|
||||
services to implement `Clone` such that you can convert the `&mut self` given to
|
||||
`Service::call` into an owned `self` that can be moved into the response future,
|
||||
if necessary. We should therefore add `#[derive(Clone)]` to our struct. We
|
||||
should also derive `Debug` while we're at it:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
struct Timeout<S> {
|
||||
inner: S,
|
||||
timeout: Duration,
|
||||
}
|
||||
```
|
||||
|
||||
Next we write a constructor:
|
||||
|
||||
```rust
|
||||
impl<S> Timeout<S> {
|
||||
pub fn new(inner: S, timeout: Duration) -> Self {
|
||||
Timeout { inner, timeout }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that we omit bounds on S even though we expect it to implement Service, as
|
||||
the [Rust API guidelines recommend][rust-guidelines].
|
||||
|
||||
Now the interesting bit. How to implement `Service` for `Timeout<S>`? Lets start
|
||||
with an implementation that just forwards everything to the inner service:
|
||||
|
||||
```rust
|
||||
use tower::Service;
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
impl<S, Request> Service<Request> for Timeout<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = S::Future;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
// Our middleware doesn't care about backpressure, so it's ready as long
|
||||
// as the inner service is ready.
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
self.inner.call(request)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Until you've written lots of middleware writing out a skeleton like this makes
|
||||
the process a bit easier.
|
||||
|
||||
To actually add a timeout to the inner service what we essentially have to do is
|
||||
detect when the future returned by `self.inner.call(request)` has been running
|
||||
longer than `self.timeout` and abort with an error.
|
||||
|
||||
The approach we're going to take is to call [`tokio::time::sleep`] to get a
|
||||
future that completes when we're out of time and then select the value from
|
||||
whichever of the two futures is the first to complete. We could also use
|
||||
`tokio::time::timeout` but `sleep` works just as well.
|
||||
|
||||
Creating both futures is done like this:
|
||||
|
||||
```rust
|
||||
use tokio::time::sleep;
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let response_future = self.inner.call(request);
|
||||
|
||||
// This variable has type `tokio::time::Sleep`.
|
||||
//
|
||||
// We don't have to clone `self.timeout` as it implements the `Copy` trait.
|
||||
let sleep = tokio::time::sleep(self.timeout);
|
||||
|
||||
// what to write here?
|
||||
}
|
||||
```
|
||||
|
||||
One possible return type is `Pin<Box<dyn Future<...>>>`. However we want our
|
||||
`Timeout` to add as little overhead as possible, so we would like to find a way
|
||||
to avoid allocating a `Box`. Imagine we have a large stack, with dozens of
|
||||
nested `Service`s, where each layer allocates a new `Box` for every request
|
||||
that passes through it. That would result in a lot of allocations which might
|
||||
impact performance[^1].
|
||||
|
||||
## The response future
|
||||
|
||||
To avoid using `Box` lets instead write our own `Future` implementation. We
|
||||
start by creating a struct called `ResponseFuture`. It has to be generic over
|
||||
the inner service's response future type. This is analogous to wrapping services
|
||||
in other services, but this time we're wrapping futures in other futures.
|
||||
|
||||
```rust
|
||||
use tokio::time::Sleep;
|
||||
|
||||
pub struct ResponseFuture<F> {
|
||||
response_future: F,
|
||||
sleep: Sleep,
|
||||
}
|
||||
```
|
||||
|
||||
`F` will be the type of `self.inner.call(request)`. Updating our `Service`
|
||||
implementation we get:
|
||||
|
||||
```rust
|
||||
impl<S, Request> Service<Request> for Timeout<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
|
||||
// Use our new `ResponseFuture` type.
|
||||
type Future = ResponseFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let response_future = self.inner.call(request);
|
||||
let sleep = tokio::time::sleep(self.timeout);
|
||||
|
||||
// Create our response future by wrapping the future from the inner
|
||||
// service.
|
||||
ResponseFuture {
|
||||
response_future,
|
||||
sleep,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A key point here is that Rust's futures are _lazy_. That means nothing actually
|
||||
happens until they're `await`ed or polled. So `self.inner.call(request)` will
|
||||
return immediately without actually processing the request.
|
||||
|
||||
Next we go ahead and implement `Future` for `ResponseFuture`:
|
||||
|
||||
```rust
|
||||
use std::{pin::Pin, future::Future};
|
||||
|
||||
impl<F, Response, Error> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response, Error>>,
|
||||
{
|
||||
type Output = Result<Response, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// What to write here?
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ideally we want to write something like this:
|
||||
|
||||
1. First poll `self.response_future`, and if it's ready, return the response or error it
|
||||
resolved to.
|
||||
2. Otherwise, poll `self.sleep`, and if it's ready, return an error.
|
||||
3. If neither future is ready return `Poll::Pending`.
|
||||
|
||||
We might try:
|
||||
|
||||
```rust
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.response_future.poll(cx) {
|
||||
Poll::Ready(result) => return Poll::Ready(result),
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
todo!()
|
||||
}
|
||||
```
|
||||
|
||||
However that gives an error like this:
|
||||
|
||||
```
|
||||
error[E0599]: no method named `poll` found for type parameter `F` in the current scope
|
||||
--> src/lib.rs:56:29
|
||||
|
|
||||
56 | match self.response_future.poll(cx) {
|
||||
| ^^^^ method not found in `F`
|
||||
|
|
||||
= help: items from traits can only be used if the type parameter is bounded by the trait
|
||||
help: the following traits define an item `poll`, perhaps you need to restrict type parameter `F` with one of them:
|
||||
|
|
||||
49 | impl<F: Future, Response, Error> Future for ResponseFuture<F>
|
||||
| ^^^^^^^^^
|
||||
|
||||
error: aborting due to previous error
|
||||
```
|
||||
|
||||
Unfortunately the error we get from Rust isn't very good. It tells us to add an
|
||||
`F: Future` bound even though we've already done that with `where F:
|
||||
Future<Output = Result<Response, E>>`.
|
||||
|
||||
The real issue has to do with [`Pin`]. The full details of pinning is outside
|
||||
the scope of this guide. If you're new to `Pin` we recommend ["The Why, What,
|
||||
and How of Pinning in Rust"][pin] by Jon Gjengset.
|
||||
|
||||
What Rust is trying to tell us is that we need a `Pin<&mut F>` to be able to
|
||||
call `poll`. Accessing `F` through `self.response_future` when `self` is a
|
||||
`Pin<&mut Self>` doesn't work.
|
||||
|
||||
What we need is called "pin projection" which means going from a `Pin<&mut
|
||||
Struct>` to a `Pin<&mut Field>`. Normally pin projection would require writing
|
||||
`unsafe` code but the excellent [pin-project] crate is able to handle all the
|
||||
`unsafe` details for us.
|
||||
|
||||
Using pin-project we can annotate a struct with `#[pin_project]` and add
|
||||
`#[pin]` to each field that we want to be able to access through a pinned
|
||||
reference:
|
||||
|
||||
```rust
|
||||
use pin_project::pin_project;
|
||||
|
||||
#[pin_project]
|
||||
pub struct ResponseFuture<F> {
|
||||
#[pin]
|
||||
response_future: F,
|
||||
#[pin]
|
||||
sleep: Sleep,
|
||||
}
|
||||
|
||||
impl<F, Response, Error> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response, Error>>,
|
||||
{
|
||||
type Output = Result<Response, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
// Call the magical `project` method generated by `#[pin_project]`.
|
||||
let this = self.project();
|
||||
|
||||
// `project` returns a `__ResponseFutureProjection` but we can ignore
|
||||
// the exact type. It has fields that matches `ResponseFuture` but
|
||||
// maintain pins for fields annotated with `#[pin]`.
|
||||
|
||||
// `this.response_future` is now a `Pin<&mut F>`.
|
||||
let response_future: Pin<&mut F> = this.response_future;
|
||||
|
||||
// And `this.sleep` is a `Pin<&mut Sleep>`.
|
||||
let sleep: Pin<&mut Sleep> = this.sleep;
|
||||
|
||||
// If we had another field that wasn't annotated with `#[pin]` that
|
||||
// would have been a regular `&mut` without `Pin`.
|
||||
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Pinning in Rust is a complex topic that is hard to understand but thanks to
|
||||
pin-project we're able to ignore most of that complexity. Crucially, it means we
|
||||
don't have to fully understand pinning to write Tower middleware. So if you
|
||||
didn't quite get all the stuff about `Pin` and `Unpin` fear not because
|
||||
pin-project has your back!
|
||||
|
||||
Notice in the previous code block we were able to obtain a `Pin<&mut F>` and a
|
||||
`Pin<&mut Sleep>` which is exactly what we need to call `poll`:
|
||||
|
||||
```rust
|
||||
impl<F, Response, Error> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response, Error>>,
|
||||
{
|
||||
type Output = Result<Response, Error>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
// First check if the response future is ready.
|
||||
match this.response_future.poll(cx) {
|
||||
Poll::Ready(result) => {
|
||||
// The inner service has a response ready for us or it has
|
||||
// failed.
|
||||
return Poll::Ready(result);
|
||||
}
|
||||
Poll::Pending => {
|
||||
// Not quite ready yet...
|
||||
}
|
||||
}
|
||||
|
||||
// Then check if the sleep is ready. If so the response has taken too
|
||||
// long and we have to return an error.
|
||||
match this.sleep.poll(cx) {
|
||||
Poll::Ready(()) => {
|
||||
// Our time is up, but what error do we return?!
|
||||
todo!()
|
||||
}
|
||||
Poll::Pending => {
|
||||
// Still some time remaining...
|
||||
}
|
||||
}
|
||||
|
||||
// If neither future is ready then we are still pending.
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now the only remaining question is what error should we return if the sleep
|
||||
finishes first?
|
||||
|
||||
## The error type
|
||||
|
||||
The error type we're promising to return now is the generic `Error` type which
|
||||
is the same as the inner service's error type. However we know nothing about
|
||||
that type. It is completely opaque to us and we have no way of constructing
|
||||
values of that type.
|
||||
|
||||
We have three options:
|
||||
|
||||
1. Return a boxed error trait object like `Box<dyn std::error::Error + Send +
|
||||
Sync>`.
|
||||
2. Return an enum with variants for the service error and the timeout error.
|
||||
3. Define a `TimeoutError` struct and require that our generic error type can be
|
||||
constructed from a `TimeoutError` using `TimeoutError: Into<Error>`.
|
||||
|
||||
While option 3 might seem like it is the most flexible it isn't great since it
|
||||
requires users using a custom error type to manually implement
|
||||
`From<TimeoutError> for MyError`. That quickly becomes tedious when using lots
|
||||
of middleware that each have their own error type.
|
||||
|
||||
Option 2 would mean defining an enum like this:
|
||||
|
||||
```rust
|
||||
enum TimeoutError<Error> {
|
||||
// Variant used if we hit the timeout
|
||||
Timeout(InnerTimeoutError),
|
||||
// Variant used if the inner service produced an error
|
||||
Service(Error),
|
||||
}
|
||||
```
|
||||
|
||||
While this seems ideal on the surface as we're not losing any type information
|
||||
and can use `match` to get at the exact error, the approach has three issues:
|
||||
|
||||
1. In practice its common to nest lots of middleware. That would make the final
|
||||
error enum very large. Its not unlikely to look something like
|
||||
`BufferError<RateLimitError<TimeoutError<MyError>>>`. Pattern matching on
|
||||
such a type (to, for example, determine if the error is retry-able) is very
|
||||
tedious.
|
||||
2. If we change the order our middleware are applied in we also change the final
|
||||
error type meaning we have to update our pattern matches.
|
||||
3. There is also the possibility of the final error type being very large and
|
||||
taking up a significant amount of space on the stack.
|
||||
|
||||
With this we're left with option 1 which is to convert the inner service error
|
||||
into a boxed trait object like `Box<dyn std::error::Error + Send + Sync>`. That
|
||||
means we can combine multiple errors type into one. That has the following
|
||||
advantages:
|
||||
|
||||
1. Our error handling is less fragile since changing the order middleware are
|
||||
applied in won't change the final error type.
|
||||
2. The error type now has a constant size regardless how many middleware we've
|
||||
applied.
|
||||
3. Extracting the error no longer requires a big `match` but can instead be done
|
||||
with `error.downcast_ref::<Timeout>()`.
|
||||
|
||||
However it also has the following downsides:
|
||||
|
||||
1. As we're using dynamic downcasting the compiler can no longer guarantee that
|
||||
we're exhaustively checking for every possible error type.
|
||||
2. Creating an error now requires an allocation. In practice we expect errors to
|
||||
be infrequent and therefore this shouldn't be a problem.
|
||||
|
||||
Which option you prefer is a matter of personal preference. Both have their
|
||||
advantages and disadvantages. However the pattern that we've decided to use in
|
||||
Tower is boxed trait objects. You can find the original discussion
|
||||
[here](https://github.com/tower-rs/tower/issues/131).
|
||||
|
||||
For our `Timeout` middleware that means we need to create a struct that
|
||||
implements `std::error::Error` such that we can convert it into a `Box<dyn
|
||||
std::error::Error + Send + Sync>`. We also have to require that the inner
|
||||
service's error type implements `Into<Box<dyn std::error::Error + Send +
|
||||
Sync>>`. Luckily most errors automatically satisfies that so it won't require
|
||||
users to write any additional code. We're using `Into` for the trait bound
|
||||
rather than `From` as recommend by the [standard
|
||||
library](https://doc.rust-lang.org/stable/std/convert/trait.From.html).
|
||||
|
||||
The code for our error type looks like this:
|
||||
|
||||
```rust
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TimeoutError(());
|
||||
|
||||
impl fmt::Display for TimeoutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.pad("request timed out")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TimeoutError {}
|
||||
```
|
||||
|
||||
We add a private field to `TimeoutError` such that users outside of Tower cannot
|
||||
construct their own `TimeoutError`. They can only be obtained through our
|
||||
middleware.
|
||||
|
||||
`Box<dyn std::error::Error + Send + Sync>` is also quite a mouthful so
|
||||
lets define a type alias for it:
|
||||
|
||||
```rust
|
||||
// This also exists as `tower::BoxError`
|
||||
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
```
|
||||
|
||||
Our future implementation now becomes:
|
||||
|
||||
```rust
|
||||
impl<F, Response, Error> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response, Error>>,
|
||||
// Require that the inner service's error can be converted into a `BoxError`.
|
||||
Error: Into<BoxError>,
|
||||
{
|
||||
type Output = Result<
|
||||
Response,
|
||||
// The error type of `ResponseFuture` is now `BoxError`.
|
||||
BoxError,
|
||||
>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
match this.response_future.poll(cx) {
|
||||
Poll::Ready(result) => {
|
||||
// Use `map_err` to convert the error type.
|
||||
let result = result.map_err(Into::into);
|
||||
return Poll::Ready(result);
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match this.sleep.poll(cx) {
|
||||
Poll::Ready(()) => {
|
||||
// Construct and return a timeout error.
|
||||
let error = Box::new(TimeoutError(()));
|
||||
return Poll::Ready(Err(error));
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally we have to revisit our `Service` implementation and update it to also
|
||||
use `BoxError`:
|
||||
|
||||
```rust
|
||||
impl<S, Request> Service<Request> for Timeout<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
// Same trait bound like we had on `impl Future for ResponseFuture`.
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
type Response = S::Response;
|
||||
// The error type of `Timeout` is now `BoxError`.
|
||||
type Error = BoxError;
|
||||
type Future = ResponseFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
// Have to map the error type here as well.
|
||||
self.inner.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let response_future = self.inner.call(request);
|
||||
let sleep = tokio::time::sleep(self.timeout);
|
||||
|
||||
ResponseFuture {
|
||||
response_future,
|
||||
sleep,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
That's it! We've now successfully implemented the `Timeout` middleware as it
|
||||
exists in Tower today.
|
||||
|
||||
Our final implementation is:
|
||||
|
||||
```rust
|
||||
use pin_project::pin_project;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::time::Sleep;
|
||||
use tower::Service;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Timeout<S> {
|
||||
inner: S,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<S> Timeout<S> {
|
||||
fn new(inner: S, timeout: Duration) -> Self {
|
||||
Timeout { inner, timeout }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Request> Service<Request> for Timeout<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<BoxError>,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = BoxError;
|
||||
type Future = ResponseFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx).map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let response_future = self.inner.call(request);
|
||||
let sleep = tokio::time::sleep(self.timeout);
|
||||
|
||||
ResponseFuture {
|
||||
response_future,
|
||||
sleep,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
struct ResponseFuture<F> {
|
||||
#[pin]
|
||||
response_future: F,
|
||||
#[pin]
|
||||
sleep: Sleep,
|
||||
}
|
||||
|
||||
impl<F, Response, Error> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future<Output = Result<Response, Error>>,
|
||||
Error: Into<BoxError>,
|
||||
{
|
||||
type Output = Result<Response, BoxError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
|
||||
match this.response_future.poll(cx) {
|
||||
Poll::Ready(result) => {
|
||||
let result = result.map_err(Into::into);
|
||||
return Poll::Ready(result);
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
match this.sleep.poll(cx) {
|
||||
Poll::Ready(()) => {
|
||||
let error = Box::new(TimeoutError(()));
|
||||
return Poll::Ready(Err(error));
|
||||
}
|
||||
Poll::Pending => {}
|
||||
}
|
||||
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TimeoutError(());
|
||||
|
||||
impl fmt::Display for TimeoutError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.pad("request timed out")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TimeoutError {}
|
||||
|
||||
type BoxError = Box<dyn std::error::Error + Send + Sync>;
|
||||
```
|
||||
|
||||
You can find the code in Tower [here][timeout-in-tower].
|
||||
|
||||
The pattern of implementing `Service` for some type that wraps another `Service`
|
||||
and returning a `Future` that wraps another `Future` is how most Tower
|
||||
middleware work.
|
||||
|
||||
Some other good examples are:
|
||||
|
||||
- [`ConcurrencyLimit`]: Limit the max number of requests being concurrently processed.
|
||||
- [`LoadShed`]: For shedding load when inner services aren't ready.
|
||||
- [`Steer`]: Routing between services.
|
||||
|
||||
With this you should be fully equipped to write robust production ready
|
||||
middleware. If you want more practice here are some exercises to play with:
|
||||
|
||||
- Implement our timeout middleware but using [`tokio::time::timeout`] instead of
|
||||
`sleep`.
|
||||
- Implement `Service` adapters similar to `Result::map` and `Result::map_err`
|
||||
that transforms the request, response, or error using a closure given by the
|
||||
user.
|
||||
- Implement [`ConcurrencyLimit`]. Hint: You're going to need [`PollSemaphore`] to
|
||||
implement `poll_ready`.
|
||||
|
||||
If you have questions you're welcome to post in `#tower` in the [Tokio Discord
|
||||
server][discord].
|
||||
|
||||
[^1]: The Rust compiler teams plans to add a feature called ["`impl Trait` in
|
||||
type aliases"](https://github.com/rust-lang/rust/issues/63063) which would
|
||||
allow us to return `impl Future` from `call` but for now it isn't possible.
|
||||
|
||||
[invent]: https://tokio.rs/blog/2021-05-14-inventing-the-service-trait
|
||||
[`Service`]: https://docs.rs/tower/latest/tower/trait.Service.html
|
||||
[`tower::timeout::Timeout`]: https://docs.rs/tower/latest/tower/timeout/struct.Timeout.html
|
||||
[`Pin`]: https://doc.rust-lang.org/stable/std/pin/struct.Pin.html
|
||||
[pin]: https://www.youtube.com/watch?v=DkMwYxfSYNQ
|
||||
[pin-project]: https://crates.io/crates/pin-project
|
||||
[timeout-in-tower]: https://github.com/tower-rs/tower/blob/master/tower/src/timeout/mod.rs
|
||||
[`ConcurrencyLimit`]: https://github.com/tower-rs/tower/blob/master/tower/src/limit/concurrency/service.rs
|
||||
[`LoadShed`]: https://github.com/tower-rs/tower/blob/master/tower/src/load_shed/mod.rs
|
||||
[`Steer`]: https://github.com/tower-rs/tower/blob/master/tower/src/steer/mod.rs
|
||||
[`tokio::time::timeout`]: https://docs.rs/tokio/latest/tokio/time/fn.timeout.html
|
||||
[`tokio::time::sleep`]: https://docs.rs/tokio/latest/tokio/time/fn.sleep.html
|
||||
[`PollSemaphore`]: https://docs.rs/tokio-util/latest/tokio_util/sync/struct.PollSemaphore.html
|
||||
[discord]: https://discord.gg/tokio
|
||||
[`Unpin`]: https://doc.rust-lang.org/stable/std/marker/trait.Unpin.html
|
||||
[rust-guidelines]: https://rust-lang.github.io/api-guidelines/future-proofing.html#c-struct-bounds
|
8
netlify.toml
Normal file
8
netlify.toml
Normal file
@ -0,0 +1,8 @@
|
||||
[build]
|
||||
command = "rustup install nightly --profile minimal && cargo doc --features=full --no-deps && cp -r target/doc _netlify_out"
|
||||
environment = { RUSTDOCFLAGS= "--cfg docsrs" }
|
||||
publish = "_netlify_out"
|
||||
|
||||
[[redirects]]
|
||||
from = "/"
|
||||
to = "/tower"
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (unreleased)
|
||||
|
||||
- Initial release
|
@ -1,44 +0,0 @@
|
||||
[package]
|
||||
name = "tower-balance"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-balance/0.1.0"
|
||||
description = """
|
||||
Balance load across a set of uniform services.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.26"
|
||||
indexmap = "1.0.2"
|
||||
log = "0.4.1"
|
||||
rand = "0.6.5"
|
||||
tokio-timer = "0.2.4"
|
||||
tower-service = "0.2.0"
|
||||
tower-discover = "0.1.0"
|
||||
tower-util = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
log = "0.4.1"
|
||||
env_logger = { version = "0.5.3", default-features = false }
|
||||
hdrsample = "6.0"
|
||||
quickcheck = { version = "0.6", default-features = false }
|
||||
tokio = "0.1.7"
|
||||
tokio-executor = "0.1.2"
|
||||
tower = { version = "0.1", path = "../tower" }
|
||||
tower-buffer = { version = "0.1", path = "../tower-buffer" }
|
||||
tower-limit = { version = "0.1", path = "../tower-limit" }
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,13 +0,0 @@
|
||||
# Tower Balance
|
||||
|
||||
Balance load across a set of uniform services.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,235 +0,0 @@
|
||||
//! Exercises load balancers with mocked services.
|
||||
|
||||
use env_logger;
|
||||
use futures::{future, stream, Future, Stream};
|
||||
use hdrsample::Histogram;
|
||||
use rand::{self, Rng};
|
||||
use std::time::{Duration, Instant};
|
||||
use tokio::{runtime, timer};
|
||||
use tower::{discover::Discover, limit::concurrency::ConcurrencyLimit, Service, ServiceExt};
|
||||
use tower_balance as lb;
|
||||
|
||||
const REQUESTS: usize = 50_000;
|
||||
const CONCURRENCY: usize = 500;
|
||||
const DEFAULT_RTT: Duration = Duration::from_millis(30);
|
||||
static ENDPOINT_CAPACITY: usize = CONCURRENCY;
|
||||
static MAX_ENDPOINT_LATENCIES: [Duration; 10] = [
|
||||
Duration::from_millis(1),
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(10),
|
||||
Duration::from_millis(10),
|
||||
Duration::from_millis(10),
|
||||
Duration::from_millis(100),
|
||||
Duration::from_millis(100),
|
||||
Duration::from_millis(100),
|
||||
Duration::from_millis(500),
|
||||
Duration::from_millis(1000),
|
||||
];
|
||||
static WEIGHTS: [f64; 10] = [1.0, 1.0, 1.0, 0.5, 1.5, 0.5, 1.5, 1.0, 1.0, 1.0];
|
||||
|
||||
struct Summary {
|
||||
latencies: Histogram<u64>,
|
||||
start: Instant,
|
||||
count_by_instance: [usize; 10],
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
println!("REQUESTS={}", REQUESTS);
|
||||
println!("CONCURRENCY={}", CONCURRENCY);
|
||||
println!("ENDPOINT_CAPACITY={}", ENDPOINT_CAPACITY);
|
||||
print!("MAX_ENDPOINT_LATENCIES=[");
|
||||
for max in &MAX_ENDPOINT_LATENCIES {
|
||||
let l = max.as_secs() * 1_000 + u64::from(max.subsec_nanos() / 1_000 / 1_000);
|
||||
print!("{}ms, ", l);
|
||||
}
|
||||
println!("]");
|
||||
print!("WEIGHTS=[");
|
||||
for w in &WEIGHTS {
|
||||
print!("{}, ", w);
|
||||
}
|
||||
println!("]");
|
||||
|
||||
let mut rt = runtime::Runtime::new().unwrap();
|
||||
|
||||
// Show weighted behavior first...
|
||||
|
||||
let fut = future::lazy(move || {
|
||||
let decay = Duration::from_secs(10);
|
||||
let d = gen_disco();
|
||||
let pe = lb::Balance::p2c(lb::WithWeighted::from(lb::load::WithPeakEwma::new(
|
||||
d,
|
||||
DEFAULT_RTT,
|
||||
decay,
|
||||
lb::load::NoInstrument,
|
||||
)));
|
||||
run("P2C+PeakEWMA w/ weights", pe)
|
||||
});
|
||||
|
||||
let fut = fut.then(move |_| {
|
||||
let d = gen_disco();
|
||||
let ll = lb::Balance::p2c(lb::WithWeighted::from(lb::load::WithPendingRequests::new(
|
||||
d,
|
||||
lb::load::NoInstrument,
|
||||
)));
|
||||
run("P2C+LeastLoaded w/ weights", ll)
|
||||
});
|
||||
|
||||
// Then run through standard comparisons...
|
||||
|
||||
let fut = fut.then(move |_| {
|
||||
let decay = Duration::from_secs(10);
|
||||
let d = gen_disco();
|
||||
let pe = lb::Balance::p2c(lb::load::WithPeakEwma::new(
|
||||
d,
|
||||
DEFAULT_RTT,
|
||||
decay,
|
||||
lb::load::NoInstrument,
|
||||
));
|
||||
run("P2C+PeakEWMA", pe)
|
||||
});
|
||||
|
||||
let fut = fut.then(move |_| {
|
||||
let d = gen_disco();
|
||||
let ll = lb::Balance::p2c(lb::load::WithPendingRequests::new(
|
||||
d,
|
||||
lb::load::NoInstrument,
|
||||
));
|
||||
run("P2C+LeastLoaded", ll)
|
||||
});
|
||||
|
||||
let fut = fut.and_then(move |_| {
|
||||
let rr = lb::Balance::round_robin(gen_disco());
|
||||
run("RoundRobin", rr)
|
||||
});
|
||||
|
||||
rt.spawn(fut);
|
||||
rt.shutdown_on_idle().wait().unwrap();
|
||||
}
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
fn gen_disco() -> impl Discover<
|
||||
Key = usize,
|
||||
Error = impl Into<Error>,
|
||||
Service = lb::Weighted<
|
||||
impl Service<Req, Response = Rsp, Error = Error, Future = impl Send> + Send,
|
||||
>,
|
||||
> + Send {
|
||||
let svcs = MAX_ENDPOINT_LATENCIES
|
||||
.iter()
|
||||
.zip(WEIGHTS.iter())
|
||||
.enumerate()
|
||||
.map(|(instance, (latency, weight))| {
|
||||
let svc = tower::service_fn(move |_| {
|
||||
let start = Instant::now();
|
||||
|
||||
let maxms = u64::from(latency.subsec_nanos() / 1_000 / 1_000)
|
||||
.saturating_add(latency.as_secs().saturating_mul(1_000));
|
||||
let latency = Duration::from_millis(rand::thread_rng().gen_range(0, maxms));
|
||||
|
||||
timer::Delay::new(start + latency).map(move |_| {
|
||||
let latency = start.elapsed();
|
||||
Rsp { latency, instance }
|
||||
})
|
||||
});
|
||||
|
||||
let svc = ConcurrencyLimit::new(svc, ENDPOINT_CAPACITY);
|
||||
lb::Weighted::new(svc, *weight)
|
||||
});
|
||||
tower_discover::ServiceList::new(svcs)
|
||||
}
|
||||
|
||||
fn run<D, C>(name: &'static str, lb: lb::Balance<D, C>) -> impl Future<Item = (), Error = ()>
|
||||
where
|
||||
D: Discover + Send + 'static,
|
||||
D::Error: Into<Error>,
|
||||
D::Key: Send,
|
||||
D::Service: Service<Req, Response = Rsp, Error = Error> + Send,
|
||||
<D::Service as Service<Req>>::Future: Send,
|
||||
C: lb::Choose<D::Key, D::Service> + Send + 'static,
|
||||
{
|
||||
println!("{}", name);
|
||||
|
||||
let requests = stream::repeat::<_, Error>(Req).take(REQUESTS as u64);
|
||||
let service = ConcurrencyLimit::new(lb, CONCURRENCY);
|
||||
let responses = service.call_all(requests).unordered();
|
||||
|
||||
compute_histo(responses).map(|s| s.report()).map_err(|_| {})
|
||||
}
|
||||
|
||||
fn compute_histo<S>(times: S) -> impl Future<Item = Summary, Error = Error> + 'static
|
||||
where
|
||||
S: Stream<Item = Rsp, Error = Error> + 'static,
|
||||
{
|
||||
times.fold(Summary::new(), |mut summary, rsp| {
|
||||
summary.count(rsp);
|
||||
Ok(summary) as Result<_, Error>
|
||||
})
|
||||
}
|
||||
|
||||
impl Summary {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
// The max delay is 2000ms. At 3 significant figures.
|
||||
latencies: Histogram::<u64>::new_with_max(3_000, 3).unwrap(),
|
||||
start: Instant::now(),
|
||||
count_by_instance: [0; 10],
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&mut self, rsp: Rsp) {
|
||||
let ms = rsp.latency.as_secs() * 1_000;
|
||||
let ms = ms + u64::from(rsp.latency.subsec_nanos()) / 1_000 / 1_000;
|
||||
self.latencies += ms;
|
||||
self.count_by_instance[rsp.instance] += 1;
|
||||
}
|
||||
|
||||
fn report(&self) {
|
||||
let mut total = 0;
|
||||
for c in &self.count_by_instance {
|
||||
total += c;
|
||||
}
|
||||
for (i, c) in self.count_by_instance.into_iter().enumerate() {
|
||||
let p = *c as f64 / total as f64 * 100.0;
|
||||
println!(" [{:02}] {:>5.01}%", i, p);
|
||||
}
|
||||
|
||||
println!(" wall {:4}s", self.start.elapsed().as_secs());
|
||||
|
||||
if self.latencies.len() < 2 {
|
||||
return;
|
||||
}
|
||||
println!(" p50 {:4}ms", self.latencies.value_at_quantile(0.5));
|
||||
|
||||
if self.latencies.len() < 10 {
|
||||
return;
|
||||
}
|
||||
println!(" p90 {:4}ms", self.latencies.value_at_quantile(0.9));
|
||||
|
||||
if self.latencies.len() < 50 {
|
||||
return;
|
||||
}
|
||||
println!(" p95 {:4}ms", self.latencies.value_at_quantile(0.95));
|
||||
|
||||
if self.latencies.len() < 100 {
|
||||
return;
|
||||
}
|
||||
println!(" p99 {:4}ms", self.latencies.value_at_quantile(0.99));
|
||||
|
||||
if self.latencies.len() < 1000 {
|
||||
return;
|
||||
}
|
||||
println!(" p999 {:4}ms", self.latencies.value_at_quantile(0.999));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Req;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Rsp {
|
||||
latency: Duration,
|
||||
instance: usize,
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
use indexmap::IndexMap;
|
||||
|
||||
mod p2c;
|
||||
mod round_robin;
|
||||
|
||||
pub use self::{p2c::PowerOfTwoChoices, round_robin::RoundRobin};
|
||||
|
||||
/// A strategy for choosing nodes.
|
||||
// TODO hide `K`
|
||||
pub trait Choose<K, N> {
|
||||
/// Returns the index of a replica to be used next.
|
||||
///
|
||||
/// `replicas` cannot be empty, so this function must always return a valid index on
|
||||
/// [0, replicas.len()-1].
|
||||
fn choose(&mut self, replicas: Replicas<K, N>) -> usize;
|
||||
}
|
||||
|
||||
/// Creates a `Replicas` if there are two or more services.
|
||||
///
|
||||
pub(crate) fn replicas<K, S>(inner: &IndexMap<K, S>) -> Result<Replicas<K, S>, TooFew> {
|
||||
if inner.len() < 2 {
|
||||
return Err(TooFew);
|
||||
}
|
||||
|
||||
Ok(Replicas(inner))
|
||||
}
|
||||
|
||||
/// Indicates that there were not at least two services.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct TooFew;
|
||||
|
||||
/// Holds two or more services.
|
||||
// TODO hide `K`
|
||||
pub struct Replicas<'a, K, S>(&'a IndexMap<K, S>);
|
||||
|
||||
impl<K, S> Replicas<'_, K, S> {
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, S> ::std::ops::Index<usize> for Replicas<'_, K, S> {
|
||||
type Output = S;
|
||||
|
||||
fn index(&self, idx: usize) -> &Self::Output {
|
||||
let (_, service) = self.0.get_index(idx).expect("out of bounds");
|
||||
service
|
||||
}
|
||||
}
|
@ -1,108 +0,0 @@
|
||||
use log::trace;
|
||||
use rand::{rngs::SmallRng, FromEntropy, Rng};
|
||||
|
||||
use crate::{
|
||||
choose::{Choose, Replicas},
|
||||
Load,
|
||||
};
|
||||
|
||||
/// Chooses nodes using the [Power of Two Choices][p2c].
|
||||
///
|
||||
/// This is a load-aware strategy, so this may only be used to choose over services that
|
||||
/// implement `Load`.
|
||||
///
|
||||
/// As described in the [Finagle Guide][finagle]:
|
||||
/// > The algorithm randomly picks two nodes from the set of ready endpoints and selects
|
||||
/// > the least loaded of the two. By repeatedly using this strategy, we can expect a
|
||||
/// > manageable upper bound on the maximum load of any server.
|
||||
/// >
|
||||
/// > The maximum load variance between any two servers is bound by `ln(ln(n))` where `n`
|
||||
/// > is the number of servers in the cluster.
|
||||
///
|
||||
/// [finagle]: https://twitter.github.io/finagle/guide/Clients.html#power-of-two-choices-p2c-least-loaded
|
||||
/// [p2c]: http://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf
|
||||
#[derive(Debug)]
|
||||
pub struct PowerOfTwoChoices {
|
||||
rng: SmallRng,
|
||||
}
|
||||
|
||||
// ==== impl PowerOfTwoChoices ====
|
||||
|
||||
impl Default for PowerOfTwoChoices {
|
||||
fn default() -> Self {
|
||||
Self::new(SmallRng::from_entropy())
|
||||
}
|
||||
}
|
||||
|
||||
impl PowerOfTwoChoices {
|
||||
pub fn new(rng: SmallRng) -> Self {
|
||||
Self { rng }
|
||||
}
|
||||
|
||||
/// Returns two random, distinct indices into `ready`.
|
||||
fn random_pair(&mut self, len: usize) -> (usize, usize) {
|
||||
debug_assert!(len >= 2);
|
||||
|
||||
// Choose a random number on [0, len-1].
|
||||
let idx0 = self.rng.gen::<usize>() % len;
|
||||
|
||||
let idx1 = {
|
||||
// Choose a random number on [1, len-1].
|
||||
let delta = (self.rng.gen::<usize>() % (len - 1)) + 1;
|
||||
// Add it to `idx0` and then mod on `len` to produce a value on
|
||||
// [idx0+1, len-1] or [0, idx0-1].
|
||||
(idx0 + delta) % len
|
||||
};
|
||||
|
||||
debug_assert!(idx0 != idx1, "random pair must be distinct");
|
||||
return (idx0, idx1);
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, L> Choose<K, L> for PowerOfTwoChoices
|
||||
where
|
||||
L: Load,
|
||||
L::Metric: PartialOrd + ::std::fmt::Debug,
|
||||
{
|
||||
/// Chooses two distinct nodes at random and compares their load.
|
||||
///
|
||||
/// Returns the index of the lesser-loaded node.
|
||||
fn choose(&mut self, replicas: Replicas<K, L>) -> usize {
|
||||
let (a, b) = self.random_pair(replicas.len());
|
||||
|
||||
let a_load = replicas[a].load();
|
||||
let b_load = replicas[b].load();
|
||||
trace!(
|
||||
"choose node[{a}]={a_load:?} node[{b}]={b_load:?}",
|
||||
a = a,
|
||||
b = b,
|
||||
a_load = a_load,
|
||||
b_load = b_load
|
||||
);
|
||||
if a_load <= b_load {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use quickcheck::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
quickcheck! {
|
||||
fn distinct_random_pairs(n: usize) -> TestResult {
|
||||
if n < 2 {
|
||||
return TestResult::discard();
|
||||
}
|
||||
|
||||
let mut p2c = PowerOfTwoChoices::default();
|
||||
|
||||
let (a, b) = p2c.random_pair(n);
|
||||
TestResult::from_bool(a != b)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use crate::choose::{Choose, Replicas};
|
||||
|
||||
/// Chooses nodes sequentially.
|
||||
///
|
||||
/// This strategy is load-agnostic and may therefore be used to choose over any type of
|
||||
/// service.
|
||||
///
|
||||
/// Note that ordering is not strictly enforced, especially when services are removed by
|
||||
/// the balancer.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct RoundRobin {
|
||||
/// References the index of the next node to be used.
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl<K, N> Choose<K, N> for RoundRobin {
|
||||
fn choose(&mut self, nodes: Replicas<K, N>) -> usize {
|
||||
let len = nodes.len();
|
||||
let idx = self.pos % len;
|
||||
self.pos = (idx + 1) % len;
|
||||
idx
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
pub(crate) type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Balance(pub(crate) Error);
|
||||
|
||||
impl fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "load balancing discover error: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Balance {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
Some(&*self.0)
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use futures::{Future, Poll};
|
||||
|
||||
pub struct ResponseFuture<F>(F);
|
||||
|
||||
impl<F> ResponseFuture<F> {
|
||||
pub(crate) fn new(future: F) -> ResponseFuture<F> {
|
||||
ResponseFuture(future)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future,
|
||||
F::Error: Into<Error>,
|
||||
{
|
||||
type Item = F::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.0.poll().map_err(Into::into)
|
||||
}
|
||||
}
|
@ -1,344 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-balance/0.1.0")]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
#[cfg(test)]
|
||||
extern crate quickcheck;
|
||||
|
||||
use futures::{Async, Poll};
|
||||
use indexmap::IndexMap;
|
||||
use log::{debug, trace};
|
||||
use rand::{rngs::SmallRng, SeedableRng};
|
||||
use std::fmt;
|
||||
use tower_discover::Discover;
|
||||
use tower_service::Service;
|
||||
|
||||
pub mod choose;
|
||||
pub mod error;
|
||||
pub mod future;
|
||||
pub mod load;
|
||||
pub mod pool;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub use self::{
|
||||
choose::Choose,
|
||||
load::{
|
||||
weight::{HasWeight, Weight, Weighted, WithWeighted},
|
||||
Load,
|
||||
},
|
||||
pool::Pool,
|
||||
};
|
||||
|
||||
use self::{error::Error, future::ResponseFuture};
|
||||
|
||||
/// Balances requests across a set of inner services.
|
||||
#[derive(Debug)]
|
||||
pub struct Balance<D: Discover, C> {
|
||||
/// Provides endpoints from service discovery.
|
||||
discover: D,
|
||||
|
||||
/// Determines which endpoint is ready to be used next.
|
||||
choose: C,
|
||||
|
||||
/// Holds an index into `ready`, indicating the service that has been chosen to
|
||||
/// dispatch the next request.
|
||||
chosen_ready_index: Option<usize>,
|
||||
|
||||
/// Holds an index into `ready`, indicating the service that dispatched the last
|
||||
/// request.
|
||||
dispatched_ready_index: Option<usize>,
|
||||
|
||||
/// Holds all possibly-available endpoints (i.e. from `discover`).
|
||||
ready: IndexMap<D::Key, D::Service>,
|
||||
|
||||
/// Newly-added endpoints that have not yet become ready.
|
||||
not_ready: IndexMap<D::Key, D::Service>,
|
||||
}
|
||||
|
||||
// ===== impl Balance =====
|
||||
|
||||
impl<D> Balance<D, choose::PowerOfTwoChoices>
|
||||
where
|
||||
D: Discover,
|
||||
D::Service: Load,
|
||||
<D::Service as Load>::Metric: PartialOrd + fmt::Debug,
|
||||
{
|
||||
/// Chooses services using the [Power of Two Choices][p2c].
|
||||
///
|
||||
/// This configuration is prefered when a load metric is known.
|
||||
///
|
||||
/// As described in the [Finagle Guide][finagle]:
|
||||
///
|
||||
/// > The algorithm randomly picks two services from the set of ready endpoints and
|
||||
/// > selects the least loaded of the two. By repeatedly using this strategy, we can
|
||||
/// > expect a manageable upper bound on the maximum load of any server.
|
||||
/// >
|
||||
/// > The maximum load variance between any two servers is bound by `ln(ln(n))` where
|
||||
/// > `n` is the number of servers in the cluster.
|
||||
///
|
||||
/// [finagle]: https://twitter.github.io/finagle/guide/Clients.html#power-of-two-choices-p2c-least-loaded
|
||||
/// [p2c]: http://www.eecs.harvard.edu/~michaelm/postscripts/handbook2001.pdf
|
||||
pub fn p2c(discover: D) -> Self {
|
||||
Self::new(discover, choose::PowerOfTwoChoices::default())
|
||||
}
|
||||
|
||||
/// Initializes a P2C load balancer from the provided randomization source.
|
||||
///
|
||||
/// This may be preferable when an application instantiates many balancers.
|
||||
pub fn p2c_with_rng<R: rand::Rng>(discover: D, rng: &mut R) -> Result<Self, rand::Error> {
|
||||
let rng = SmallRng::from_rng(rng)?;
|
||||
Ok(Self::new(discover, choose::PowerOfTwoChoices::new(rng)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<D: Discover> Balance<D, choose::RoundRobin> {
|
||||
/// Attempts to choose services sequentially.
|
||||
///
|
||||
/// This configuration is prefered when no load metric is known.
|
||||
pub fn round_robin(discover: D) -> Self {
|
||||
Self::new(discover, choose::RoundRobin::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, C> Balance<D, C>
|
||||
where
|
||||
D: Discover,
|
||||
C: Choose<D::Key, D::Service>,
|
||||
{
|
||||
/// Creates a new balancer.
|
||||
pub fn new(discover: D, choose: C) -> Self {
|
||||
Self {
|
||||
discover,
|
||||
choose,
|
||||
chosen_ready_index: None,
|
||||
dispatched_ready_index: None,
|
||||
ready: IndexMap::default(),
|
||||
not_ready: IndexMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true iff there are ready services.
|
||||
///
|
||||
/// This is not authoritative and is only useful after `poll_ready` has been called.
|
||||
pub fn is_ready(&self) -> bool {
|
||||
!self.ready.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true iff there are no ready services.
|
||||
///
|
||||
/// This is not authoritative and is only useful after `poll_ready` has been called.
|
||||
pub fn is_not_ready(&self) -> bool {
|
||||
self.ready.is_empty()
|
||||
}
|
||||
|
||||
/// Counts the number of services considered to be ready.
|
||||
///
|
||||
/// This is not authoritative and is only useful after `poll_ready` has been called.
|
||||
pub fn num_ready(&self) -> usize {
|
||||
self.ready.len()
|
||||
}
|
||||
|
||||
/// Counts the number of services not considered to be ready.
|
||||
///
|
||||
/// This is not authoritative and is only useful after `poll_ready` has been called.
|
||||
pub fn num_not_ready(&self) -> usize {
|
||||
self.not_ready.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, C> Balance<D, C>
|
||||
where
|
||||
D: Discover,
|
||||
D::Error: Into<Error>,
|
||||
C: Choose<D::Key, D::Service>,
|
||||
{
|
||||
/// Polls `discover` for updates, adding new items to `not_ready`.
|
||||
///
|
||||
/// Removals may alter the order of either `ready` or `not_ready`.
|
||||
fn update_from_discover(&mut self) -> Result<(), error::Balance> {
|
||||
debug!("updating from discover");
|
||||
use tower_discover::Change::*;
|
||||
|
||||
while let Async::Ready(change) =
|
||||
self.discover.poll().map_err(|e| error::Balance(e.into()))?
|
||||
{
|
||||
match change {
|
||||
Insert(key, svc) => {
|
||||
// If the `Insert`ed service is a duplicate of a service already
|
||||
// in the ready list, remove the ready service first. The new
|
||||
// service will then be inserted into the not-ready list.
|
||||
self.ready.remove(&key);
|
||||
|
||||
self.not_ready.insert(key, svc);
|
||||
}
|
||||
|
||||
Remove(key) => {
|
||||
let _ejected = match self.ready.remove(&key) {
|
||||
None => self.not_ready.remove(&key),
|
||||
Some(s) => Some(s),
|
||||
};
|
||||
// XXX is it safe to just drop the Service? Or do we need some sort of
|
||||
// graceful teardown?
|
||||
// TODO: poll_close
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls `poll_ready` on all services in `not_ready`.
|
||||
///
|
||||
/// When `poll_ready` returns ready, the service is removed from `not_ready` and inserted
|
||||
/// into `ready`, potentially altering the order of `ready` and/or `not_ready`.
|
||||
fn promote_to_ready<Request>(&mut self) -> Result<(), <D::Service as Service<Request>>::Error>
|
||||
where
|
||||
D::Service: Service<Request>,
|
||||
{
|
||||
let n = self.not_ready.len();
|
||||
if n == 0 {
|
||||
trace!("promoting to ready: not_ready is empty, skipping.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("promoting to ready: {}", n);
|
||||
// Iterate through the not-ready endpoints from right to left to prevent removals
|
||||
// from reordering services in a way that could prevent a service from being polled.
|
||||
for idx in (0..n).rev() {
|
||||
let is_ready = {
|
||||
let (_, svc) = self
|
||||
.not_ready
|
||||
.get_index_mut(idx)
|
||||
.expect("invalid not_ready index");;
|
||||
svc.poll_ready()?.is_ready()
|
||||
};
|
||||
trace!("not_ready[{:?}]: is_ready={:?};", idx, is_ready);
|
||||
if is_ready {
|
||||
debug!("not_ready[{:?}]: promoting to ready", idx);
|
||||
let (key, svc) = self
|
||||
.not_ready
|
||||
.swap_remove_index(idx)
|
||||
.expect("invalid not_ready index");
|
||||
self.ready.insert(key, svc);
|
||||
} else {
|
||||
debug!("not_ready[{:?}]: not promoting to ready", idx);
|
||||
}
|
||||
}
|
||||
|
||||
debug!("promoting to ready: done");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Polls a `ready` service or moves it to `not_ready`.
|
||||
///
|
||||
/// If the service exists in `ready` and does not poll as ready, it is moved to
|
||||
/// `not_ready`, potentially altering the order of `ready` and/or `not_ready`.
|
||||
fn poll_ready_index<Request>(
|
||||
&mut self,
|
||||
idx: usize,
|
||||
) -> Option<Poll<(), <D::Service as Service<Request>>::Error>>
|
||||
where
|
||||
D::Service: Service<Request>,
|
||||
{
|
||||
match self.ready.get_index_mut(idx) {
|
||||
None => return None,
|
||||
Some((_, svc)) => match svc.poll_ready() {
|
||||
Ok(Async::Ready(())) => return Some(Ok(Async::Ready(()))),
|
||||
Err(e) => return Some(Err(e)),
|
||||
Ok(Async::NotReady) => {}
|
||||
},
|
||||
}
|
||||
|
||||
let (key, svc) = self
|
||||
.ready
|
||||
.swap_remove_index(idx)
|
||||
.expect("invalid ready index");
|
||||
self.not_ready.insert(key, svc);
|
||||
Some(Ok(Async::NotReady))
|
||||
}
|
||||
|
||||
/// Chooses the next service to which a request will be dispatched.
|
||||
///
|
||||
/// Ensures that .
|
||||
fn choose_and_poll_ready<Request>(
|
||||
&mut self,
|
||||
) -> Poll<(), <D::Service as Service<Request>>::Error>
|
||||
where
|
||||
D::Service: Service<Request>,
|
||||
{
|
||||
loop {
|
||||
let n = self.ready.len();
|
||||
debug!("choosing from {} replicas", n);
|
||||
let idx = match n {
|
||||
0 => return Ok(Async::NotReady),
|
||||
1 => 0,
|
||||
_ => {
|
||||
let replicas = choose::replicas(&self.ready).expect("too few replicas");
|
||||
self.choose.choose(replicas)
|
||||
}
|
||||
};
|
||||
|
||||
// XXX Should we handle per-endpoint errors?
|
||||
if self
|
||||
.poll_ready_index(idx)
|
||||
.expect("invalid ready index")?
|
||||
.is_ready()
|
||||
{
|
||||
self.chosen_ready_index = Some(idx);
|
||||
return Ok(Async::Ready(()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, C, Svc, Request> Service<Request> for Balance<D, C>
|
||||
where
|
||||
D: Discover<Service = Svc>,
|
||||
D::Error: Into<Error>,
|
||||
Svc: Service<Request>,
|
||||
Svc::Error: Into<Error>,
|
||||
C: Choose<D::Key, Svc>,
|
||||
{
|
||||
type Response = Svc::Response;
|
||||
type Error = Error;
|
||||
type Future = ResponseFuture<Svc::Future>;
|
||||
|
||||
/// Prepares the balancer to process a request.
|
||||
///
|
||||
/// When `Async::Ready` is returned, `chosen_ready_index` is set with a valid index
|
||||
/// into `ready` referring to a `Service` that is ready to disptach a request.
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
// Clear before `ready` is altered.
|
||||
self.chosen_ready_index = None;
|
||||
|
||||
// Before `ready` is altered, check the readiness of the last-used service, moving it
|
||||
// to `not_ready` if appropriate.
|
||||
if let Some(idx) = self.dispatched_ready_index.take() {
|
||||
// XXX Should we handle per-endpoint errors?
|
||||
self.poll_ready_index(idx)
|
||||
.expect("invalid dispatched ready key")
|
||||
.map_err(Into::into)?;
|
||||
}
|
||||
|
||||
// Update `not_ready` and `ready`.
|
||||
self.update_from_discover()?;
|
||||
self.promote_to_ready().map_err(Into::into)?;
|
||||
|
||||
// Choose the next service to be used by `call`.
|
||||
self.choose_and_poll_ready().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let idx = self.chosen_ready_index.take().expect("not ready");
|
||||
let (_, svc) = self
|
||||
.ready
|
||||
.get_index_mut(idx)
|
||||
.expect("invalid chosen ready index");
|
||||
self.dispatched_ready_index = Some(idx);
|
||||
|
||||
let rsp = svc.call(request);
|
||||
ResponseFuture::new(rsp)
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
use futures::{try_ready, Async, Poll};
|
||||
use tower_discover::{Change, Discover};
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::Load;
|
||||
|
||||
/// Wraps a type so that `Load::load` returns a constant value.
|
||||
pub struct Constant<T, M> {
|
||||
inner: T,
|
||||
load: M,
|
||||
}
|
||||
|
||||
// ===== impl Constant =====
|
||||
|
||||
impl<T, M: Copy> Constant<T, M> {
|
||||
pub fn new(inner: T, load: M) -> Self {
|
||||
Self { inner, load }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, M: Copy + PartialOrd> Load for Constant<T, M> {
|
||||
type Metric = M;
|
||||
|
||||
fn load(&self) -> M {
|
||||
self.load
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, M, Request> Service<Request> for Constant<S, M>
|
||||
where
|
||||
S: Service<Request>,
|
||||
M: Copy,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = S::Future;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.inner.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
self.inner.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
/// Proxies `Discover` such that all changes are wrapped with a constant load.
|
||||
impl<D: Discover, M: Copy> Discover for Constant<D, M> {
|
||||
type Key = D::Key;
|
||||
type Service = Constant<D::Service, M>;
|
||||
type Error = D::Error;
|
||||
|
||||
/// Yields the next discovery change set.
|
||||
fn poll(&mut self) -> Poll<Change<D::Key, Self::Service>, D::Error> {
|
||||
use self::Change::*;
|
||||
|
||||
let change = match try_ready!(self.inner.poll()) {
|
||||
Insert(k, svc) => Insert(k, Constant::new(svc, self.load)),
|
||||
Remove(k) => Remove(k),
|
||||
};
|
||||
|
||||
Ok(Async::Ready(change))
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
use futures::{try_ready, Future, Poll};
|
||||
|
||||
/// Attaches `I`-typed instruments to `V` typed values.
|
||||
///
|
||||
/// This utility allows load metrics to have a protocol-agnostic means to track streams
|
||||
/// past their initial response future. For example, if `V` represents an HTTP response
|
||||
/// type, an implementation could add `H`-typed handles to each response's extensions to
|
||||
/// detect when the response is dropped.
|
||||
///
|
||||
/// Handles are intended to be RAII guards that primarily implement `Drop` and update load
|
||||
/// metric state as they are dropped.
|
||||
///
|
||||
/// A base `impl<H, V> Instrument<H, V> for NoInstrument` is provided to drop the handle
|
||||
/// immediately. This is appropriate when a response is discrete and cannot comprise
|
||||
/// multiple messages.
|
||||
///
|
||||
/// In many cases, the `Output` type is simply `V`. However, `Instrument` may alter the
|
||||
/// typein order to instrument it appropriately. For example, an HTTP Instrument may
|
||||
/// modify the body type: so an `Instrument` that takes values of type `http::Response<A>`
|
||||
/// may output values of type `http::Response<B>`.
|
||||
pub trait Instrument<H, V>: Clone {
|
||||
type Output;
|
||||
|
||||
/// Attaches an `H`-typed handle to a `V`-typed value.
|
||||
fn instrument(&self, handle: H, value: V) -> Self::Output;
|
||||
}
|
||||
|
||||
/// A `Instrument` implementation that drops each instrument immediately.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct NoInstrument;
|
||||
|
||||
/// Attaches a `I`-typed instruments to the result of an `F`-typed `Future`.
|
||||
#[derive(Debug)]
|
||||
pub struct InstrumentFuture<F, I, H>
|
||||
where
|
||||
F: Future,
|
||||
I: Instrument<H, F::Item>,
|
||||
{
|
||||
future: F,
|
||||
handle: Option<H>,
|
||||
instrument: I,
|
||||
}
|
||||
|
||||
// ===== impl InstrumentFuture =====
|
||||
|
||||
impl<F, I, H> InstrumentFuture<F, I, H>
|
||||
where
|
||||
F: Future,
|
||||
I: Instrument<H, F::Item>,
|
||||
{
|
||||
pub fn new(instrument: I, handle: H, future: F) -> Self {
|
||||
InstrumentFuture {
|
||||
future,
|
||||
instrument,
|
||||
handle: Some(handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, I, H> Future for InstrumentFuture<F, I, H>
|
||||
where
|
||||
F: Future,
|
||||
I: Instrument<H, F::Item>,
|
||||
{
|
||||
type Item = I::Output;
|
||||
type Error = F::Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
let rsp = try_ready!(self.future.poll());
|
||||
let h = self.handle.take().expect("handle");
|
||||
Ok(self.instrument.instrument(h, rsp).into())
|
||||
}
|
||||
}
|
||||
|
||||
// ===== NoInstrument =====
|
||||
|
||||
impl<H, V> Instrument<H, V> for NoInstrument {
|
||||
type Output = V;
|
||||
|
||||
fn instrument(&self, handle: H, value: V) -> V {
|
||||
drop(handle);
|
||||
value
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
mod constant;
|
||||
mod instrument;
|
||||
pub mod peak_ewma;
|
||||
pub mod pending_requests;
|
||||
pub(crate) mod weight;
|
||||
|
||||
pub use self::{
|
||||
constant::Constant,
|
||||
instrument::{Instrument, InstrumentFuture, NoInstrument},
|
||||
peak_ewma::{PeakEwma, WithPeakEwma},
|
||||
pending_requests::{PendingRequests, WithPendingRequests},
|
||||
};
|
||||
|
||||
/// Exposes a load metric.
|
||||
///
|
||||
/// Implementors should choose load values so that lesser-loaded instances return lesser
|
||||
/// values than higher-load instances.
|
||||
pub trait Load {
|
||||
type Metric: PartialOrd;
|
||||
|
||||
fn load(&self) -> Self::Metric;
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
use futures::{try_ready, Async, Poll};
|
||||
use std::{ops, sync::Arc};
|
||||
use tower_discover::{Change, Discover};
|
||||
use tower_service::Service;
|
||||
|
||||
use super::{Instrument, InstrumentFuture, NoInstrument};
|
||||
use crate::{HasWeight, Load, Weight};
|
||||
|
||||
/// Expresses load based on the number of currently-pending requests.
|
||||
#[derive(Debug)]
|
||||
pub struct PendingRequests<S, I = NoInstrument> {
|
||||
service: S,
|
||||
ref_count: RefCount,
|
||||
instrument: I,
|
||||
}
|
||||
|
||||
/// Shared between instances of `PendingRequests` and `Handle` to track active
|
||||
/// references.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RefCount(Arc<()>);
|
||||
|
||||
/// Wraps `inner`'s services with `PendingRequests`.
|
||||
#[derive(Debug)]
|
||||
pub struct WithPendingRequests<D, I = NoInstrument> {
|
||||
discover: D,
|
||||
instrument: I,
|
||||
}
|
||||
|
||||
/// Represents the number of currently-pending requests to a given service.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub struct Count(usize);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Handle(RefCount);
|
||||
|
||||
// ===== impl Count =====
|
||||
|
||||
impl ops::Div<Weight> for Count {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, weight: Weight) -> f64 {
|
||||
self.0 / weight
|
||||
}
|
||||
}
|
||||
|
||||
// ===== impl PendingRequests =====
|
||||
|
||||
impl<S, I> PendingRequests<S, I> {
|
||||
fn new(service: S, instrument: I) -> Self {
|
||||
Self {
|
||||
service,
|
||||
instrument,
|
||||
ref_count: RefCount::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&self) -> Handle {
|
||||
Handle(self.ref_count.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, I> Load for PendingRequests<S, I> {
|
||||
type Metric = Count;
|
||||
|
||||
fn load(&self) -> Count {
|
||||
// Count the number of references that aren't `self`.
|
||||
Count(self.ref_count.ref_count() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: HasWeight, I> HasWeight for PendingRequests<S, I> {
|
||||
fn weight(&self) -> Weight {
|
||||
self.service.weight()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, I, Request> Service<Request> for PendingRequests<S, I>
|
||||
where
|
||||
S: Service<Request>,
|
||||
I: Instrument<Handle, S::Response>,
|
||||
{
|
||||
type Response = I::Output;
|
||||
type Error = S::Error;
|
||||
type Future = InstrumentFuture<S::Future, I, Handle>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.service.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
InstrumentFuture::new(
|
||||
self.instrument.clone(),
|
||||
self.handle(),
|
||||
self.service.call(req),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===== impl WithPendingRequests =====
|
||||
|
||||
impl<D, I> WithPendingRequests<D, I> {
|
||||
pub fn new<Request>(discover: D, instrument: I) -> Self
|
||||
where
|
||||
D: Discover,
|
||||
D::Service: Service<Request>,
|
||||
I: Instrument<Handle, <D::Service as Service<Request>>::Response>,
|
||||
{
|
||||
Self {
|
||||
discover,
|
||||
instrument,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, I> Discover for WithPendingRequests<D, I>
|
||||
where
|
||||
D: Discover,
|
||||
I: Clone,
|
||||
{
|
||||
type Key = D::Key;
|
||||
type Service = PendingRequests<D::Service, I>;
|
||||
type Error = D::Error;
|
||||
|
||||
/// Yields the next discovery change set.
|
||||
fn poll(&mut self) -> Poll<Change<D::Key, Self::Service>, D::Error> {
|
||||
use self::Change::*;
|
||||
|
||||
let change = match try_ready!(self.discover.poll()) {
|
||||
Insert(k, svc) => Insert(k, PendingRequests::new(svc, self.instrument.clone())),
|
||||
Remove(k) => Remove(k),
|
||||
};
|
||||
|
||||
Ok(Async::Ready(change))
|
||||
}
|
||||
}
|
||||
|
||||
// ==== RefCount ====
|
||||
|
||||
impl RefCount {
|
||||
pub fn ref_count(&self) -> usize {
|
||||
Arc::strong_count(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{future, Future, Poll};
|
||||
|
||||
struct Svc;
|
||||
impl Service<()> for Svc {
|
||||
type Response = ();
|
||||
type Error = ();
|
||||
type Future = future::FutureResult<(), ()>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), ()> {
|
||||
Ok(().into())
|
||||
}
|
||||
|
||||
fn call(&mut self, (): ()) -> Self::Future {
|
||||
future::ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let mut svc = PendingRequests::new(Svc, NoInstrument);
|
||||
assert_eq!(svc.load(), Count(0));
|
||||
|
||||
let rsp0 = svc.call(());
|
||||
assert_eq!(svc.load(), Count(1));
|
||||
|
||||
let rsp1 = svc.call(());
|
||||
assert_eq!(svc.load(), Count(2));
|
||||
|
||||
let () = rsp0.wait().unwrap();
|
||||
assert_eq!(svc.load(), Count(1));
|
||||
|
||||
let () = rsp1.wait().unwrap();
|
||||
assert_eq!(svc.load(), Count(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn instrumented() {
|
||||
#[derive(Clone)]
|
||||
struct IntoHandle;
|
||||
impl Instrument<Handle, ()> for IntoHandle {
|
||||
type Output = Handle;
|
||||
fn instrument(&self, i: Handle, (): ()) -> Handle {
|
||||
i
|
||||
}
|
||||
}
|
||||
|
||||
let mut svc = PendingRequests::new(Svc, IntoHandle);
|
||||
assert_eq!(svc.load(), Count(0));
|
||||
|
||||
let rsp = svc.call(());
|
||||
assert_eq!(svc.load(), Count(1));
|
||||
let i0 = rsp.wait().unwrap();
|
||||
assert_eq!(svc.load(), Count(1));
|
||||
|
||||
let rsp = svc.call(());
|
||||
assert_eq!(svc.load(), Count(2));
|
||||
let i1 = rsp.wait().unwrap();
|
||||
assert_eq!(svc.load(), Count(2));
|
||||
|
||||
drop(i1);
|
||||
assert_eq!(svc.load(), Count(1));
|
||||
|
||||
drop(i0);
|
||||
assert_eq!(svc.load(), Count(0));
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
use futures::{try_ready, Async, Poll};
|
||||
use std::ops;
|
||||
use tower_discover::{Change, Discover};
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::Load;
|
||||
|
||||
/// A weight on [0.0, ∞].
|
||||
///
|
||||
/// Lesser-weighted nodes receive less traffic than heavier-weighted nodes.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Weight(f64);
|
||||
|
||||
/// A Service, that implements Load, that
|
||||
#[derive(Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct Weighted<T> {
|
||||
inner: T,
|
||||
weight: Weight,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WithWeighted<T>(T);
|
||||
|
||||
pub trait HasWeight {
|
||||
fn weight(&self) -> Weight;
|
||||
}
|
||||
|
||||
// === impl Weighted ===
|
||||
|
||||
impl<T: HasWeight> From<T> for Weighted<T> {
|
||||
fn from(inner: T) -> Self {
|
||||
let weight = inner.weight();
|
||||
Self { inner, weight }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> HasWeight for Weighted<T> {
|
||||
fn weight(&self) -> Weight {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Weighted<T> {
|
||||
pub fn new<W: Into<Weight>>(inner: T, w: W) -> Self {
|
||||
let weight = w.into();
|
||||
Self { inner, weight }
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (T, Weight) {
|
||||
let Self { inner, weight } = self;
|
||||
(inner, weight)
|
||||
}
|
||||
}
|
||||
|
||||
impl<L> Load for Weighted<L>
|
||||
where
|
||||
L: Load,
|
||||
L::Metric: ops::Div<Weight>,
|
||||
<L::Metric as ops::Div<Weight>>::Output: PartialOrd,
|
||||
{
|
||||
type Metric = <L::Metric as ops::Div<Weight>>::Output;
|
||||
|
||||
fn load(&self) -> Self::Metric {
|
||||
self.inner.load() / self.weight
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, S: Service<R>> Service<R> for Weighted<S> {
|
||||
type Response = S::Response;
|
||||
type Error = S::Error;
|
||||
type Future = S::Future;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.inner.poll_ready()
|
||||
}
|
||||
|
||||
fn call(&mut self, req: R) -> Self::Future {
|
||||
self.inner.call(req)
|
||||
}
|
||||
}
|
||||
|
||||
// === impl withWeight ===
|
||||
|
||||
impl<D> From<D> for WithWeighted<D>
|
||||
where
|
||||
D: Discover,
|
||||
D::Service: HasWeight,
|
||||
{
|
||||
fn from(d: D) -> Self {
|
||||
WithWeighted(d)
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Discover for WithWeighted<D>
|
||||
where
|
||||
D: Discover,
|
||||
D::Service: HasWeight,
|
||||
{
|
||||
type Key = D::Key;
|
||||
type Error = D::Error;
|
||||
type Service = Weighted<D::Service>;
|
||||
|
||||
fn poll(&mut self) -> Poll<Change<D::Key, Self::Service>, Self::Error> {
|
||||
let c = match try_ready!(self.0.poll()) {
|
||||
Change::Insert(k, svc) => Change::Insert(k, Weighted::from(svc)),
|
||||
Change::Remove(k) => Change::Remove(k),
|
||||
};
|
||||
|
||||
Ok(Async::Ready(c))
|
||||
}
|
||||
}
|
||||
|
||||
// === impl Weight ===
|
||||
|
||||
impl Weight {
|
||||
pub const MIN: Weight = Weight(0.0);
|
||||
pub const DEFAULT: Weight = Weight(1.0);
|
||||
}
|
||||
|
||||
impl Default for Weight {
|
||||
fn default() -> Self {
|
||||
Weight::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Weight {
|
||||
fn from(w: f64) -> Self {
|
||||
if w < 0.0 {
|
||||
Weight::MIN
|
||||
} else {
|
||||
Weight(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<f64> for Weight {
|
||||
fn into(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<Weight> for f64 {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, Weight(w): Weight) -> f64 {
|
||||
if w == 0.0 {
|
||||
::std::f64::INFINITY
|
||||
} else {
|
||||
self / w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Div<Weight> for usize {
|
||||
type Output = f64;
|
||||
|
||||
fn div(self, w: Weight) -> f64 {
|
||||
(self as f64) / w
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn div_min() {
|
||||
assert_eq!(10.0 / Weight::MIN, ::std::f64::INFINITY);
|
||||
assert_eq!(10 / Weight::MIN, ::std::f64::INFINITY);
|
||||
assert_eq!(0 / Weight::MIN, ::std::f64::INFINITY);
|
||||
}
|
@ -1,295 +0,0 @@
|
||||
//! This module defines a load-balanced pool of services that adds new services when load is high.
|
||||
//!
|
||||
//! The pool uses `poll_ready` as a signal indicating whether additional services should be spawned
|
||||
//! to handle the current level of load. Specifically, every time `poll_ready` on the inner service
|
||||
//! returns `Ready`, [`Pool`] consider that a 0, and every time it returns `NotReady`, [`Pool`]
|
||||
//! considers it a 1. [`Pool`] then maintains an [exponential moving
|
||||
//! average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average) over those
|
||||
//! samples, which gives an estimate of how often the underlying service has been ready when it was
|
||||
//! needed "recently" (see [`Builder::urgency`]). If the service is loaded (see
|
||||
//! [`Builder::loaded_above`]), a new service is created and added to the underlying [`Balance`].
|
||||
//! If the service is underutilized (see [`Builder::underutilized_below`]) and there are two or
|
||||
//! more services, then the latest added service is removed. In either case, the load estimate is
|
||||
//! reset to its initial value (see [`Builder::initial`] to prevent services from being rapidly
|
||||
//! added or removed.
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use super::{Balance, Choose};
|
||||
use futures::{try_ready, Async, Future, Poll};
|
||||
use tower_discover::{Change, Discover};
|
||||
use tower_service::Service;
|
||||
use tower_util::MakeService;
|
||||
|
||||
enum Load {
|
||||
/// Load is low -- remove a service instance.
|
||||
Low,
|
||||
/// Load is normal -- keep the service set as it is.
|
||||
Normal,
|
||||
/// Load is high -- add another service instance.
|
||||
High,
|
||||
}
|
||||
|
||||
/// A wrapper around `MakeService` that discovers a new service when load is high, and removes a
|
||||
/// service when load is low. See [`Pool`].
|
||||
pub struct PoolDiscoverer<MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
{
|
||||
maker: MS,
|
||||
making: Option<MS::Future>,
|
||||
target: Target,
|
||||
load: Load,
|
||||
services: usize,
|
||||
}
|
||||
|
||||
impl<MS, Target, Request> Discover for PoolDiscoverer<MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
// NOTE: these bounds should go away once MakeService adopts Box<dyn Error>
|
||||
MS::MakeError: ::std::error::Error + Send + Sync + 'static,
|
||||
MS::Error: ::std::error::Error + Send + Sync + 'static,
|
||||
Target: Clone,
|
||||
{
|
||||
type Key = usize;
|
||||
type Service = MS::Service;
|
||||
type Error = MS::MakeError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::Error> {
|
||||
if self.services == 0 && self.making.is_none() {
|
||||
self.making = Some(self.maker.make_service(self.target.clone()));
|
||||
}
|
||||
|
||||
if let Load::High = self.load {
|
||||
if self.making.is_none() {
|
||||
try_ready!(self.maker.poll_ready());
|
||||
// TODO: it'd be great if we could avoid the clone here and use, say, &Target
|
||||
self.making = Some(self.maker.make_service(self.target.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut fut) = self.making.take() {
|
||||
if let Async::Ready(s) = fut.poll()? {
|
||||
self.services += 1;
|
||||
self.load = Load::Normal;
|
||||
return Ok(Async::Ready(Change::Insert(self.services, s)));
|
||||
} else {
|
||||
self.making = Some(fut);
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
}
|
||||
|
||||
match self.load {
|
||||
Load::High => {
|
||||
unreachable!("found high load but no Service being made");
|
||||
}
|
||||
Load::Normal => Ok(Async::NotReady),
|
||||
Load::Low if self.services == 1 => Ok(Async::NotReady),
|
||||
Load::Low => {
|
||||
self.load = Load::Normal;
|
||||
let rm = self.services;
|
||||
self.services -= 1;
|
||||
Ok(Async::Ready(Change::Remove(rm)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [builder] that lets you configure how a [`Pool`] determines whether the underlying service is
|
||||
/// loaded or not. See the [module-level documentation](index.html) and the builder's methods for
|
||||
/// details.
|
||||
///
|
||||
/// [builder]: https://rust-lang-nursery.github.io/api-guidelines/type-safety.html#builders-enable-construction-of-complex-values-c-builder
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Builder {
|
||||
low: f64,
|
||||
high: f64,
|
||||
init: f64,
|
||||
alpha: f64,
|
||||
}
|
||||
|
||||
impl Default for Builder {
|
||||
fn default() -> Self {
|
||||
Builder {
|
||||
init: 0.1,
|
||||
low: 0.00001,
|
||||
high: 0.2,
|
||||
alpha: 0.03,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
/// Create a new builder with default values for all load settings.
|
||||
///
|
||||
/// If you just want to use the defaults, you can just use [`Pool::new`].
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// When the estimated load (see the [module-level docs](index.html)) drops below this
|
||||
/// threshold, and there are at least two services active, a service is removed.
|
||||
///
|
||||
/// The default value is 0.01. That is, when one in every 100 `poll_ready` calls return
|
||||
/// `NotReady`, then the underlying service is considered underutilized.
|
||||
pub fn underutilized_below(&mut self, low: f64) -> &mut Self {
|
||||
self.low = low;
|
||||
self
|
||||
}
|
||||
|
||||
/// When the estimated load (see the [module-level docs](index.html)) exceeds this
|
||||
/// threshold, and no service is currently in the process of being added, a new service is
|
||||
/// scheduled to be added to the underlying [`Balance`].
|
||||
///
|
||||
/// The default value is 0.5. That is, when every other call to `poll_ready` returns
|
||||
/// `NotReady`, then the underlying service is considered highly loaded.
|
||||
pub fn loaded_above(&mut self, high: f64) -> &mut Self {
|
||||
self.high = high;
|
||||
self
|
||||
}
|
||||
|
||||
/// The initial estimated load average.
|
||||
///
|
||||
/// This is also the value that the estimated load will be reset to whenever a service is added
|
||||
/// or removed.
|
||||
///
|
||||
/// The default value is 0.1.
|
||||
pub fn initial(&mut self, init: f64) -> &mut Self {
|
||||
self.init = init;
|
||||
self
|
||||
}
|
||||
|
||||
/// How aggressively the estimated load average is updated.
|
||||
///
|
||||
/// This is the α parameter of the formula for the [exponential moving
|
||||
/// average](https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average), and
|
||||
/// dictates how quickly new samples of the current load affect the estimated load. If the
|
||||
/// value is closer to 1, newer samples affect the load average a lot (when α is 1, the load
|
||||
/// average is immediately set to the current load). If the value is closer to 0, newer samples
|
||||
/// affect the load average very little at a time.
|
||||
///
|
||||
/// The given value is clamped to `[0,1]`.
|
||||
///
|
||||
/// The default value is 0.05, meaning, in very approximate terms, that each new load sample
|
||||
/// affects the estimated load by 5%.
|
||||
pub fn urgency(&mut self, alpha: f64) -> &mut Self {
|
||||
self.alpha = alpha.max(0.0).min(1.0);
|
||||
self
|
||||
}
|
||||
|
||||
/// See [`Pool::new`].
|
||||
pub fn build<C, MS, Target, Request>(
|
||||
&self,
|
||||
make_service: MS,
|
||||
target: Target,
|
||||
choose: C,
|
||||
) -> Pool<C, MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
MS::MakeError: ::std::error::Error + Send + Sync + 'static,
|
||||
MS::Error: ::std::error::Error + Send + Sync + 'static,
|
||||
Target: Clone,
|
||||
C: Choose<usize, MS::Service>,
|
||||
{
|
||||
let d = PoolDiscoverer {
|
||||
maker: make_service,
|
||||
making: None,
|
||||
target,
|
||||
load: Load::Normal,
|
||||
services: 0,
|
||||
};
|
||||
|
||||
Pool {
|
||||
balance: Balance::new(d, choose),
|
||||
options: *self,
|
||||
ewma: self.init,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A dynamically sized, load-balanced pool of `Service` instances.
|
||||
pub struct Pool<C, MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
MS::MakeError: ::std::error::Error + Send + Sync + 'static,
|
||||
MS::Error: ::std::error::Error + Send + Sync + 'static,
|
||||
Target: Clone,
|
||||
{
|
||||
balance: Balance<PoolDiscoverer<MS, Target, Request>, C>,
|
||||
options: Builder,
|
||||
ewma: f64,
|
||||
}
|
||||
|
||||
impl<C, MS, Target, Request> Pool<C, MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
MS::MakeError: ::std::error::Error + Send + Sync + 'static,
|
||||
MS::Error: ::std::error::Error + Send + Sync + 'static,
|
||||
Target: Clone,
|
||||
C: Choose<usize, MS::Service>,
|
||||
{
|
||||
/// Construct a new dynamically sized `Pool`.
|
||||
///
|
||||
/// If many calls to `poll_ready` return `NotReady`, `new_service` is used to construct another
|
||||
/// `Service` that is then added to the load-balanced pool. If multiple services are available,
|
||||
/// `choose` is used to determine which one to use (just as in `Balance`). If many calls to
|
||||
/// `poll_ready` succeed, the most recently added `Service` is dropped from the pool.
|
||||
pub fn new(make_service: MS, target: Target, choose: C) -> Self {
|
||||
Builder::new().build(make_service, target, choose)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, MS, Target, Request> Service<Request> for Pool<C, MS, Target, Request>
|
||||
where
|
||||
MS: MakeService<Target, Request>,
|
||||
MS::MakeError: ::std::error::Error + Send + Sync + 'static,
|
||||
MS::Error: ::std::error::Error + Send + Sync + 'static,
|
||||
Target: Clone,
|
||||
C: Choose<usize, MS::Service>,
|
||||
{
|
||||
type Response = <Balance<PoolDiscoverer<MS, Target, Request>, C> as Service<Request>>::Response;
|
||||
type Error = <Balance<PoolDiscoverer<MS, Target, Request>, C> as Service<Request>>::Error;
|
||||
type Future = <Balance<PoolDiscoverer<MS, Target, Request>, C> as Service<Request>>::Future;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
if let Async::Ready(()) = self.balance.poll_ready()? {
|
||||
// services was ready -- there are enough services
|
||||
// update ewma with a 0 sample
|
||||
self.ewma = (1.0 - self.options.alpha) * self.ewma;
|
||||
|
||||
if self.ewma < self.options.low {
|
||||
self.balance.discover.load = Load::Low;
|
||||
|
||||
if self.balance.discover.services > 1 {
|
||||
// reset EWMA so we don't immediately try to remove another service
|
||||
self.ewma = self.options.init;
|
||||
}
|
||||
} else {
|
||||
self.balance.discover.load = Load::Normal;
|
||||
}
|
||||
|
||||
Ok(Async::Ready(()))
|
||||
} else if self.balance.discover.making.is_none() {
|
||||
// no services are ready -- we're overloaded
|
||||
// update ewma with a 1 sample
|
||||
self.ewma = self.options.alpha + (1.0 - self.options.alpha) * self.ewma;
|
||||
|
||||
if self.ewma > self.options.high {
|
||||
self.balance.discover.load = Load::High;
|
||||
|
||||
// don't reset the EWMA -- in theory, poll_ready should now start returning
|
||||
// `Ready`, so we won't try to launch another service immediately.
|
||||
} else {
|
||||
self.balance.discover.load = Load::Normal;
|
||||
}
|
||||
|
||||
Ok(Async::NotReady)
|
||||
} else {
|
||||
// no services are ready, but we're already making another service!
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
Service::call(&mut self.balance, req)
|
||||
}
|
||||
}
|
@ -1,136 +0,0 @@
|
||||
use futures::{future, Async, Poll};
|
||||
use quickcheck::*;
|
||||
use std::collections::VecDeque;
|
||||
use tower_discover::Change;
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::*;
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
struct ReluctantDisco(VecDeque<Change<usize, ReluctantService>>);
|
||||
|
||||
struct ReluctantService {
|
||||
polls_until_ready: usize,
|
||||
}
|
||||
|
||||
impl Discover for ReluctantDisco {
|
||||
type Key = usize;
|
||||
type Service = ReluctantService;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::Error> {
|
||||
let r = self
|
||||
.0
|
||||
.pop_front()
|
||||
.map(Async::Ready)
|
||||
.unwrap_or(Async::NotReady);
|
||||
debug!("polling disco: {:?}", r.is_ready());
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl Service<()> for ReluctantService {
|
||||
type Response = ();
|
||||
type Error = Error;
|
||||
type Future = future::FutureResult<Self::Response, Self::Error>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
if self.polls_until_ready == 0 {
|
||||
return Ok(Async::Ready(()));
|
||||
}
|
||||
|
||||
self.polls_until_ready -= 1;
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
|
||||
fn call(&mut self, _: ()) -> Self::Future {
|
||||
future::ok(())
|
||||
}
|
||||
}
|
||||
|
||||
quickcheck! {
|
||||
/// Creates a random number of services, each of which must be polled a random
|
||||
/// number of times before becoming ready. As the balancer is polled, ensure that
|
||||
/// it does not become ready prematurely and that services are promoted from
|
||||
/// not_ready to ready.
|
||||
fn poll_ready(service_tries: Vec<usize>) -> TestResult {
|
||||
// Stores the number of pending services after each poll_ready call.
|
||||
let mut pending_at = Vec::new();
|
||||
|
||||
let disco = {
|
||||
let mut changes = VecDeque::new();
|
||||
for (i, n) in service_tries.iter().map(|n| *n).enumerate() {
|
||||
for j in 0..n {
|
||||
if j == pending_at.len() {
|
||||
pending_at.push(1);
|
||||
} else {
|
||||
pending_at[j] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let s = ReluctantService { polls_until_ready: n };
|
||||
changes.push_back(Change::Insert(i, s));
|
||||
}
|
||||
ReluctantDisco(changes)
|
||||
};
|
||||
pending_at.push(0);
|
||||
|
||||
let mut balancer = Balance::new(disco, choose::RoundRobin::default());
|
||||
|
||||
let services = service_tries.len();
|
||||
let mut next_pos = 0;
|
||||
for pending in pending_at.iter().map(|p| *p) {
|
||||
assert!(pending <= services);
|
||||
let ready = services - pending;
|
||||
|
||||
match balancer.poll_ready() {
|
||||
Err(_) => return TestResult::error("poll_ready failed"),
|
||||
Ok(p) => {
|
||||
if p.is_ready() != (ready > 0) {
|
||||
return TestResult::failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if balancer.num_ready() != ready {
|
||||
return TestResult::failed();
|
||||
}
|
||||
|
||||
if balancer.num_not_ready() != pending {
|
||||
return TestResult::failed();
|
||||
}
|
||||
|
||||
if balancer.is_ready() != (ready > 0) {
|
||||
return TestResult::failed();
|
||||
}
|
||||
if balancer.is_not_ready() != (ready == 0) {
|
||||
return TestResult::failed();
|
||||
}
|
||||
|
||||
if balancer.dispatched_ready_index.is_some() {
|
||||
return TestResult::failed();
|
||||
}
|
||||
|
||||
if ready == 0 {
|
||||
if balancer.chosen_ready_index.is_some() {
|
||||
return TestResult::failed();
|
||||
}
|
||||
} else {
|
||||
// Check that the round-robin chooser is doing its thing:
|
||||
match balancer.chosen_ready_index {
|
||||
None => return TestResult::failed(),
|
||||
Some(idx) => {
|
||||
if idx != next_pos {
|
||||
return TestResult::failed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next_pos = (next_pos + 1) % ready;
|
||||
}
|
||||
}
|
||||
|
||||
TestResult::passed()
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
@ -1,33 +0,0 @@
|
||||
[package]
|
||||
name = "tower-buffer"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-buffer/0.1.0"
|
||||
description = """
|
||||
Buffer requests before dispatching to a `Service`.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.25"
|
||||
tower-service = "0.2.0"
|
||||
tower-layer = "0.1.0"
|
||||
tokio-executor = "0.1.7"
|
||||
tokio-sync = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tower = { version = "0.1.0", path = "../tower" }
|
||||
tower-test = { version = "0.1.0", path = "../tower-test" }
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,13 +0,0 @@
|
||||
# Tower Buffer
|
||||
|
||||
Buffer requests before dispatching to a `Service`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,70 +0,0 @@
|
||||
//! Future types
|
||||
|
||||
use crate::{
|
||||
error::{Closed, Error},
|
||||
message,
|
||||
};
|
||||
use futures::{Async, Future, Poll};
|
||||
|
||||
/// Future eventually completed with the response to the original request.
|
||||
pub struct ResponseFuture<T> {
|
||||
state: ResponseState<T>,
|
||||
}
|
||||
|
||||
enum ResponseState<T> {
|
||||
Failed(Option<Error>),
|
||||
Rx(message::Rx<T>),
|
||||
Poll(T),
|
||||
}
|
||||
|
||||
impl<T> ResponseFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
pub(crate) fn new(rx: message::Rx<T>) -> Self {
|
||||
ResponseFuture {
|
||||
state: ResponseState::Rx(rx),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn failed(err: Error) -> Self {
|
||||
ResponseFuture {
|
||||
state: ResponseState::Failed(Some(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ResponseFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
use self::ResponseState::*;
|
||||
|
||||
loop {
|
||||
let fut;
|
||||
|
||||
match self.state {
|
||||
Failed(ref mut e) => {
|
||||
return Err(e.take().expect("polled after error"));
|
||||
}
|
||||
Rx(ref mut rx) => match rx.poll() {
|
||||
Ok(Async::Ready(Ok(f))) => fut = f,
|
||||
Ok(Async::Ready(Err(e))) => return Err(e.into()),
|
||||
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
||||
Err(_) => return Err(Closed::new().into()),
|
||||
},
|
||||
Poll(ref mut fut) => {
|
||||
return fut.poll().map_err(Into::into);
|
||||
}
|
||||
}
|
||||
|
||||
self.state = Poll(fut);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
use crate::{error::Error, service::Buffer, worker::WorkerExecutor};
|
||||
use std::marker::PhantomData;
|
||||
use tokio_executor::DefaultExecutor;
|
||||
use tower_layer::Layer;
|
||||
use tower_service::Service;
|
||||
|
||||
/// Buffer requests with a bounded buffer
|
||||
pub struct BufferLayer<Request, E = DefaultExecutor> {
|
||||
bound: usize,
|
||||
executor: E,
|
||||
_p: PhantomData<fn(Request)>,
|
||||
}
|
||||
|
||||
impl<Request> BufferLayer<Request, DefaultExecutor> {
|
||||
pub fn new(bound: usize) -> Self {
|
||||
BufferLayer {
|
||||
bound,
|
||||
executor: DefaultExecutor::current(),
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Request, E: Clone> BufferLayer<Request, E> {
|
||||
pub fn with_executor(bound: usize, executor: E) -> Self {
|
||||
BufferLayer {
|
||||
bound,
|
||||
executor,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, S, Request> Layer<S> for BufferLayer<Request, E>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error>,
|
||||
E: WorkerExecutor<S, Request> + Clone,
|
||||
{
|
||||
type Service = Buffer<S, Request>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
Buffer::with_executor(service, self.bound, &mut self.executor.clone())
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-buffer/0.1.0")]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
|
||||
//! Buffer requests when the inner service is out of capacity.
|
||||
//!
|
||||
//! Buffering works by spawning a new task that is dedicated to pulling requests
|
||||
//! out of the buffer and dispatching them to the inner service. By adding a
|
||||
//! buffer and a dedicated task, the `Buffer` layer in front of the service can
|
||||
//! be `Clone` even if the inner service is not.
|
||||
|
||||
pub mod error;
|
||||
pub mod future;
|
||||
mod layer;
|
||||
mod message;
|
||||
mod service;
|
||||
mod worker;
|
||||
|
||||
pub use crate::layer::BufferLayer;
|
||||
pub use crate::service::Buffer;
|
||||
pub use crate::worker::WorkerExecutor;
|
@ -1,126 +0,0 @@
|
||||
use crate::{
|
||||
error::{Error, SpawnError},
|
||||
future::ResponseFuture,
|
||||
message::Message,
|
||||
worker::{Handle, Worker, WorkerExecutor},
|
||||
};
|
||||
|
||||
use futures::Poll;
|
||||
use tokio_executor::DefaultExecutor;
|
||||
use tokio_sync::{mpsc, oneshot};
|
||||
use tower_service::Service;
|
||||
|
||||
/// Adds a buffer in front of an inner service.
|
||||
///
|
||||
/// See crate level documentation for more details.
|
||||
pub struct Buffer<T, Request>
|
||||
where
|
||||
T: Service<Request>,
|
||||
{
|
||||
tx: mpsc::Sender<Message<Request, T::Future>>,
|
||||
worker: Option<Handle>,
|
||||
}
|
||||
|
||||
impl<T, Request> Buffer<T, Request>
|
||||
where
|
||||
T: Service<Request>,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
/// Creates a new `Buffer` wrapping `service`.
|
||||
///
|
||||
/// `bound` gives the maximal number of requests that can be queued for the service before
|
||||
/// backpressure is applied to callers.
|
||||
///
|
||||
/// The default Tokio executor is used to run the given service, which means that this method
|
||||
/// must be called while on the Tokio runtime.
|
||||
pub fn new(service: T, bound: usize) -> Self
|
||||
where
|
||||
T: Send + 'static,
|
||||
T::Future: Send,
|
||||
T::Error: Send + Sync,
|
||||
Request: Send + 'static,
|
||||
{
|
||||
Self::with_executor(service, bound, &mut DefaultExecutor::current())
|
||||
}
|
||||
|
||||
/// Creates a new `Buffer` wrapping `service`.
|
||||
///
|
||||
/// `executor` is used to spawn a new `Worker` task that is dedicated to
|
||||
/// draining the buffer and dispatching the requests to the internal
|
||||
/// service.
|
||||
///
|
||||
/// `bound` gives the maximal number of requests that can be queued for the service before
|
||||
/// backpressure is applied to callers.
|
||||
pub fn with_executor<E>(service: T, bound: usize, executor: &mut E) -> Self
|
||||
where
|
||||
E: WorkerExecutor<T, Request>,
|
||||
{
|
||||
let (tx, rx) = mpsc::channel(bound);
|
||||
let worker = Worker::spawn(service, rx, executor);
|
||||
Buffer { tx, worker }
|
||||
}
|
||||
|
||||
fn get_worker_error(&self) -> Error {
|
||||
self.worker
|
||||
.as_ref()
|
||||
.map(|w| w.get_error_on_closed())
|
||||
.unwrap_or_else(|| {
|
||||
// If there's no worker handle, that's because spawning it
|
||||
// at the beginning failed.
|
||||
SpawnError::new().into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Request> Service<Request> for Buffer<T, Request>
|
||||
where
|
||||
T: Service<Request>,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
type Response = T::Response;
|
||||
type Error = Error;
|
||||
type Future = ResponseFuture<T::Future>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
// If the inner service has errored, then we error here.
|
||||
self.tx.poll_ready().map_err(|_| self.get_worker_error())
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
// TODO:
|
||||
// ideally we'd poll_ready again here so we don't allocate the oneshot
|
||||
// if the try_send is about to fail, but sadly we can't call poll_ready
|
||||
// outside of task context.
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
match self.tx.try_send(Message { request, tx }) {
|
||||
Err(e) => {
|
||||
if e.is_closed() {
|
||||
ResponseFuture::failed(self.get_worker_error())
|
||||
} else {
|
||||
// When `mpsc::Sender::poll_ready` returns `Ready`, a slot
|
||||
// in the channel is reserved for the handle. Other `Sender`
|
||||
// handles may not send a message using that slot. This
|
||||
// guarantees capacity for `request`.
|
||||
//
|
||||
// Given this, the only way to hit this code path is if
|
||||
// `poll_ready` has not been called & `Ready` returned.
|
||||
panic!("buffer full; poll_ready must be called first");
|
||||
}
|
||||
}
|
||||
Ok(_) => ResponseFuture::new(rx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Request> Clone for Buffer<T, Request>
|
||||
where
|
||||
T: Service<Request>,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
tx: self.tx.clone(),
|
||||
worker: self.worker.clone(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,207 +0,0 @@
|
||||
use futures::prelude::*;
|
||||
use std::{cell::RefCell, thread};
|
||||
use tokio_executor::{SpawnError, TypedExecutor};
|
||||
use tower::{
|
||||
buffer::{error, Buffer},
|
||||
Service,
|
||||
};
|
||||
use tower_test::{assert_request_eq, mock};
|
||||
|
||||
#[test]
|
||||
fn req_and_res() {
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
let response = service.call("hello");
|
||||
|
||||
assert_request_eq!(handle, "hello").send_response("world");
|
||||
|
||||
assert_eq!(response.wait().unwrap(), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clears_canceled_requests() {
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
handle.allow(1);
|
||||
|
||||
let res1 = service.call("hello");
|
||||
|
||||
let send_response1 = assert_request_eq!(handle, "hello");
|
||||
|
||||
// don't respond yet, new requests will get buffered
|
||||
|
||||
let res2 = service.call("hello2");
|
||||
with_task(|| {
|
||||
assert!(handle.poll_request().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
let res3 = service.call("hello3");
|
||||
|
||||
drop(res2);
|
||||
|
||||
send_response1.send_response("world");
|
||||
assert_eq!(res1.wait().unwrap(), "world");
|
||||
|
||||
// res2 was dropped, so it should have been canceled in the buffer
|
||||
handle.allow(1);
|
||||
|
||||
assert_request_eq!(handle, "hello3").send_response("world3");
|
||||
|
||||
assert_eq!(res3.wait().unwrap(), "world3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_inner_is_not_ready() {
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
// Make the service NotReady
|
||||
handle.allow(0);
|
||||
|
||||
let mut res1 = service.call("hello");
|
||||
|
||||
// Allow the Buffer's executor to do work
|
||||
::std::thread::sleep(::std::time::Duration::from_millis(100));
|
||||
with_task(|| {
|
||||
assert!(res1.poll().expect("res1.poll").is_not_ready());
|
||||
assert!(handle.poll_request().expect("poll_request").is_not_ready());
|
||||
});
|
||||
|
||||
handle.allow(1);
|
||||
|
||||
assert_request_eq!(handle, "hello").send_response("world");
|
||||
|
||||
assert_eq!(res1.wait().expect("res1.wait"), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_inner_fails() {
|
||||
use std::error::Error as StdError;
|
||||
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
// Make the service NotReady
|
||||
handle.allow(0);
|
||||
handle.send_error("foobar");
|
||||
|
||||
let mut res1 = service.call("hello");
|
||||
|
||||
// Allow the Buffer's executor to do work
|
||||
::std::thread::sleep(::std::time::Duration::from_millis(100));
|
||||
with_task(|| {
|
||||
let e = res1.poll().unwrap_err();
|
||||
if let Some(e) = e.downcast_ref::<error::ServiceError>() {
|
||||
let e = e.source().unwrap();
|
||||
|
||||
assert_eq!(e.to_string(), "foobar");
|
||||
} else {
|
||||
panic!("unexpected error type: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_spawn_fails() {
|
||||
let (service, _handle) = mock::pair::<(), ()>();
|
||||
|
||||
let mut exec = ExecFn(|_| Err(()));
|
||||
|
||||
let mut service = Buffer::with_executor(service, 1, &mut exec);
|
||||
|
||||
let err = with_task(|| {
|
||||
service
|
||||
.poll_ready()
|
||||
.expect_err("buffer poll_ready should error")
|
||||
});
|
||||
|
||||
assert!(
|
||||
err.is::<error::SpawnError>(),
|
||||
"should be a SpawnError: {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poll_ready_when_worker_is_dropped_early() {
|
||||
let (service, _handle) = mock::pair::<(), ()>();
|
||||
|
||||
// drop that worker right on the floor!
|
||||
let mut exec = ExecFn(|fut| {
|
||||
drop(fut);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut service = Buffer::with_executor(service, 1, &mut exec);
|
||||
|
||||
let err = with_task(|| {
|
||||
service
|
||||
.poll_ready()
|
||||
.expect_err("buffer poll_ready should error")
|
||||
});
|
||||
|
||||
assert!(err.is::<error::Closed>(), "should be a Closed: {:?}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_future_when_worker_is_dropped_early() {
|
||||
let (service, mut handle) = mock::pair::<_, ()>();
|
||||
|
||||
// hold the worker in a cell until we want to drop it later
|
||||
let cell = RefCell::new(None);
|
||||
let mut exec = ExecFn(|fut| {
|
||||
*cell.borrow_mut() = Some(fut);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut service = Buffer::with_executor(service, 1, &mut exec);
|
||||
|
||||
// keep the request in the worker
|
||||
handle.allow(0);
|
||||
let response = service.call("hello");
|
||||
|
||||
// drop the worker (like an executor closing up)
|
||||
cell.borrow_mut().take();
|
||||
|
||||
let err = response.wait().expect_err("res.wait");
|
||||
assert!(err.is::<error::Closed>(), "should be a Closed: {:?}", err);
|
||||
}
|
||||
|
||||
type Mock = mock::Mock<&'static str, &'static str>;
|
||||
type Handle = mock::Handle<&'static str, &'static str>;
|
||||
|
||||
struct Exec;
|
||||
|
||||
impl<F> TypedExecutor<F> for Exec
|
||||
where
|
||||
F: Future<Item = (), Error = ()> + Send + 'static,
|
||||
{
|
||||
fn spawn(&mut self, fut: F) -> Result<(), SpawnError> {
|
||||
thread::spawn(move || {
|
||||
fut.wait().unwrap();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecFn<Func>(Func);
|
||||
|
||||
impl<Func, F> TypedExecutor<F> for ExecFn<Func>
|
||||
where
|
||||
Func: Fn(F) -> Result<(), ()>,
|
||||
F: Future<Item = (), Error = ()> + Send + 'static,
|
||||
{
|
||||
fn spawn(&mut self, fut: F) -> Result<(), SpawnError> {
|
||||
(self.0)(fut).map_err(|()| SpawnError::shutdown())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_service() -> (Buffer<Mock, &'static str>, Handle) {
|
||||
let (service, handle) = mock::pair();
|
||||
// bound is >0 here because clears_canceled_requests needs multiple outstanding requests
|
||||
let service = Buffer::with_executor(service, 10, &mut Exec);
|
||||
(service, handle)
|
||||
}
|
||||
|
||||
fn with_task<F: FnOnce() -> U, U>(f: F) -> U {
|
||||
use futures::future::lazy;
|
||||
lazy(|| Ok::<_, ()>(f())).wait().unwrap()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "tower-discover"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-discover/0.1.0"
|
||||
description = """
|
||||
Abstracts over service discovery strategies.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.26"
|
||||
tower-service = "0.2.0"
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,13 +0,0 @@
|
||||
# Tower Discovery
|
||||
|
||||
Abstracts over service discovery strategies.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,12 +0,0 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Never {}
|
||||
|
||||
impl fmt::Display for Never {
|
||||
fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Never {}
|
@ -1,44 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-discover/0.1.0")]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
|
||||
//! # Tower service discovery
|
||||
//!
|
||||
//! Service discovery is the automatic detection of services available to the
|
||||
//! consumer. These services typically live on other servers and are accessible
|
||||
//! via the network; however, it is possible to discover services available in
|
||||
//! other processes or even in process.
|
||||
|
||||
mod error;
|
||||
mod list;
|
||||
mod stream;
|
||||
|
||||
pub use crate::{list::ServiceList, stream::ServiceStream};
|
||||
|
||||
use futures::Poll;
|
||||
use std::hash::Hash;
|
||||
|
||||
/// Provide a uniform set of services able to satisfy a request.
|
||||
///
|
||||
/// This set of services may be updated over time. On each change to the set, a
|
||||
/// new `NewServiceSet` is yielded by `Discover`.
|
||||
///
|
||||
/// See crate documentation for more details.
|
||||
pub trait Discover {
|
||||
/// NewService key
|
||||
type Key: Hash + Eq;
|
||||
|
||||
type Service;
|
||||
|
||||
/// Error produced during discovery
|
||||
type Error;
|
||||
|
||||
/// Yields the next discovery change set.
|
||||
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::Error>;
|
||||
}
|
||||
|
||||
/// A change in the service set
|
||||
pub enum Change<K, V> {
|
||||
Insert(K, V),
|
||||
Remove(K),
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use crate::{error::Never, Change, Discover};
|
||||
use futures::{Async, Poll};
|
||||
use std::iter::{Enumerate, IntoIterator};
|
||||
use tower_service::Service;
|
||||
|
||||
/// Static service discovery based on a predetermined list of services.
|
||||
///
|
||||
/// `ServiceList` is created with an initial list of services. The discovery
|
||||
/// process will yield this list once and do nothing after.
|
||||
pub struct ServiceList<T>
|
||||
where
|
||||
T: IntoIterator,
|
||||
{
|
||||
inner: Enumerate<T::IntoIter>,
|
||||
}
|
||||
|
||||
impl<T, U> ServiceList<T>
|
||||
where
|
||||
T: IntoIterator<Item = U>,
|
||||
{
|
||||
pub fn new<Request>(services: T) -> ServiceList<T>
|
||||
where
|
||||
U: Service<Request>,
|
||||
{
|
||||
ServiceList {
|
||||
inner: services.into_iter().enumerate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> Discover for ServiceList<T>
|
||||
where
|
||||
T: IntoIterator<Item = U>,
|
||||
{
|
||||
type Key = usize;
|
||||
type Service = U;
|
||||
type Error = Never;
|
||||
|
||||
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::Error> {
|
||||
match self.inner.next() {
|
||||
Some((i, service)) => Ok(Change::Insert(i, service).into()),
|
||||
None => Ok(Async::NotReady),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check that List can be directly over collections
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
type ListVecTest<T> = ServiceList<Vec<T>>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
type ListVecIterTest<T> = ServiceList<::std::vec::IntoIter<T>>;
|
@ -1,42 +0,0 @@
|
||||
use crate::{Change, Discover};
|
||||
use futures::{try_ready, Async, Poll, Stream};
|
||||
use std::hash::Hash;
|
||||
use tower_service::Service;
|
||||
|
||||
/// Dynamic service discovery based on a stream of service changes.
|
||||
pub struct ServiceStream<S> {
|
||||
inner: futures::stream::Fuse<S>,
|
||||
}
|
||||
|
||||
impl<S> ServiceStream<S> {
|
||||
pub fn new<K, Svc, Request>(services: S) -> Self
|
||||
where
|
||||
S: Stream<Item = Change<K, Svc>>,
|
||||
K: Hash + Eq,
|
||||
Svc: Service<Request>,
|
||||
{
|
||||
ServiceStream {
|
||||
inner: services.fuse(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, K, Svc> Discover for ServiceStream<S>
|
||||
where
|
||||
K: Hash + Eq,
|
||||
S: Stream<Item = Change<K, Svc>>,
|
||||
{
|
||||
type Key = K;
|
||||
type Service = Svc;
|
||||
type Error = S::Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Change<Self::Key, Self::Service>, Self::Error> {
|
||||
match try_ready!(self.inner.poll()) {
|
||||
Some(c) => Ok(Async::Ready(c)),
|
||||
None => {
|
||||
// there are no more service changes coming
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (unreleased)
|
||||
|
||||
- Initial release
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "tower-filter"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-filter/0.1.0"
|
||||
description = """
|
||||
Conditionally allow requests to be dispatched to a service based on the result
|
||||
of a predicate.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.26"
|
||||
tower-service = "0.2.0"
|
||||
tower-layer = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tower-test = { version = "0.1", path = "../tower-test" }
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,14 +0,0 @@
|
||||
# Tower Filter
|
||||
|
||||
Conditionally allow requests to be dispatched to a service based on the result
|
||||
of a predicate.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,48 +0,0 @@
|
||||
//! Error types
|
||||
|
||||
use std::{error, fmt};
|
||||
|
||||
/// Error produced by `Filter`
|
||||
#[derive(Debug)]
|
||||
pub struct Error {
|
||||
source: Option<Source>,
|
||||
}
|
||||
|
||||
pub(crate) type Source = Box<dyn error::Error + Send + Sync>;
|
||||
|
||||
impl Error {
|
||||
/// Create a new `Error` representing a rejected request.
|
||||
pub fn rejected() -> Error {
|
||||
Error { source: None }
|
||||
}
|
||||
|
||||
/// Create a new `Error` representing an inner service error.
|
||||
pub fn inner<E>(source: E) -> Error
|
||||
where
|
||||
E: Into<Source>,
|
||||
{
|
||||
Error {
|
||||
source: Some(source.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
if self.source.is_some() {
|
||||
write!(fmt, "inner service errored")
|
||||
} else {
|
||||
write!(fmt, "rejected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
|
||||
if let Some(ref err) = self.source {
|
||||
Some(&**err)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
//! Future types
|
||||
|
||||
use crate::error::{self, Error};
|
||||
use futures::{Async, Future, Poll};
|
||||
use tower_service::Service;
|
||||
|
||||
/// Filtered response future
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseFuture<T, S, Request>
|
||||
where
|
||||
S: Service<Request>,
|
||||
{
|
||||
/// Response future state
|
||||
state: State<Request, S::Future>,
|
||||
|
||||
/// Predicate future
|
||||
check: T,
|
||||
|
||||
/// Inner service
|
||||
service: S,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State<Request, U> {
|
||||
Check(Request),
|
||||
WaitResponse(U),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl<T, S, Request> ResponseFuture<T, S, Request>
|
||||
where
|
||||
T: Future<Error = Error>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<error::Source>,
|
||||
{
|
||||
pub(crate) fn new(request: Request, check: T, service: S) -> Self {
|
||||
ResponseFuture {
|
||||
state: State::Check(request),
|
||||
check,
|
||||
service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, Request> Future for ResponseFuture<T, S, Request>
|
||||
where
|
||||
T: Future<Error = Error>,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<error::Source>,
|
||||
{
|
||||
type Item = S::Response;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
use self::State::*;
|
||||
use std::mem;
|
||||
|
||||
loop {
|
||||
match mem::replace(&mut self.state, Invalid) {
|
||||
Check(request) => {
|
||||
// Poll predicate
|
||||
match self.check.poll()? {
|
||||
Async::Ready(_) => {
|
||||
let response = self.service.call(request);
|
||||
self.state = WaitResponse(response);
|
||||
}
|
||||
Async::NotReady => {
|
||||
self.state = Check(request);
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
}
|
||||
}
|
||||
WaitResponse(mut response) => {
|
||||
let ret = response.poll().map_err(Error::inner);
|
||||
|
||||
self.state = WaitResponse(response);
|
||||
|
||||
return ret;
|
||||
}
|
||||
Invalid => {
|
||||
panic!("invalid state");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use crate::Filter;
|
||||
use tower_layer::Layer;
|
||||
|
||||
pub struct FilterLayer<U> {
|
||||
predicate: U,
|
||||
}
|
||||
|
||||
impl<U> FilterLayer<U> {
|
||||
pub fn new(predicate: U) -> Self {
|
||||
FilterLayer { predicate }
|
||||
}
|
||||
}
|
||||
|
||||
impl<U: Clone, S> Layer<S> for FilterLayer<U> {
|
||||
type Service = Filter<S, U>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let predicate = self.predicate.clone();
|
||||
Filter::new(service, predicate)
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-filter/0.1.0")]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
|
||||
//! Conditionally dispatch requests to the inner service based on the result of
|
||||
//! a predicate.
|
||||
|
||||
pub mod error;
|
||||
pub mod future;
|
||||
mod layer;
|
||||
mod predicate;
|
||||
|
||||
pub use crate::{layer::FilterLayer, predicate::Predicate};
|
||||
|
||||
use crate::{error::Error, future::ResponseFuture};
|
||||
use futures::Poll;
|
||||
use tower_service::Service;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Filter<T, U> {
|
||||
inner: T,
|
||||
predicate: U,
|
||||
}
|
||||
|
||||
impl<T, U> Filter<T, U> {
|
||||
pub fn new(inner: T, predicate: U) -> Self {
|
||||
Filter { inner, predicate }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U, Request> Service<Request> for Filter<T, U>
|
||||
where
|
||||
T: Service<Request> + Clone,
|
||||
T::Error: Into<error::Source>,
|
||||
U: Predicate<Request>,
|
||||
{
|
||||
type Response = T::Response;
|
||||
type Error = Error;
|
||||
type Future = ResponseFuture<U::Future, T, Request>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
self.inner.poll_ready().map_err(error::Error::inner)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
use std::mem;
|
||||
|
||||
let inner = self.inner.clone();
|
||||
let inner = mem::replace(&mut self.inner, inner);
|
||||
|
||||
// Check the request
|
||||
let check = self.predicate.check(&request);
|
||||
|
||||
ResponseFuture::new(request, check, inner)
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
use crate::error::Error;
|
||||
use futures::{Future, IntoFuture};
|
||||
|
||||
/// Checks a request
|
||||
pub trait Predicate<Request> {
|
||||
type Future: Future<Item = (), Error = Error>;
|
||||
|
||||
fn check(&mut self, request: &Request) -> Self::Future;
|
||||
}
|
||||
|
||||
impl<F, T, U> Predicate<T> for F
|
||||
where
|
||||
F: Fn(&T) -> U,
|
||||
U: IntoFuture<Item = (), Error = Error>,
|
||||
{
|
||||
type Future = U::Future;
|
||||
|
||||
fn check(&mut self, request: &T) -> Self::Future {
|
||||
self(request).into_future()
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
use futures::*;
|
||||
use std::thread;
|
||||
use tower_filter::{error::Error, Filter};
|
||||
use tower_service::Service;
|
||||
use tower_test::{assert_request_eq, mock};
|
||||
|
||||
#[test]
|
||||
fn passthrough_sync() {
|
||||
let (mut service, mut handle) = new_service(|_| Ok(()));
|
||||
|
||||
let th = thread::spawn(move || {
|
||||
// Receive the requests and respond
|
||||
for i in 0..10 {
|
||||
assert_request_eq!(handle, format!("ping-{}", i)).send_response(format!("pong-{}", i));
|
||||
}
|
||||
});
|
||||
|
||||
let mut responses = vec![];
|
||||
|
||||
for i in 0..10 {
|
||||
let request = format!("ping-{}", i);
|
||||
assert!(service.poll_ready().unwrap().is_ready());
|
||||
let exchange = service.call(request).and_then(move |response| {
|
||||
let expect = format!("pong-{}", i);
|
||||
assert_eq!(response.as_str(), expect.as_str());
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
responses.push(exchange);
|
||||
}
|
||||
|
||||
future::join_all(responses).wait().unwrap();
|
||||
th.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_sync() {
|
||||
let (mut service, _handle) = new_service(|_| Err(Error::rejected()));
|
||||
|
||||
let response = service.call("hello".into()).wait();
|
||||
assert!(response.is_err());
|
||||
}
|
||||
|
||||
type Mock = mock::Mock<String, String>;
|
||||
type Handle = mock::Handle<String, String>;
|
||||
|
||||
fn new_service<F, U>(f: F) -> (Filter<Mock, F>, Handle)
|
||||
where
|
||||
F: Fn(&String) -> U,
|
||||
U: IntoFuture<Item = (), Error = Error>,
|
||||
{
|
||||
let (service, handle) = mock::pair();
|
||||
let service = Filter::new(service, f);
|
||||
(service, handle)
|
||||
}
|
@ -1,3 +1,48 @@
|
||||
# 0.3.3 (August 1, 2024)
|
||||
|
||||
### Added
|
||||
|
||||
- **builder,util**: add convenience methods for boxing services ([#616])
|
||||
- **all**: new functions const when possible ([#760])
|
||||
|
||||
[#616]: https://github.com/tower-rs/tower/pull/616
|
||||
[#760]: https://github.com/tower-rs/tower/pull/760
|
||||
|
||||
# 0.3.2 (Octpber 10, 2022)
|
||||
|
||||
## Added
|
||||
|
||||
- Implement `Layer` for tuples of up to 16 elements ([#694])
|
||||
|
||||
[#694]: https://github.com/tower-rs/tower/pull/694
|
||||
|
||||
# 0.3.1 (January 7, 2021)
|
||||
|
||||
### Added
|
||||
|
||||
- Added `layer_fn`, for constructing a `Layer` from a function taking
|
||||
a `Service` and returning a different `Service` ([#491])
|
||||
- Added an implementation of `Layer` for `&Layer` ([#446])
|
||||
- Multiple documentation improvements ([#487], [#490])
|
||||
|
||||
[#491]: https://github.com/tower-rs/tower/pull/491
|
||||
[#446]: https://github.com/tower-rs/tower/pull/446
|
||||
[#487]: https://github.com/tower-rs/tower/pull/487
|
||||
[#490]: https://github.com/tower-rs/tower/pull/490
|
||||
|
||||
# 0.3.0 (November 29, 2019)
|
||||
|
||||
- Move layer builder from `tower-util` to tower-layer.
|
||||
|
||||
# 0.3.0-alpha.2 (September 30, 2019)
|
||||
|
||||
- Move to `futures-*-preview 0.3.0-alpha.19`
|
||||
- Move to `pin-project 0.4`
|
||||
|
||||
# 0.3.0-alpha.1 (September 11, 2019)
|
||||
|
||||
- Move to `std::future`
|
||||
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
||||
|
@ -1,20 +1,18 @@
|
||||
[package]
|
||||
name = "tower-layer"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
version = "0.3.3"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-layer/0.1.0"
|
||||
documentation = "https://docs.rs/tower-layer/0.3.3"
|
||||
description = """
|
||||
Decorates a `Service` to allow easy composition between `Service`s.
|
||||
"""
|
||||
@ -22,8 +20,7 @@ categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.26"
|
||||
tower-service = "0.2.0"
|
||||
|
||||
[dev-dependencies]
|
||||
void = "1.0.2"
|
||||
tower-service = { path = "../tower-service" }
|
||||
tower = { path = "../tower" }
|
||||
|
@ -1,6 +1,26 @@
|
||||
# Tower Layer
|
||||
|
||||
Decorates a `Service`, transforming either the request or the response.
|
||||
Decorates a [Tower] `Service`, transforming either the request or the response.
|
||||
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![Documentation][docs-badge]][docs-url]
|
||||
[![Documentation (master)][docs-master-badge]][docs-master-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build Status][actions-badge]][actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/tower-layer.svg
|
||||
[crates-url]: https://crates.io/crates/tower-layer
|
||||
[docs-badge]: https://docs.rs/tower-layer/badge.svg
|
||||
[docs-url]: https://docs.rs/tower-layer
|
||||
[docs-master-badge]: https://img.shields.io/badge/docs-master-blue
|
||||
[docs-master-url]: https://tower-rs.github.io/tower/tower_layer
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: LICENSE
|
||||
[actions-badge]: https://github.com/tower-rs/tower/workflows/CI/badge.svg
|
||||
[actions-url]:https://github.com/tower-rs/tower/actions?query=workflow%3ACI
|
||||
[discord-badge]: https://img.shields.io/discord/500028886025895936?logo=discord&label=discord&logoColor=white
|
||||
[discord-url]: https://discord.gg/EeF3cQw
|
||||
|
||||
## Overview
|
||||
|
||||
@ -10,6 +30,8 @@ reusable components that can be applied to very different kinds of services;
|
||||
for example, it can be applied to services operating on different protocols,
|
||||
and to both the client and server side of a network transaction.
|
||||
|
||||
`tower-layer` is `no_std` compatible.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
@ -19,3 +41,5 @@ This project is licensed under the [MIT license](LICENSE).
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
||||
|
||||
[Tower]: https://crates.io/crates/tower
|
||||
|
51
tower-layer/src/identity.rs
Normal file
51
tower-layer/src/identity.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use super::Layer;
|
||||
use core::fmt;
|
||||
|
||||
/// A no-op middleware.
|
||||
///
|
||||
/// When wrapping a [`Service`], the [`Identity`] layer returns the provided
|
||||
/// service without modifying it.
|
||||
///
|
||||
/// [`Service`]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tower_layer::Identity;
|
||||
/// use tower_layer::Layer;
|
||||
///
|
||||
/// let identity = Identity::new();
|
||||
///
|
||||
/// assert_eq!(identity.layer(42), 42);
|
||||
/// ```
|
||||
#[derive(Default, Clone)]
|
||||
pub struct Identity {
|
||||
_p: (),
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
/// Creates a new [`Identity`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use tower_layer::Identity;
|
||||
///
|
||||
/// let identity = Identity::new();
|
||||
/// ```
|
||||
pub const fn new() -> Identity {
|
||||
Identity { _p: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for Identity {
|
||||
type Service = S;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
inner
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Identity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity").finish()
|
||||
}
|
||||
}
|
116
tower-layer/src/layer_fn.rs
Normal file
116
tower-layer/src/layer_fn.rs
Normal file
@ -0,0 +1,116 @@
|
||||
use super::Layer;
|
||||
use core::fmt;
|
||||
|
||||
/// Returns a new [`LayerFn`] that implements [`Layer`] by calling the
|
||||
/// given function.
|
||||
///
|
||||
/// The [`Layer::layer()`] method takes a type implementing [`Service`] and
|
||||
/// returns a different type implementing [`Service`]. In many cases, this can
|
||||
/// be implemented by a function or a closure. The [`LayerFn`] helper allows
|
||||
/// writing simple [`Layer`] implementations without needing the boilerplate of
|
||||
/// a new struct implementing [`Layer`].
|
||||
///
|
||||
/// [`Service`]: https://docs.rs/tower-service/latest/tower_service/trait.Service.html
|
||||
/// [`Layer::layer()`]: crate::Layer::layer
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tower::Service;
|
||||
/// # use core::task::{Poll, Context};
|
||||
/// # use tower_layer::{Layer, layer_fn};
|
||||
/// # use core::fmt;
|
||||
/// # use core::convert::Infallible;
|
||||
/// #
|
||||
/// // A middleware that logs requests before forwarding them to another service
|
||||
/// pub struct LogService<S> {
|
||||
/// target: &'static str,
|
||||
/// service: S,
|
||||
/// }
|
||||
///
|
||||
/// impl<S, Request> Service<Request> for LogService<S>
|
||||
/// where
|
||||
/// S: Service<Request>,
|
||||
/// Request: fmt::Debug,
|
||||
/// {
|
||||
/// type Response = S::Response;
|
||||
/// type Error = S::Error;
|
||||
/// type Future = S::Future;
|
||||
///
|
||||
/// fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
/// self.service.poll_ready(cx)
|
||||
/// }
|
||||
///
|
||||
/// fn call(&mut self, request: Request) -> Self::Future {
|
||||
/// // Log the request
|
||||
/// println!("request = {:?}, target = {:?}", request, self.target);
|
||||
///
|
||||
/// self.service.call(request)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // A `Layer` that wraps services in `LogService`
|
||||
/// let log_layer = layer_fn(|service| {
|
||||
/// LogService {
|
||||
/// service,
|
||||
/// target: "tower-docs",
|
||||
/// }
|
||||
/// });
|
||||
///
|
||||
/// // An example service. This one uppercases strings
|
||||
/// let uppercase_service = tower::service_fn(|request: String| async move {
|
||||
/// Ok::<_, Infallible>(request.to_uppercase())
|
||||
/// });
|
||||
///
|
||||
/// // Wrap our service in a `LogService` so requests are logged.
|
||||
/// let wrapped_service = log_layer.layer(uppercase_service);
|
||||
/// ```
|
||||
pub fn layer_fn<T>(f: T) -> LayerFn<T> {
|
||||
LayerFn { f }
|
||||
}
|
||||
|
||||
/// A `Layer` implemented by a closure. See the docs for [`layer_fn`] for more details.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct LayerFn<F> {
|
||||
f: F,
|
||||
}
|
||||
|
||||
impl<F, S, Out> Layer<S> for LayerFn<F>
|
||||
where
|
||||
F: Fn(S) -> Out,
|
||||
{
|
||||
type Service = Out;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
(self.f)(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> fmt::Debug for LayerFn<F> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("LayerFn")
|
||||
.field("f", &format_args!("{}", core::any::type_name::<F>()))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::{format, string::ToString};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[test]
|
||||
fn layer_fn_has_useful_debug_impl() {
|
||||
struct WrappedService<S> {
|
||||
inner: S,
|
||||
}
|
||||
let layer = layer_fn(|svc| WrappedService { inner: svc });
|
||||
let _svc = layer.layer("foo");
|
||||
|
||||
assert_eq!(
|
||||
"LayerFn { f: tower_layer::layer_fn::tests::layer_fn_has_useful_debug_impl::{{closure}} }".to_string(),
|
||||
format!("{layer:?}"),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,11 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-layer/0.1.0")]
|
||||
#![deny(missing_docs, rust_2018_idioms)]
|
||||
#![warn(
|
||||
missing_debug_implementations,
|
||||
missing_docs,
|
||||
rust_2018_idioms,
|
||||
unreachable_pub
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
// `rustdoc::broken_intra_doc_links` is checked on CI
|
||||
|
||||
//! Layer traits and extensions.
|
||||
//!
|
||||
@ -7,8 +13,26 @@
|
||||
//! allows other services to be composed with the service that implements layer.
|
||||
//!
|
||||
//! A middleware implements the [`Layer`] and [`Service`] trait.
|
||||
//!
|
||||
//! [`Service`]: https://docs.rs/tower/*/tower/trait.Service.html
|
||||
|
||||
/// Decorates a `Service`, transforming either the request or the response.
|
||||
#![no_std]
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate alloc;
|
||||
|
||||
mod identity;
|
||||
mod layer_fn;
|
||||
mod stack;
|
||||
mod tuple;
|
||||
|
||||
pub use self::{
|
||||
identity::Identity,
|
||||
layer_fn::{layer_fn, LayerFn},
|
||||
stack::Stack,
|
||||
};
|
||||
|
||||
/// Decorates a [`Service`], transforming either the request or the response.
|
||||
///
|
||||
/// Often, many of the pieces needed for writing network applications can be
|
||||
/// reused across multiple services. The `Layer` trait can be used to write
|
||||
@ -21,14 +45,10 @@
|
||||
/// Take request logging as an example:
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate futures;
|
||||
/// # extern crate tower_service;
|
||||
/// # extern crate void;
|
||||
/// # use tower_service::Service;
|
||||
/// # use futures::{Poll, Async};
|
||||
/// # use core::task::{Poll, Context};
|
||||
/// # use tower_layer::Layer;
|
||||
/// # use std::fmt;
|
||||
/// # use void::Void;
|
||||
/// # use core::fmt;
|
||||
///
|
||||
/// pub struct LogLayer {
|
||||
/// target: &'static str,
|
||||
@ -60,8 +80,8 @@
|
||||
/// type Error = S::Error;
|
||||
/// type Future = S::Future;
|
||||
///
|
||||
/// fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
/// self.service.poll_ready()
|
||||
/// fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
/// self.service.poll_ready(cx)
|
||||
/// }
|
||||
///
|
||||
/// fn call(&mut self, request: Request) -> Self::Future {
|
||||
@ -75,6 +95,8 @@
|
||||
/// The above log implementation is decoupled from the underlying protocol and
|
||||
/// is also decoupled from client or server concerns. In other words, the same
|
||||
/// log middleware could be used in either a client or a server.
|
||||
///
|
||||
/// [`Service`]: https://docs.rs/tower/*/tower/trait.Service.html
|
||||
pub trait Layer<S> {
|
||||
/// The wrapped service
|
||||
type Service;
|
||||
@ -82,3 +104,14 @@ pub trait Layer<S> {
|
||||
/// that has been decorated with the middleware.
|
||||
fn layer(&self, inner: S) -> Self::Service;
|
||||
}
|
||||
|
||||
impl<T, S> Layer<S> for &T
|
||||
where
|
||||
T: ?Sized + Layer<S>,
|
||||
{
|
||||
type Service = T::Service;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
(**self).layer(inner)
|
||||
}
|
||||
}
|
||||
|
85
tower-layer/src/stack.rs
Normal file
85
tower-layer/src/stack.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use super::Layer;
|
||||
use core::fmt;
|
||||
|
||||
/// Two [`Layer`]s chained together.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tower_layer::{Stack, layer_fn, Layer};
|
||||
///
|
||||
/// let inner = layer_fn(|service| service+2);
|
||||
/// let outer = layer_fn(|service| service*2);
|
||||
///
|
||||
/// let inner_outer_stack = Stack::new(inner, outer);
|
||||
///
|
||||
/// // (4 + 2) * 2 = 12
|
||||
/// // (4 * 2) + 2 = 10
|
||||
/// assert_eq!(inner_outer_stack.layer(4), 12);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct Stack<Inner, Outer> {
|
||||
inner: Inner,
|
||||
outer: Outer,
|
||||
}
|
||||
|
||||
impl<Inner, Outer> Stack<Inner, Outer> {
|
||||
/// Creates a new [`Stack`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use tower_layer::{Stack, Identity};
|
||||
///
|
||||
/// let stack = Stack::new(Identity::new(), Identity::new());
|
||||
/// ```
|
||||
pub const fn new(inner: Inner, outer: Outer) -> Self {
|
||||
Stack { inner, outer }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Inner, Outer> Layer<S> for Stack<Inner, Outer>
|
||||
where
|
||||
Inner: Layer<S>,
|
||||
Outer: Layer<Inner::Service>,
|
||||
{
|
||||
type Service = Outer::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let inner = self.inner.layer(service);
|
||||
|
||||
self.outer.layer(inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Inner, Outer> fmt::Debug for Stack<Inner, Outer>
|
||||
where
|
||||
Inner: fmt::Debug,
|
||||
Outer: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// The generated output of nested `Stack`s is very noisy and makes
|
||||
// it harder to understand what is in a `ServiceBuilder`.
|
||||
//
|
||||
// Instead, this output is designed assuming that a `Stack` is
|
||||
// usually quite nested, and inside a `ServiceBuilder`. Therefore,
|
||||
// this skips using `f.debug_struct()`, since each one would force
|
||||
// a new layer of indentation.
|
||||
//
|
||||
// - In compact mode, a nested stack ends up just looking like a flat
|
||||
// list of layers.
|
||||
//
|
||||
// - In pretty mode, while a newline is inserted between each layer,
|
||||
// the `DebugStruct` used in the `ServiceBuilder` will inject padding
|
||||
// to that each line is at the same indentation level.
|
||||
//
|
||||
// Also, the order of [outer, inner] is important, since it reflects
|
||||
// the order that the layers were added to the stack.
|
||||
if f.alternate() {
|
||||
// pretty
|
||||
write!(f, "{:#?},\n{:#?}", self.outer, self.inner)
|
||||
} else {
|
||||
write!(f, "{:?}, {:?}", self.outer, self.inner)
|
||||
}
|
||||
}
|
||||
}
|
330
tower-layer/src/tuple.rs
Normal file
330
tower-layer/src/tuple.rs
Normal file
@ -0,0 +1,330 @@
|
||||
use crate::Layer;
|
||||
|
||||
impl<S> Layer<S> for () {
|
||||
type Service = S;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
service
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1> Layer<S> for (L1,)
|
||||
where
|
||||
L1: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1,) = self;
|
||||
l1.layer(service)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2> Layer<S> for (L1, L2)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2) = self;
|
||||
l1.layer(l2.layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3> Layer<S> for (L1, L2, L3)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3) = self;
|
||||
l1.layer((l2, l3).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4> Layer<S> for (L1, L2, L3, L4)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4) = self;
|
||||
l1.layer((l2, l3, l4).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5> Layer<S> for (L1, L2, L3, L4, L5)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5) = self;
|
||||
l1.layer((l2, l3, l4, l5).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6> Layer<S> for (L1, L2, L3, L4, L5, L6)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7> Layer<S> for (L1, L2, L3, L4, L5, L6, L7)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8> Layer<S> for (L1, L2, L3, L4, L5, L6, L7, L8)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9> Layer<S> for (L1, L2, L3, L4, L5, L6, L7, L8, L9)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<L12::Service>,
|
||||
L12: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<L12::Service>,
|
||||
L12: Layer<L13::Service>,
|
||||
L13: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<L12::Service>,
|
||||
L12: Layer<L13::Service>,
|
||||
L13: Layer<L14::Service>,
|
||||
L14: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<L12::Service>,
|
||||
L12: Layer<L13::Service>,
|
||||
L13: Layer<L14::Service>,
|
||||
L14: Layer<L15::Service>,
|
||||
L15: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15).layer(service))
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
impl<S, L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15, L16> Layer<S>
|
||||
for (L1, L2, L3, L4, L5, L6, L7, L8, L9, L10, L11, L12, L13, L14, L15, L16)
|
||||
where
|
||||
L1: Layer<L2::Service>,
|
||||
L2: Layer<L3::Service>,
|
||||
L3: Layer<L4::Service>,
|
||||
L4: Layer<L5::Service>,
|
||||
L5: Layer<L6::Service>,
|
||||
L6: Layer<L7::Service>,
|
||||
L7: Layer<L8::Service>,
|
||||
L8: Layer<L9::Service>,
|
||||
L9: Layer<L10::Service>,
|
||||
L10: Layer<L11::Service>,
|
||||
L11: Layer<L12::Service>,
|
||||
L12: Layer<L13::Service>,
|
||||
L13: Layer<L14::Service>,
|
||||
L14: Layer<L15::Service>,
|
||||
L15: Layer<L16::Service>,
|
||||
L16: Layer<S>,
|
||||
{
|
||||
type Service = L1::Service;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
let (l1, l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16) = self;
|
||||
l1.layer((l2, l3, l4, l5, l6, l7, l8, l9, l10, l11, l12, l13, l14, l15, l16).layer(service))
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
@ -1,34 +0,0 @@
|
||||
[package]
|
||||
name = "tower-limit"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-limit/0.1.0"
|
||||
description = """
|
||||
Limit maximum request rate to a `Service`.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.26"
|
||||
tower-service = "0.2.0"
|
||||
tower-layer = "0.1.0"
|
||||
tokio-sync = "0.1.3"
|
||||
tokio-timer = "0.2.6"
|
||||
|
||||
[dev-dependencies]
|
||||
tower-test = { version = "0.1", path = "../tower-test" }
|
||||
tokio = "0.1.19"
|
||||
tokio-mock-task = "0.1.1"
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,13 +0,0 @@
|
||||
# Tower Rate Limit
|
||||
|
||||
Limit maximum request rate to a `Service`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,38 +0,0 @@
|
||||
//! Future types
|
||||
//!
|
||||
use super::Error;
|
||||
use futures::{Future, Poll};
|
||||
use std::sync::Arc;
|
||||
use tokio_sync::semaphore::Semaphore;
|
||||
|
||||
/// Future for the `ConcurrencyLimit` service.
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseFuture<T> {
|
||||
inner: T,
|
||||
semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl<T> ResponseFuture<T> {
|
||||
pub(crate) fn new(inner: T, semaphore: Arc<Semaphore>) -> ResponseFuture<T> {
|
||||
ResponseFuture { inner, semaphore }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ResponseFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
T::Error: Into<Error>,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.inner.poll().map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Drop for ResponseFuture<T> {
|
||||
fn drop(&mut self) {
|
||||
self.semaphore.add_permits(1);
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use super::ConcurrencyLimit;
|
||||
use tower_layer::Layer;
|
||||
|
||||
/// Enforces a limit on the concurrent number of requests the underlying
|
||||
/// service can handle.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConcurrencyLimitLayer {
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl ConcurrencyLimitLayer {
|
||||
/// Create a new concurrency limit layer.
|
||||
pub fn new(max: usize) -> Self {
|
||||
ConcurrencyLimitLayer { max }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for ConcurrencyLimitLayer {
|
||||
type Service = ConcurrencyLimit<S>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
ConcurrencyLimit::new(service, self.max)
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
//! Limit the max number of requests being concurrently processed.
|
||||
|
||||
pub mod future;
|
||||
mod layer;
|
||||
mod service;
|
||||
|
||||
pub use self::{layer::ConcurrencyLimitLayer, service::ConcurrencyLimit};
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
@ -1,12 +0,0 @@
|
||||
use std::fmt;
|
||||
#[derive(Debug)]
|
||||
/// An error that can never occur.
|
||||
pub enum Never {}
|
||||
|
||||
impl fmt::Display for Never {
|
||||
fn fmt(&self, _: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Never {}
|
@ -1,111 +0,0 @@
|
||||
use super::{future::ResponseFuture, Error};
|
||||
|
||||
use tower_service::Service;
|
||||
|
||||
use futures::{try_ready, Poll};
|
||||
use std::sync::Arc;
|
||||
use tokio_sync::semaphore::{self, Semaphore};
|
||||
|
||||
/// Enforces a limit on the concurrent number of requests the underlying
|
||||
/// service can handle.
|
||||
#[derive(Debug)]
|
||||
pub struct ConcurrencyLimit<T> {
|
||||
inner: T,
|
||||
limit: Limit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Limit {
|
||||
semaphore: Arc<Semaphore>,
|
||||
permit: semaphore::Permit,
|
||||
}
|
||||
|
||||
impl<T> ConcurrencyLimit<T> {
|
||||
/// Create a new concurrency limiter.
|
||||
pub fn new(inner: T, max: usize) -> Self {
|
||||
ConcurrencyLimit {
|
||||
inner,
|
||||
limit: Limit {
|
||||
semaphore: Arc::new(Semaphore::new(max)),
|
||||
permit: semaphore::Permit::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the inner service
|
||||
pub fn get_ref(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the inner service
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
/// Consume `self`, returning the inner service
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Request> Service<Request> for ConcurrencyLimit<S>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Error>,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = Error;
|
||||
type Future = ResponseFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
try_ready!(self
|
||||
.limit
|
||||
.permit
|
||||
.poll_acquire(&self.limit.semaphore)
|
||||
.map_err(Error::from));
|
||||
|
||||
self.inner.poll_ready().map_err(Into::into)
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
// Make sure a permit has been acquired
|
||||
if self
|
||||
.limit
|
||||
.permit
|
||||
.try_acquire(&self.limit.semaphore)
|
||||
.is_err()
|
||||
{
|
||||
panic!("max requests in-flight; poll_ready must be called first");
|
||||
}
|
||||
|
||||
// Call the inner service
|
||||
let future = self.inner.call(request);
|
||||
|
||||
// Forget the permit, the permit will be returned when
|
||||
// `future::ResponseFuture` is dropped.
|
||||
self.limit.permit.forget();
|
||||
|
||||
ResponseFuture::new(future, self.limit.semaphore.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Clone for ConcurrencyLimit<S>
|
||||
where
|
||||
S: Clone,
|
||||
{
|
||||
fn clone(&self) -> ConcurrencyLimit<S> {
|
||||
ConcurrencyLimit {
|
||||
inner: self.inner.clone(),
|
||||
limit: Limit {
|
||||
semaphore: self.limit.semaphore.clone(),
|
||||
permit: semaphore::Permit::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Limit {
|
||||
fn drop(&mut self) {
|
||||
self.permit.release(&self.semaphore);
|
||||
}
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-limit/0.1.0")]
|
||||
#![cfg_attr(test, deny(warnings))]
|
||||
#![deny(missing_debug_implementations, missing_docs, rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
|
||||
//! Tower middleware for limiting requests.
|
||||
|
||||
pub mod concurrency;
|
||||
pub mod rate;
|
||||
|
||||
pub use crate::{
|
||||
concurrency::{ConcurrencyLimit, ConcurrencyLimitLayer},
|
||||
rate::{RateLimit, RateLimitLayer},
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
use std::error;
|
||||
|
||||
pub(crate) type Error = Box<dyn error::Error + Send + Sync>;
|
@ -1,29 +0,0 @@
|
||||
//! Future types
|
||||
|
||||
use super::error::Error;
|
||||
use futures::{Future, Poll};
|
||||
|
||||
/// Future for the `RateLimit` service.
|
||||
#[derive(Debug)]
|
||||
pub struct ResponseFuture<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T> ResponseFuture<T> {
|
||||
pub(crate) fn new(inner: T) -> ResponseFuture<T> {
|
||||
ResponseFuture { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for ResponseFuture<T>
|
||||
where
|
||||
T: Future,
|
||||
Error: From<T::Error>,
|
||||
{
|
||||
type Item = T::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.inner.poll().map_err(Into::into)
|
||||
}
|
||||
}
|
@ -1,287 +0,0 @@
|
||||
use futures::{
|
||||
self,
|
||||
future::{poll_fn, Future},
|
||||
};
|
||||
use tokio_mock_task::MockTask;
|
||||
use tower_limit::concurrency::ConcurrencyLimit;
|
||||
use tower_service::Service;
|
||||
use tower_test::{assert_request_eq, mock};
|
||||
|
||||
macro_rules! assert_ready {
|
||||
($e:expr) => {{
|
||||
use futures::Async::*;
|
||||
match $e {
|
||||
Ok(Ready(v)) => v,
|
||||
Ok(NotReady) => panic!("not ready"),
|
||||
Err(e) => panic!("err = {:?}", e),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_not_ready {
|
||||
($e:expr) => {{
|
||||
use futures::Async::*;
|
||||
match $e {
|
||||
Ok(NotReady) => {}
|
||||
r => panic!("unexpected poll status = {:?}", r),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_service_limit_functionality_with_poll_ready() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut service, mut handle) = new_service(2);
|
||||
|
||||
poll_fn(|| service.poll_ready()).wait().unwrap();
|
||||
let r1 = service.call("hello 1");
|
||||
|
||||
poll_fn(|| service.poll_ready()).wait().unwrap();
|
||||
let r2 = service.call("hello 2");
|
||||
|
||||
task.enter(|| {
|
||||
assert!(service.poll_ready().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
assert!(!task.is_notified());
|
||||
|
||||
// The request gets passed through
|
||||
assert_request_eq!(handle, "hello 1").send_response("world 1");
|
||||
|
||||
// The next request gets passed through
|
||||
assert_request_eq!(handle, "hello 2").send_response("world 2");
|
||||
|
||||
// There are no more requests
|
||||
task.enter(|| {
|
||||
assert!(handle.poll_request().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
assert_eq!(r1.wait().unwrap(), "world 1");
|
||||
assert!(task.is_notified());
|
||||
|
||||
// Another request can be sent
|
||||
task.enter(|| {
|
||||
assert!(service.poll_ready().unwrap().is_ready());
|
||||
});
|
||||
|
||||
let r3 = service.call("hello 3");
|
||||
|
||||
task.enter(|| {
|
||||
assert!(service.poll_ready().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
assert_eq!(r2.wait().unwrap(), "world 2");
|
||||
|
||||
// The request gets passed through
|
||||
assert_request_eq!(handle, "hello 3").send_response("world 3");
|
||||
|
||||
assert_eq!(r3.wait().unwrap(), "world 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_service_limit_functionality_without_poll_ready() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut service, mut handle) = new_service(2);
|
||||
|
||||
assert_ready!(service.poll_ready());
|
||||
let r1 = service.call("hello 1");
|
||||
|
||||
assert_ready!(service.poll_ready());
|
||||
let r2 = service.call("hello 2");
|
||||
|
||||
task.enter(|| {
|
||||
assert_not_ready!(service.poll_ready());
|
||||
});
|
||||
|
||||
// The request gets passed through
|
||||
assert_request_eq!(handle, "hello 1").send_response("world 1");
|
||||
|
||||
assert!(!task.is_notified());
|
||||
|
||||
// The next request gets passed through
|
||||
assert_request_eq!(handle, "hello 2").send_response("world 2");
|
||||
|
||||
assert!(!task.is_notified());
|
||||
|
||||
// There are no more requests
|
||||
task.enter(|| {
|
||||
assert!(handle.poll_request().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
assert_eq!(r1.wait().unwrap(), "world 1");
|
||||
|
||||
assert!(task.is_notified());
|
||||
|
||||
// One more request can be sent
|
||||
assert_ready!(service.poll_ready());
|
||||
let r4 = service.call("hello 4");
|
||||
|
||||
task.enter(|| {
|
||||
assert_not_ready!(service.poll_ready());
|
||||
});
|
||||
|
||||
assert_eq!(r2.wait().unwrap(), "world 2");
|
||||
assert!(task.is_notified());
|
||||
|
||||
// The request gets passed through
|
||||
assert_request_eq!(handle, "hello 4").send_response("world 4");
|
||||
|
||||
assert_eq!(r4.wait().unwrap(), "world 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_without_capacity() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut service, _) = new_service(0);
|
||||
|
||||
task.enter(|| {
|
||||
assert_not_ready!(service.poll_ready());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reserve_capacity_without_sending_request() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut s1, mut handle) = new_service(1);
|
||||
|
||||
let mut s2 = s1.clone();
|
||||
|
||||
// Reserve capacity in s1
|
||||
task.enter(|| {
|
||||
assert!(s1.poll_ready().unwrap().is_ready());
|
||||
});
|
||||
|
||||
// Service 2 cannot get capacity
|
||||
task.enter(|| {
|
||||
assert!(s2.poll_ready().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
// s1 sends the request, then s2 is able to get capacity
|
||||
let r1 = s1.call("hello");
|
||||
|
||||
assert_request_eq!(handle, "hello").send_response("world");
|
||||
|
||||
task.enter(|| {
|
||||
assert!(s2.poll_ready().unwrap().is_not_ready());
|
||||
});
|
||||
|
||||
r1.wait().unwrap();
|
||||
|
||||
task.enter(|| {
|
||||
assert!(s2.poll_ready().unwrap().is_ready());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn service_drop_frees_capacity() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut s1, _handle) = new_service(1);
|
||||
|
||||
let mut s2 = s1.clone();
|
||||
|
||||
// Reserve capacity in s1
|
||||
assert_ready!(s1.poll_ready());
|
||||
|
||||
// Service 2 cannot get capacity
|
||||
task.enter(|| {
|
||||
assert_not_ready!(s2.poll_ready());
|
||||
});
|
||||
|
||||
drop(s1);
|
||||
|
||||
assert!(task.is_notified());
|
||||
assert_ready!(s2.poll_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_error_releases_capacity() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut s1, mut handle) = new_service(1);
|
||||
|
||||
let mut s2 = s1.clone();
|
||||
|
||||
// Reserve capacity in s1
|
||||
task.enter(|| {
|
||||
assert_ready!(s1.poll_ready());
|
||||
});
|
||||
|
||||
// s1 sends the request, then s2 is able to get capacity
|
||||
let r1 = s1.call("hello");
|
||||
|
||||
assert_request_eq!(handle, "hello").send_error("boom");
|
||||
|
||||
r1.wait().unwrap_err();
|
||||
|
||||
task.enter(|| {
|
||||
assert!(s2.poll_ready().unwrap().is_ready());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_future_drop_releases_capacity() {
|
||||
let mut task = MockTask::new();
|
||||
|
||||
let (mut s1, _handle) = new_service(1);
|
||||
|
||||
let mut s2 = s1.clone();
|
||||
|
||||
// Reserve capacity in s1
|
||||
task.enter(|| {
|
||||
assert_ready!(s1.poll_ready());
|
||||
});
|
||||
|
||||
// s1 sends the request, then s2 is able to get capacity
|
||||
let r1 = s1.call("hello");
|
||||
|
||||
task.enter(|| {
|
||||
assert_not_ready!(s2.poll_ready());
|
||||
});
|
||||
|
||||
drop(r1);
|
||||
|
||||
task.enter(|| {
|
||||
assert!(s2.poll_ready().unwrap().is_ready());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_waiters() {
|
||||
let mut task1 = MockTask::new();
|
||||
let mut task2 = MockTask::new();
|
||||
let mut task3 = MockTask::new();
|
||||
|
||||
let (mut s1, _handle) = new_service(1);
|
||||
let mut s2 = s1.clone();
|
||||
let mut s3 = s1.clone();
|
||||
|
||||
// Reserve capacity in s1
|
||||
task1.enter(|| assert_ready!(s1.poll_ready()));
|
||||
|
||||
// s2 and s3 are not ready
|
||||
task2.enter(|| assert_not_ready!(s2.poll_ready()));
|
||||
task3.enter(|| assert_not_ready!(s3.poll_ready()));
|
||||
|
||||
drop(s1);
|
||||
|
||||
assert!(task2.is_notified());
|
||||
assert!(!task3.is_notified());
|
||||
|
||||
drop(s2);
|
||||
|
||||
assert!(task3.is_notified());
|
||||
}
|
||||
|
||||
type Mock = mock::Mock<&'static str, &'static str>;
|
||||
type Handle = mock::Handle<&'static str, &'static str>;
|
||||
|
||||
fn new_service(max: usize) -> (ConcurrencyLimit<Mock>, Handle) {
|
||||
let (service, handle) = mock::pair();
|
||||
let service = ConcurrencyLimit::new(service, max);
|
||||
(service, handle)
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
use futures::future;
|
||||
use tokio::runtime::current_thread::Runtime;
|
||||
use tokio_timer::Delay;
|
||||
use tower_limit::rate::*;
|
||||
use tower_service::*;
|
||||
use tower_test::{assert_request_eq, mock};
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
macro_rules! assert_ready {
|
||||
($e:expr) => {{
|
||||
use futures::Async::*;
|
||||
match $e {
|
||||
Ok(Ready(v)) => v,
|
||||
Ok(NotReady) => panic!("not ready"),
|
||||
Err(e) => panic!("err = {:?}", e),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! assert_not_ready {
|
||||
($e:expr) => {{
|
||||
use futures::Async::*;
|
||||
match $e {
|
||||
Ok(NotReady) => {}
|
||||
r => panic!("unexpected poll status = {:?}", r),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reaching_capacity() {
|
||||
let mut rt = Runtime::new().unwrap();
|
||||
let (mut service, mut handle) = new_service(Rate::new(1, from_millis(100)));
|
||||
|
||||
assert_ready!(service.poll_ready());
|
||||
let response = service.call("hello");
|
||||
|
||||
assert_request_eq!(handle, "hello").send_response("world");
|
||||
|
||||
let response = rt.block_on(response);
|
||||
assert_eq!(response.unwrap(), "world");
|
||||
|
||||
rt.block_on(future::lazy(|| {
|
||||
assert_not_ready!(service.poll_ready());
|
||||
Ok::<_, ()>(())
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let poll_request = rt.block_on(future::lazy(|| handle.poll_request()));
|
||||
assert!(poll_request.unwrap().is_not_ready());
|
||||
|
||||
// Unlike `thread::sleep`, this advances the timer.
|
||||
rt.block_on(Delay::new(Instant::now() + Duration::from_millis(100)))
|
||||
.unwrap();
|
||||
|
||||
let poll_ready = rt.block_on(future::lazy(|| service.poll_ready()));
|
||||
assert_ready!(poll_ready);
|
||||
|
||||
// Send a second request
|
||||
let response = service.call("two");
|
||||
|
||||
assert_request_eq!(handle, "two").send_response("done");
|
||||
|
||||
let response = rt.block_on(response);
|
||||
assert_eq!(response.unwrap(), "done");
|
||||
}
|
||||
|
||||
type Mock = mock::Mock<&'static str, &'static str>;
|
||||
type Handle = mock::Handle<&'static str, &'static str>;
|
||||
|
||||
fn new_service(rate: Rate) -> (RateLimit<Mock>, Handle) {
|
||||
let (service, handle) = mock::pair();
|
||||
let service = RateLimit::new(service, rate);
|
||||
(service, handle)
|
||||
}
|
||||
|
||||
fn from_millis(n: u64) -> Duration {
|
||||
Duration::from_millis(n)
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
@ -1,32 +0,0 @@
|
||||
[package]
|
||||
name = "tower-load-shed"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-load-shed/0.1.0"
|
||||
description = """
|
||||
Immediately reject requests if the inner service is not ready. This is also
|
||||
known as load-shedding.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.1.25"
|
||||
tower-service = "0.2.0"
|
||||
tower-layer = "0.1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-mock-task = "0.1.1"
|
||||
tower-test = { version = "0.1.0", path = "../tower-test" }
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,14 +0,0 @@
|
||||
# Tower Load Shed
|
||||
|
||||
Immediately reject requests if the inner service is not ready. This is also
|
||||
known as load-shedding.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,48 +0,0 @@
|
||||
//! Future types
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use futures::{Future, Poll};
|
||||
|
||||
use crate::error::{Error, Overloaded};
|
||||
|
||||
/// Future for the `LoadShed` service.
|
||||
pub struct ResponseFuture<F> {
|
||||
state: Result<F, ()>,
|
||||
}
|
||||
|
||||
impl<F> ResponseFuture<F> {
|
||||
pub(crate) fn called(fut: F) -> Self {
|
||||
ResponseFuture { state: Ok(fut) }
|
||||
}
|
||||
|
||||
pub(crate) fn overloaded() -> Self {
|
||||
ResponseFuture { state: Err(()) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future,
|
||||
F::Error: Into<Error>,
|
||||
{
|
||||
type Item = F::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
match self.state {
|
||||
Ok(ref mut fut) => fut.poll().map_err(Into::into),
|
||||
Err(()) => Err(Overloaded::new().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> fmt::Debug for ResponseFuture<F>
|
||||
where
|
||||
// bounds for future-proofing...
|
||||
F: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("ResponseFuture")
|
||||
}
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
use tower_layer::Layer;
|
||||
|
||||
use crate::LoadShed;
|
||||
|
||||
/// A `tower-layer` to wrap services in `LoadShed` middleware.
|
||||
#[derive(Debug)]
|
||||
pub struct LoadShedLayer {
|
||||
_p: (),
|
||||
}
|
||||
|
||||
impl LoadShedLayer {
|
||||
/// Creates a new layer.
|
||||
pub fn new() -> Self {
|
||||
LoadShedLayer { _p: () }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Layer<S> for LoadShedLayer {
|
||||
type Service = LoadShed<S>;
|
||||
|
||||
fn layer(&self, service: S) -> Self::Service {
|
||||
LoadShed::new(service)
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use futures::Future;
|
||||
use tower_load_shed::{self, LoadShed};
|
||||
use tower_service::Service;
|
||||
use tower_test::{assert_request_eq, mock};
|
||||
|
||||
#[test]
|
||||
fn when_ready() {
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
with_task(|| {
|
||||
assert!(
|
||||
service.poll_ready().unwrap().is_ready(),
|
||||
"overload always reports ready",
|
||||
);
|
||||
});
|
||||
|
||||
let response = service.call("hello");
|
||||
|
||||
assert_request_eq!(handle, "hello").send_response("world");
|
||||
assert_eq!(response.wait().unwrap(), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_not_ready() {
|
||||
let (mut service, mut handle) = new_service();
|
||||
|
||||
handle.allow(0);
|
||||
|
||||
with_task(|| {
|
||||
assert!(
|
||||
service.poll_ready().unwrap().is_ready(),
|
||||
"overload always reports ready",
|
||||
);
|
||||
});
|
||||
|
||||
let fut = service.call("hello");
|
||||
|
||||
let err = fut.wait().unwrap_err();
|
||||
assert!(err.is::<tower_load_shed::error::Overloaded>());
|
||||
}
|
||||
|
||||
type Mock = mock::Mock<&'static str, &'static str>;
|
||||
type Handle = mock::Handle<&'static str, &'static str>;
|
||||
|
||||
fn new_service() -> (LoadShed<Mock>, Handle) {
|
||||
let (service, handle) = mock::pair();
|
||||
let service = LoadShed::new(service);
|
||||
(service, handle)
|
||||
}
|
||||
|
||||
fn with_task<F: FnOnce() -> U, U>(f: F) -> U {
|
||||
use futures::future::{lazy, Future};
|
||||
lazy(|| Ok::<_, ()>(f())).wait().unwrap()
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (unreleased)
|
||||
|
||||
- Initial release
|
@ -1,29 +0,0 @@
|
||||
[package]
|
||||
name = "tower-reconnect"
|
||||
# When releasing to crates.io:
|
||||
# - Remove path dependencies
|
||||
# - Update html_root_url.
|
||||
# - Update doc url
|
||||
# - Cargo.toml
|
||||
# - README.md
|
||||
# - Update CHANGELOG.md.
|
||||
# - Create "v0.1.x" git tag.
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/tower-rs/tower"
|
||||
homepage = "https://github.com/tower-rs/tower"
|
||||
documentation = "https://docs.rs/tower-reconnect/0.1.0"
|
||||
description = """
|
||||
Automatically recreate a new `Service` instance when an error is encountered.
|
||||
"""
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.1"
|
||||
futures = "0.1.26"
|
||||
tower-service = "0.2.0"
|
||||
tower-util = "0.1.0"
|
@ -1,25 +0,0 @@
|
||||
Copyright (c) 2019 Tower Contributors
|
||||
|
||||
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.
|
@ -1,13 +0,0 @@
|
||||
# Tower Reconnect
|
||||
|
||||
Automatically recreate a new `Service` instance when an error is encountered.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT license](LICENSE).
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in Tower by you, shall be licensed as MIT, without any additional
|
||||
terms or conditions.
|
@ -1,25 +0,0 @@
|
||||
use crate::Error;
|
||||
use futures::{Future, Poll};
|
||||
|
||||
pub struct ResponseFuture<F> {
|
||||
inner: F,
|
||||
}
|
||||
|
||||
impl<F> ResponseFuture<F> {
|
||||
pub(crate) fn new(inner: F) -> Self {
|
||||
ResponseFuture { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Future for ResponseFuture<F>
|
||||
where
|
||||
F: Future,
|
||||
F::Error: Into<Error>,
|
||||
{
|
||||
type Item = F::Item;
|
||||
type Error = Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.inner.poll().map_err(Into::into)
|
||||
}
|
||||
}
|
@ -1,151 +0,0 @@
|
||||
#![doc(html_root_url = "https://docs.rs/tower-load-shed/0.1.0")]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(elided_lifetimes_in_paths)]
|
||||
|
||||
pub mod future;
|
||||
|
||||
use crate::future::ResponseFuture;
|
||||
use futures::{Async, Future, Poll};
|
||||
use log::trace;
|
||||
use std::fmt;
|
||||
use tower_service::Service;
|
||||
use tower_util::MakeService;
|
||||
|
||||
pub struct Reconnect<M, Target>
|
||||
where
|
||||
M: Service<Target>,
|
||||
{
|
||||
mk_service: M,
|
||||
state: State<M::Future, M::Response>,
|
||||
target: Target,
|
||||
}
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum State<F, S> {
|
||||
Idle,
|
||||
Connecting(F),
|
||||
Connected(S),
|
||||
}
|
||||
|
||||
impl<M, Target> Reconnect<M, Target>
|
||||
where
|
||||
M: Service<Target>,
|
||||
{
|
||||
pub fn new<S, Request>(mk_service: M, target: Target) -> Self
|
||||
where
|
||||
M: Service<Target, Response = S>,
|
||||
S: Service<Request>,
|
||||
Error: From<M::Error> + From<S::Error>,
|
||||
Target: Clone,
|
||||
{
|
||||
Reconnect {
|
||||
mk_service,
|
||||
state: State::Idle,
|
||||
target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, Target, S, Request> Service<Request> for Reconnect<M, Target>
|
||||
where
|
||||
M: Service<Target, Response = S>,
|
||||
S: Service<Request>,
|
||||
Error: From<M::Error> + From<S::Error>,
|
||||
Target: Clone,
|
||||
{
|
||||
type Response = S::Response;
|
||||
type Error = Error;
|
||||
type Future = ResponseFuture<S::Future>;
|
||||
|
||||
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
|
||||
let ret;
|
||||
let mut state;
|
||||
|
||||
loop {
|
||||
match self.state {
|
||||
State::Idle => {
|
||||
trace!("poll_ready; idle");
|
||||
match self.mk_service.poll_ready()? {
|
||||
Async::Ready(()) => (),
|
||||
Async::NotReady => {
|
||||
trace!("poll_ready; MakeService not ready");
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
}
|
||||
|
||||
let fut = self.mk_service.make_service(self.target.clone());
|
||||
self.state = State::Connecting(fut);
|
||||
continue;
|
||||
}
|
||||
State::Connecting(ref mut f) => {
|
||||
trace!("poll_ready; connecting");
|
||||
match f.poll() {
|
||||
Ok(Async::Ready(service)) => {
|
||||
state = State::Connected(service);
|
||||
}
|
||||
Ok(Async::NotReady) => {
|
||||
trace!("poll_ready; not ready");
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
Err(e) => {
|
||||
trace!("poll_ready; error");
|
||||
state = State::Idle;
|
||||
ret = Err(e.into());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Connected(ref mut inner) => {
|
||||
trace!("poll_ready; connected");
|
||||
match inner.poll_ready() {
|
||||
Ok(Async::Ready(_)) => {
|
||||
trace!("poll_ready; ready");
|
||||
return Ok(Async::Ready(()));
|
||||
}
|
||||
Ok(Async::NotReady) => {
|
||||
trace!("poll_ready; not ready");
|
||||
return Ok(Async::NotReady);
|
||||
}
|
||||
Err(_) => {
|
||||
trace!("poll_ready; error");
|
||||
state = State::Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
self.state = state;
|
||||
ret
|
||||
}
|
||||
|
||||
fn call(&mut self, request: Request) -> Self::Future {
|
||||
let service = match self.state {
|
||||
State::Connected(ref mut service) => service,
|
||||
_ => panic!("service not ready; poll_ready must be called first"),
|
||||
};
|
||||
|
||||
let fut = service.call(request);
|
||||
ResponseFuture::new(fut)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, Target> fmt::Debug for Reconnect<M, Target>
|
||||
where
|
||||
M: Service<Target> + fmt::Debug,
|
||||
M::Future: fmt::Debug,
|
||||
M::Response: fmt::Debug,
|
||||
Target: fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt.debug_struct("Reconnect")
|
||||
.field("mk_service", &self.mk_service)
|
||||
.field("state", &self.state)
|
||||
.field("target", &self.target)
|
||||
.finish()
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
# 0.1.0 (April 26, 2019)
|
||||
|
||||
- Initial release
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user