mirror of
https://github.com/OMGeeky/tarpc.git
synced 2026-02-23 15:49:54 +01:00
Compare commits
496 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a6404a30 | ||
|
|
b36eac80b1 | ||
|
|
d7070e4bc3 | ||
|
|
b5d1828308 | ||
|
|
92cfe63c4f | ||
|
|
839a2f067c | ||
|
|
b5d593488c | ||
|
|
eea38b8bf4 | ||
|
|
70493c15f4 | ||
|
|
f7c5d6a7c3 | ||
|
|
98c5d2a18b | ||
|
|
46b534f7c6 | ||
|
|
42b4fc52b1 | ||
|
|
350dbcdad0 | ||
|
|
b1b4461d89 | ||
|
|
f694b7573a | ||
|
|
1e680e3a5a | ||
|
|
2591d21e94 | ||
|
|
6632f68d95 | ||
|
|
25985ad56a | ||
|
|
d6a24e9420 | ||
|
|
281a78f3c7 | ||
|
|
a0787d0091 | ||
|
|
d2acba0e8a | ||
|
|
ea7b6763c4 | ||
|
|
eb67c540b9 | ||
|
|
4151d0abd3 | ||
|
|
d0c11a6efa | ||
|
|
82c4da1743 | ||
|
|
0a15e0b75c | ||
|
|
0b315c29bf | ||
|
|
56f09bf61f | ||
|
|
6d82e82419 | ||
|
|
9bebaf814a | ||
|
|
5f4d6e6008 | ||
|
|
07d07d7ba3 | ||
|
|
a41bbf65b2 | ||
|
|
21e2f7ca62 | ||
|
|
7b7c182411 | ||
|
|
db0c778ead | ||
|
|
c3efb83ac1 | ||
|
|
3d7b0171fe | ||
|
|
c191ff5b2e | ||
|
|
90bc7f741d | ||
|
|
d3f6c01df2 | ||
|
|
c6450521e6 | ||
|
|
1da6bcec57 | ||
|
|
75a5591158 | ||
|
|
9462aad3bf | ||
|
|
0964fc51ff | ||
|
|
27aacab432 | ||
|
|
3feb465ad3 | ||
|
|
66cdc99ae0 | ||
|
|
66419db6fd | ||
|
|
72d5dbba89 | ||
|
|
e75193c191 | ||
|
|
ce4fd49161 | ||
|
|
3c978c5bf6 | ||
|
|
6f419e9a9a | ||
|
|
b3eb8d0b7a | ||
|
|
3b422eb179 | ||
|
|
4b513bad73 | ||
|
|
e71e17866d | ||
|
|
7e3fbec077 | ||
|
|
e4bc5e8e32 | ||
|
|
bc982c5584 | ||
|
|
d440e12c19 | ||
|
|
bc8128af69 | ||
|
|
1d87c14262 | ||
|
|
ca929c2178 | ||
|
|
569039734b | ||
|
|
3d43310e6a | ||
|
|
d21cbddb0d | ||
|
|
25aa857edf | ||
|
|
0bb2e2bbbe | ||
|
|
dc376343d6 | ||
|
|
2e7d1f8a88 | ||
|
|
6314591c65 | ||
|
|
7dd7494420 | ||
|
|
6c10e3649f | ||
|
|
4c6dee13d2 | ||
|
|
e45abe953a | ||
|
|
dec3e491b5 | ||
|
|
6ce341cf79 | ||
|
|
b9868250f8 | ||
|
|
a3f1064efe | ||
|
|
026083d653 | ||
|
|
d27f341bde | ||
|
|
2264ebecfc | ||
|
|
3207affb4a | ||
|
|
0602afd50c | ||
|
|
4343e12217 | ||
|
|
7fda862fb8 | ||
|
|
aa7b875b1a | ||
|
|
54d6e0e3b6 | ||
|
|
bea3b442aa | ||
|
|
954a2502e7 | ||
|
|
e3f34917c5 | ||
|
|
f65dd05949 | ||
|
|
240c436b34 | ||
|
|
c9803688cc | ||
|
|
4987094483 | ||
|
|
ff55080193 | ||
|
|
258193c932 | ||
|
|
67823ef5de | ||
|
|
a671457243 | ||
|
|
cf654549da | ||
|
|
6a01e32a2d | ||
|
|
e6597fab03 | ||
|
|
ebd245a93d | ||
|
|
3ebc3b5845 | ||
|
|
0e5973109d | ||
|
|
5f02d7383a | ||
|
|
2bae148529 | ||
|
|
42a2e03aab | ||
|
|
b566d0c646 | ||
|
|
b359f16767 | ||
|
|
f8681ab134 | ||
|
|
7e521768ab | ||
|
|
e9b1e7d101 | ||
|
|
f0322fb892 | ||
|
|
617daebb88 | ||
|
|
a11d4fff58 | ||
|
|
bf42a04d83 | ||
|
|
06528d6953 | ||
|
|
9f00395746 | ||
|
|
e0674cd57f | ||
|
|
7e49bd9ee7 | ||
|
|
8a1baa9c4e | ||
|
|
31c713d188 | ||
|
|
d905bc1591 | ||
|
|
7f946c7f83 | ||
|
|
36cfdb6c6f | ||
|
|
dbabe9774f | ||
|
|
deb041b8d3 | ||
|
|
85d49477f5 | ||
|
|
45af6ccdeb | ||
|
|
917c0c5e2d | ||
|
|
bbbd43e282 | ||
|
|
f945392b5a | ||
|
|
f4060779e4 | ||
|
|
7cc8d9640b | ||
|
|
7f871f03ef | ||
|
|
709b966150 | ||
|
|
5e19b79aa4 | ||
|
|
6eb806907a | ||
|
|
8250ca31ff | ||
|
|
7cd776143b | ||
|
|
5f6c3d7d98 | ||
|
|
915fe3ed4e | ||
|
|
d8c7b9feb2 | ||
|
|
5ab3866d96 | ||
|
|
184ea42033 | ||
|
|
014c209b8e | ||
|
|
e91005855c | ||
|
|
46bcc0f559 | ||
|
|
61322ebf41 | ||
|
|
db0c9c4182 | ||
|
|
9ee3011687 | ||
|
|
5aa4a2cef6 | ||
|
|
f38a172523 | ||
|
|
66dbca80b2 | ||
|
|
61377dd4ff | ||
|
|
cd03f3ff8c | ||
|
|
9479963773 | ||
|
|
f974533bf7 | ||
|
|
d560ac6197 | ||
|
|
1cdff15412 | ||
|
|
f8ba7d9f4e | ||
|
|
41c1aafaf7 | ||
|
|
75d1e877be | ||
|
|
88e1cf558b | ||
|
|
50879d2acb | ||
|
|
13cb14a119 | ||
|
|
22ef6b7800 | ||
|
|
e48e6dfe67 | ||
|
|
1b58914d59 | ||
|
|
2f24842b2d | ||
|
|
5c485fe608 | ||
|
|
b0319e7db9 | ||
|
|
a4d9581888 | ||
|
|
fb5022b1c0 | ||
|
|
abb0b5b3ac | ||
|
|
49f2641e3c | ||
|
|
650c60fe44 | ||
|
|
1d0bbcb36c | ||
|
|
c456ad7fa5 | ||
|
|
537446a5c9 | ||
|
|
94b5b2c431 | ||
|
|
9863433fea | ||
|
|
9a27465a25 | ||
|
|
263cfe1435 | ||
|
|
6ae5302a70 | ||
|
|
c67b7283e7 | ||
|
|
7b6e98da7b | ||
|
|
15b65fa20f | ||
|
|
372900173a | ||
|
|
1089415451 | ||
|
|
8dbeeff0eb | ||
|
|
85312d430c | ||
|
|
9843af9e00 | ||
|
|
a6bd423ef0 | ||
|
|
146496d08c | ||
|
|
b562051c38 | ||
|
|
fe164ca368 | ||
|
|
950ad5187c | ||
|
|
e6ab69c314 | ||
|
|
373dcbed57 | ||
|
|
ce9c057b1b | ||
|
|
6745cee72c | ||
|
|
31abea18b3 | ||
|
|
593ac135ce | ||
|
|
05a924d27f | ||
|
|
af9d71ed0d | ||
|
|
9b90f6ae51 | ||
|
|
bbfc8ac352 | ||
|
|
ad86a967ba | ||
|
|
58a0eced19 | ||
|
|
46fffd13e7 | ||
|
|
6c8d4be462 | ||
|
|
e3a517bf0d | ||
|
|
f4e22bdc2e | ||
|
|
46f56fbdc0 | ||
|
|
8665655592 | ||
|
|
4569d26d81 | ||
|
|
b8b92ddb5f | ||
|
|
8dd3390876 | ||
|
|
06c420b60c | ||
|
|
a7fb4d22cc | ||
|
|
b1cd5f34e5 | ||
|
|
088e5f8f2c | ||
|
|
4e0be5b626 | ||
|
|
5516034bbc | ||
|
|
06544faa5a | ||
|
|
39737b720a | ||
|
|
0f36985440 | ||
|
|
959bb691cd | ||
|
|
2a3162c5fa | ||
|
|
0cc976b729 | ||
|
|
4d2d3f24c6 | ||
|
|
2c7c64841f | ||
|
|
4ea142d0f3 | ||
|
|
00751d2518 | ||
|
|
4394a52b65 | ||
|
|
70938501d7 | ||
|
|
d5f5cf4300 | ||
|
|
e2c4164d8c | ||
|
|
78124ef7a8 | ||
|
|
096d354b7e | ||
|
|
7ad0e4b070 | ||
|
|
64755d5329 | ||
|
|
3071422132 | ||
|
|
8847330dbe | ||
|
|
6d396520f4 | ||
|
|
79a2f7fe2f | ||
|
|
af66841f68 | ||
|
|
1ab4cfdff9 | ||
|
|
f7e03eeeb7 | ||
|
|
29067b7773 | ||
|
|
905e5be8bb | ||
|
|
5e4b97e589 | ||
|
|
9bd66b7e49 | ||
|
|
0ecc7a80c1 | ||
|
|
92f157206d | ||
|
|
b093db63a3 | ||
|
|
8c3e3df47f | ||
|
|
6907c6e0a3 | ||
|
|
4b5273127d | ||
|
|
4b763e9f52 | ||
|
|
848eb00bea | ||
|
|
44ec68c002 | ||
|
|
b2282f9d7a | ||
|
|
326f0270b9 | ||
|
|
fd47a6c038 | ||
|
|
77cfffaaed | ||
|
|
118893678b | ||
|
|
ae3985de46 | ||
|
|
49f36e0b2b | ||
|
|
4a7082b27c | ||
|
|
3aa53a06fb | ||
|
|
a0afbefef4 | ||
|
|
5b554f7062 | ||
|
|
0411a90be9 | ||
|
|
9ce7938fdc | ||
|
|
650dc88da5 | ||
|
|
3601763442 | ||
|
|
4aaaea1e04 | ||
|
|
2e214c85d3 | ||
|
|
0676ab67df | ||
|
|
0b843512dd | ||
|
|
85d9416750 | ||
|
|
5e3cf3c807 | ||
|
|
4dfb3a48c3 | ||
|
|
21e8883877 | ||
|
|
7dbfe07c97 | ||
|
|
8bc01a993b | ||
|
|
e2728d84f3 | ||
|
|
85a8f8fce7 | ||
|
|
9267daa409 | ||
|
|
a01ecd3314 | ||
|
|
dd662beb9b | ||
|
|
5857de5164 | ||
|
|
8cb6ff89cc | ||
|
|
a441fcb771 | ||
|
|
f0ad99b900 | ||
|
|
5add81b5f3 | ||
|
|
15080b2889 | ||
|
|
8598d4beaf | ||
|
|
40faf25d99 | ||
|
|
c8be9b690b | ||
|
|
f4018a431e | ||
|
|
79aee18d17 | ||
|
|
f9ff2c4e50 | ||
|
|
073bc25e18 | ||
|
|
d0d65c413a | ||
|
|
e59116fb48 | ||
|
|
daa96a69a2 | ||
|
|
85ae614983 | ||
|
|
e17549a1f7 | ||
|
|
0b8a845ec1 | ||
|
|
2b8f3db1fd | ||
|
|
44792347b1 | ||
|
|
f7c371930f | ||
|
|
3eec8fe1dd | ||
|
|
9c973eb80b | ||
|
|
37763f25e4 | ||
|
|
515ab90299 | ||
|
|
9987eae290 | ||
|
|
22545c653c | ||
|
|
8b9847e347 | ||
|
|
49bc4d0bce | ||
|
|
0780af9e05 | ||
|
|
bc16ffd2d0 | ||
|
|
c7831e8aa6 | ||
|
|
8faba59d66 | ||
|
|
bceaea1206 | ||
|
|
fea8d5eb1d | ||
|
|
db9c23058d | ||
|
|
77638b388d | ||
|
|
eac6b64aeb | ||
|
|
7ae107cf2b | ||
|
|
12efcae80c | ||
|
|
5fbd45ea49 | ||
|
|
e651838f19 | ||
|
|
cdb44090bd | ||
|
|
fc2edc89af | ||
|
|
30eed41c40 | ||
|
|
517477129c | ||
|
|
1325d1a2d7 | ||
|
|
15f83961e9 | ||
|
|
4fc37fe707 | ||
|
|
8ee6ce0307 | ||
|
|
07f9d5a34d | ||
|
|
a0a11f8704 | ||
|
|
6673869721 | ||
|
|
63caacf0c1 | ||
|
|
2c09a35705 | ||
|
|
6bf4d171c1 | ||
|
|
0f52b08426 | ||
|
|
acdf03c8ca | ||
|
|
b2f69faa13 | ||
|
|
2749d33f88 | ||
|
|
338c91d393 | ||
|
|
fa2df184e9 | ||
|
|
fe4eab38f1 | ||
|
|
06beec3e5a | ||
|
|
d39a382012 | ||
|
|
efcc914e65 | ||
|
|
a1072c8c06 | ||
|
|
ed90f4ecea | ||
|
|
b5bf696017 | ||
|
|
fe20c8af14 | ||
|
|
2ffa1138dd | ||
|
|
9d552e48a4 | ||
|
|
fafe569ebc | ||
|
|
c286c596bd | ||
|
|
348111a423 | ||
|
|
984c1c29c5 | ||
|
|
cc1290636d | ||
|
|
41683eee1d | ||
|
|
6c2239d6f2 | ||
|
|
3196fd91ff | ||
|
|
45fa4c7bf1 | ||
|
|
c7c18cbaaa | ||
|
|
1c0cf2a67f | ||
|
|
15a3900f3d | ||
|
|
f0ecd7008d | ||
|
|
3567202aa3 | ||
|
|
802ee838ca | ||
|
|
c976ca710a | ||
|
|
9d2d69b4f4 | ||
|
|
558dda28ad | ||
|
|
4a4ffab611 | ||
|
|
cabbbb2a0b | ||
|
|
3dc1b6381d | ||
|
|
c96ab77dcf | ||
|
|
865712f36e | ||
|
|
a8e5bc45a1 | ||
|
|
95c57a4b2d | ||
|
|
ab3e73812c | ||
|
|
d34ca2acda | ||
|
|
9e92666932 | ||
|
|
a6b25dc268 | ||
|
|
77e12f56cc | ||
|
|
05c6be192d | ||
|
|
568484f14f | ||
|
|
918b6b3b75 | ||
|
|
d5854fd049 | ||
|
|
9646d92cae | ||
|
|
a660ed7f1a | ||
|
|
3eb2292841 | ||
|
|
5a525d9fb7 | ||
|
|
e4ef0881e6 | ||
|
|
91e9ad3001 | ||
|
|
b3c187cdac | ||
|
|
ffea090726 | ||
|
|
e8f942f463 | ||
|
|
5e9527e583 | ||
|
|
48452c04a6 | ||
|
|
626254e836 | ||
|
|
3719564efc | ||
|
|
ef41d4349c | ||
|
|
200407a4c9 | ||
|
|
388c4be69a | ||
|
|
77653282b6 | ||
|
|
5dbfa99d0b | ||
|
|
18cbbe5b15 | ||
|
|
e22210bfd8 | ||
|
|
d242bdbb82 | ||
|
|
bdd6737914 | ||
|
|
f2bf1adf8b | ||
|
|
be156f4d6b | ||
|
|
35f8aefb30 | ||
|
|
8a29aa29b2 | ||
|
|
b5750365ca | ||
|
|
f6b1660092 | ||
|
|
5c17ffacae | ||
|
|
13e56481bb | ||
|
|
608be5372b | ||
|
|
583f2cd92f | ||
|
|
ef8deb2059 | ||
|
|
c437d66603 | ||
|
|
662a627e4e | ||
|
|
d47a931f9f | ||
|
|
a30e929b63 | ||
|
|
b638f45d27 | ||
|
|
68ba7505ac | ||
|
|
3afcfe6274 | ||
|
|
85fbe411e6 | ||
|
|
aaaaf942d6 | ||
|
|
29b6425fb5 | ||
|
|
539776eb27 | ||
|
|
61e7d9aab8 | ||
|
|
59988c6ee1 | ||
|
|
a1de4c1b05 | ||
|
|
b6e9d61286 | ||
|
|
67ad2b90be | ||
|
|
3506397150 | ||
|
|
db665ebb60 | ||
|
|
cff8782e18 | ||
|
|
531dc20d66 | ||
|
|
d8d240ec12 | ||
|
|
eb49c30fdb | ||
|
|
b661ff0175 | ||
|
|
b880d65f44 | ||
|
|
451b99b92a | ||
|
|
99b13ae6fc | ||
|
|
5bace01f2b | ||
|
|
4a63064cbd | ||
|
|
3eb57d4009 | ||
|
|
14c97b61f9 | ||
|
|
20d1a019ae | ||
|
|
8c0181633d | ||
|
|
987e445935 | ||
|
|
1c318182c4 | ||
|
|
ac7e2eedb2 | ||
|
|
6d1fbab73c | ||
|
|
e8902c21a2 | ||
|
|
be5f55c5f6 | ||
|
|
54017839d1 | ||
|
|
b5f2fe5f4f | ||
|
|
437997b2d1 | ||
|
|
b5e472b374 | ||
|
|
f76030ecd7 | ||
|
|
b7952d3f47 | ||
|
|
8d561d21b2 | ||
|
|
8968b1fbd2 | ||
|
|
ab6e829ddd | ||
|
|
246c09c876 | ||
|
|
e7cfbc1085 | ||
|
|
7aabfb3c14 | ||
|
|
64ca851209 | ||
|
|
1738864d32 | ||
|
|
683617674c | ||
|
|
3c4932f55a | ||
|
|
3cf8e440f7 |
112
.github/workflows/main.yml
vendored
Normal file
112
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
name: Continuous integration
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
target: mipsel-unknown-linux-gnu
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all-features
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all-features --target mipsel-unknown-linux-gnu
|
||||
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path tarpc/Cargo.toml --features serde1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path tarpc/Cargo.toml --features tokio1
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path tarpc/Cargo.toml --features serde-transport
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --manifest-path tarpc/Cargo.toml --features tcp
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features
|
||||
|
||||
fmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Cancel previous
|
||||
uses: styfle/cancel-workflow-action@0.7.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-features -- -D warnings
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
target
|
||||
Cargo.lock
|
||||
.cargo
|
||||
*.swp
|
||||
*.bk
|
||||
tarpc.iml
|
||||
.idea
|
||||
|
||||
32
.travis.yml
32
.travis.yml
@@ -1,32 +0,0 @@
|
||||
language: rust
|
||||
sudo: false
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
|
||||
before_script:
|
||||
- |
|
||||
pip install 'travis-cargo<0.2' --user &&
|
||||
export PATH=$HOME/.local/bin:$PATH
|
||||
|
||||
script:
|
||||
- |
|
||||
(cd tarpc && travis-cargo build) &&
|
||||
(cd tarpc && travis-cargo test)
|
||||
|
||||
after_success:
|
||||
- (cd tarpc && travis-cargo coveralls --no-sudo)
|
||||
|
||||
env:
|
||||
global:
|
||||
# override the default `--features unstable` used for the nightly branch
|
||||
- TRAVIS_CARGO_NIGHTLY_FEATURE=""
|
||||
11
Cargo.toml
Normal file
11
Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"example-service",
|
||||
"tarpc",
|
||||
"plugins",
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
211
README.md
211
README.md
@@ -1,92 +1,171 @@
|
||||
## tarpc: Tim & Adam's RPC lib
|
||||
[](https://travis-ci.org/google/tarpc)
|
||||
[](https://coveralls.io/github/google/tarpc?branch=master)
|
||||
[](LICENSE.txt)
|
||||
[](https://crates.io/crates/tarpc)
|
||||
[![Crates.io][crates-badge]][crates-url]
|
||||
[![MIT licensed][mit-badge]][mit-url]
|
||||
[![Build status][gh-actions-badge]][gh-actions-url]
|
||||
[![Discord chat][discord-badge]][discord-url]
|
||||
|
||||
[crates-badge]: https://img.shields.io/crates/v/tarpc.svg
|
||||
[crates-url]: https://crates.io/crates/tarpc
|
||||
[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[mit-url]: LICENSE
|
||||
[gh-actions-badge]: https://github.com/google/tarpc/workflows/Continuous%20integration/badge.svg
|
||||
[gh-actions-url]: https://github.com/google/tarpc/actions?query=workflow%3A%22Continuous+integration%22
|
||||
[discord-badge]: https://img.shields.io/discord/647529123996237854.svg?logo=discord&style=flat-square
|
||||
[discord-url]: https://discord.gg/gXwpdSt
|
||||
|
||||
# tarpc
|
||||
|
||||
<!-- cargo-sync-readme start -->
|
||||
|
||||
*Disclaimer*: This is not an official Google product.
|
||||
|
||||
tarpc is an RPC framework for rust with a focus on ease of use. Defining a service can be done in
|
||||
just a few lines of code, and most of the boilerplate of writing a server is taken care of for you.
|
||||
tarpc is an RPC framework for rust with a focus on ease of use. Defining a
|
||||
service can be done in just a few lines of code, and most of the boilerplate of
|
||||
writing a server is taken care of for you.
|
||||
|
||||
[Documentation](https://google.github.io/tarpc)
|
||||
[Documentation](https://docs.rs/crate/tarpc/)
|
||||
|
||||
## What is an RPC framework?
|
||||
"RPC" stands for "Remote Procedure Call," a function call where the work of producing the return
|
||||
value is being done somewhere else. When an rpc function is invoked, behind the scenes the function
|
||||
contacts some other process somewhere and asks them to compute the function instead. The original
|
||||
function then returns the value produced by that other server.
|
||||
"RPC" stands for "Remote Procedure Call," a function call where the work of
|
||||
producing the return value is being done somewhere else. When an rpc function is
|
||||
invoked, behind the scenes the function contacts some other process somewhere
|
||||
and asks them to evaluate the function instead. The original function then
|
||||
returns the value produced by the other process.
|
||||
|
||||
[More information](https://www.cs.cf.ac.uk/Dave/C/node33.html)
|
||||
RPC frameworks are a fundamental building block of most microservices-oriented
|
||||
architectures. Two well-known ones are [gRPC](http://www.grpc.io) and
|
||||
[Cap'n Proto](https://capnproto.org/).
|
||||
|
||||
tarpc differentiates itself from other RPC frameworks by defining the schema in code,
|
||||
rather than in a separate language such as .proto. This means there's no separate compilation
|
||||
process, and no context switching between different languages.
|
||||
|
||||
Some other features of tarpc:
|
||||
- Pluggable transport: any type implementing `Stream<Item = Request> + Sink<Response>` can be
|
||||
used as a transport to connect the client and server.
|
||||
- `Send + 'static` optional: if the transport doesn't require it, neither does tarpc!
|
||||
- Cascading cancellation: dropping a request will send a cancellation message to the server.
|
||||
The server will cease any unfinished work on the request, subsequently cancelling any of its
|
||||
own requests, repeating for the entire chain of transitive dependencies.
|
||||
- Configurable deadlines and deadline propagation: request deadlines default to 10s if
|
||||
unspecified. The server will automatically cease work when the deadline has passed. Any
|
||||
requests sent by the server that use the request context will propagate the request deadline.
|
||||
For example, if a server is handling a request with a 10s deadline, does 2s of work, then
|
||||
sends a request to another server, that server will see an 8s deadline.
|
||||
- Distributed tracing: tarpc is instrumented with
|
||||
[tracing](https://github.com/tokio-rs/tracing) primitives extended with
|
||||
[OpenTelemetry](https://opentelemetry.io/) traces. Using a compatible tracing subscriber like
|
||||
[Jaeger](https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-jaeger),
|
||||
each RPC can be traced through the client, server, and other dependencies downstream of the
|
||||
server. Even for applications not connected to a distributed tracing collector, the
|
||||
instrumentation can also be ingested by regular loggers like
|
||||
[env_logger](https://github.com/env-logger-rs/env_logger/).
|
||||
- Serde serialization: enabling the `serde1` Cargo feature will make service requests and
|
||||
responses `Serialize + Deserialize`. It's entirely optional, though: in-memory transports can
|
||||
be used, as well, so the price of serialization doesn't have to be paid when it's not needed.
|
||||
|
||||
## Usage
|
||||
Add to your `Cargo.toml` dependencies:
|
||||
|
||||
```toml
|
||||
tarpc = "0.5.1"
|
||||
tarpc = "0.28"
|
||||
```
|
||||
|
||||
The `tarpc::service` attribute expands to a collection of items that form an rpc service.
|
||||
These generated types make it easy and ergonomic to write servers with less boilerplate.
|
||||
Simply implement the generated service trait, and you're off to the races!
|
||||
|
||||
## Example
|
||||
|
||||
This example uses [tokio](https://tokio.rs), so add the following dependencies to
|
||||
your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
tarpc = { version = "0.28", features = ["tokio1"] }
|
||||
tokio = { version = "1.0", features = ["macros"] }
|
||||
```
|
||||
|
||||
In the following example, we use an in-process channel for communication between
|
||||
client and server. In real code, you will likely communicate over the network.
|
||||
For a more real-world example, see [example-service](example-service).
|
||||
|
||||
First, let's set up the dependencies and service definition.
|
||||
|
||||
```rust
|
||||
#[macro_use]
|
||||
extern crate tarpc;
|
||||
|
||||
mod hello_service {
|
||||
service! {
|
||||
rpc hello(name: String) -> String;
|
||||
}
|
||||
}
|
||||
use hello_service::Service as HelloService;
|
||||
use futures::{
|
||||
future::{self, Ready},
|
||||
prelude::*,
|
||||
};
|
||||
use tarpc::{
|
||||
client, context,
|
||||
server::{self, incoming::Incoming, Channel},
|
||||
};
|
||||
|
||||
struct HelloServer;
|
||||
impl HelloService for HelloServer {
|
||||
fn hello(&self, name: String) -> String {
|
||||
format!("Hello, {}!", name)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let addr = "localhost:10000";
|
||||
let server_handle = HelloServer.spawn(addr).unwrap();
|
||||
let client = hello_service::Client::new(addr).unwrap();
|
||||
assert_eq!("Hello, Mom!", client.hello("Mom".into()).unwrap());
|
||||
drop(client);
|
||||
server_handle.shutdown();
|
||||
// This is the service definition. It looks a lot like a trait definition.
|
||||
// It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
#[tarpc::service]
|
||||
trait World {
|
||||
/// Returns a greeting for name.
|
||||
async fn hello(name: String) -> String;
|
||||
}
|
||||
```
|
||||
|
||||
The `service!` macro expands to a collection of items that collectively form an rpc service. In the
|
||||
above example, the macro is called within the `hello_service` module. This module will contain a
|
||||
`Client` (and `AsyncClient`) type, and a `Service` trait. The trait provides default `fn`s for
|
||||
starting the service: `spawn` and `spawn_with_config`, which start the service listening over an
|
||||
arbitrary transport. A `Client` (or `AsyncClient`) can connect to such a service. These generated
|
||||
types make it easy and ergonomic to write servers without dealing with sockets or serialization
|
||||
directly. See the tarpc_examples package for more sophisticated examples.
|
||||
This service definition generates a trait called `World`. Next we need to
|
||||
implement it for our Server struct.
|
||||
|
||||
```rust
|
||||
// This is the type that implements the generated World trait. It is the business logic
|
||||
// and is used to start the server.
|
||||
#[derive(Clone)]
|
||||
struct HelloServer;
|
||||
|
||||
impl World for HelloServer {
|
||||
// Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
// an associated type representing the future output by the fn.
|
||||
|
||||
type HelloFut = Ready<String>;
|
||||
|
||||
fn hello(self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
future::ready(format!("Hello, {name}!"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Lastly let's write our `main` that will start the server. While this example uses an
|
||||
[in-process channel](transport::channel), tarpc also ships a generic [`serde_transport`]
|
||||
behind the `serde-transport` feature, with additional [TCP](serde_transport::tcp) functionality
|
||||
available behind the `tcp` feature.
|
||||
|
||||
```rust
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
|
||||
|
||||
let server = server::BaseChannel::with_defaults(server_transport);
|
||||
tokio::spawn(server.execute(HelloServer.serve()));
|
||||
|
||||
// WorldClient is generated by the #[tarpc::service] attribute. It has a constructor `new`
|
||||
// that takes a config and any Transport as input.
|
||||
let mut client = WorldClient::new(client::Config::default(), client_transport).spawn();
|
||||
|
||||
// The client has an RPC method for each RPC defined in the annotated trait. It takes the same
|
||||
// args as defined, with the addition of a Context, which is always the first arg. The Context
|
||||
// specifies a deadline and trace information which can be helpful in debugging requests.
|
||||
let hello = client.hello(context::current(), "Stim".to_string()).await?;
|
||||
|
||||
println!("{hello}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Service Documentation
|
||||
|
||||
## Documentation
|
||||
Use `cargo doc` as you normally would to see the documentation created for all
|
||||
items expanded by a `service!` invocation.
|
||||
|
||||
## Additional Features
|
||||
- Connect over any transport that `impl`s the `Transport` trait.
|
||||
- Concurrent requests from a single client.
|
||||
- Any type that `impl`s `serde`'s `Serialize` and `Deserialize` can be used in the rpc signatures.
|
||||
- Attributes can be specified on rpc methods. These will be included on both the `Service` trait
|
||||
methods as well as on the `Client`'s stub methods.
|
||||
- Just like regular fns, the return type can be left off when it's `-> ()`.
|
||||
- Arg-less rpc's are also allowed.
|
||||
<!-- cargo-sync-readme end -->
|
||||
|
||||
## Planned Improvements (actively being worked on)
|
||||
- Automatically reconnect on the client side when the connection cuts out.
|
||||
- Support asynchronous server implementations (currently thread per connection).
|
||||
- Support generic serialization protocols.
|
||||
|
||||
## Contributing
|
||||
|
||||
To contribute to tarpc, please see [CONTRIBUTING](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
tarpc is distributed under the terms of the MIT license.
|
||||
|
||||
See [LICENSE](LICENSE) for details.
|
||||
License: MIT
|
||||
|
||||
406
RELEASES.md
406
RELEASES.md
@@ -1,3 +1,409 @@
|
||||
## 0.28.0 (2022-04-06)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- The minimum supported Rust version has increased to 1.58.0.
|
||||
- The version of opentelemetry depended on by tarpc has increased to 0.17.0.
|
||||
|
||||
## 0.27.2 (2021-10-08)
|
||||
|
||||
### Fixes
|
||||
|
||||
Clients will now close their transport before dropping it. An attempt at a clean shutdown can help
|
||||
the server drop its connections more quickly.
|
||||
|
||||
## 0.27.1 (2021-09-22)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
### RPC error type is changing
|
||||
|
||||
RPC return types are changing from `Result<Response, io::Error>` to `Result<Response,
|
||||
tarpc::client::RpcError>`.
|
||||
|
||||
Becaue tarpc is a library, not an application, it should strive to
|
||||
use structured errors in its API so that users have maximal flexibility
|
||||
in how they handle errors. io::Error makes that hard, because it is a
|
||||
kitchen-sink error type.
|
||||
|
||||
RPCs in particular only have 3 classes of errors:
|
||||
|
||||
- The connection breaks.
|
||||
- The request expires.
|
||||
- The server decides not to process the request.
|
||||
|
||||
RPC responses can also contain application-specific errors, but from the
|
||||
perspective of the RPC library, those are opaque to the framework, classified
|
||||
as successful responsees.
|
||||
|
||||
### Open Telemetry
|
||||
|
||||
The Opentelemetry dependency is updated to version 0.16.x.
|
||||
|
||||
## 0.27.0 (2021-09-22)
|
||||
|
||||
This version was yanked due to tarpc-plugins version mismatches.
|
||||
|
||||
|
||||
## 0.26.0 (2021-04-14)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Tracing
|
||||
|
||||
tarpc is now instrumented with tracing primitives extended with
|
||||
OpenTelemetry traces. Using a compatible tracing-opentelemetry
|
||||
subscriber like Jaeger, each RPC can be traced through the client,
|
||||
server, amd other dependencies downstream of the server. Even for
|
||||
applications not connected to a distributed tracing collector, the
|
||||
instrumentation can also be ingested by regular loggers like env_logger.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
#### Logging
|
||||
|
||||
Logged events are now structured using tracing. For applications using a
|
||||
logger and not a tracing subscriber, these logs may look different or
|
||||
contain information in a less consumable manner. The easiest solution is
|
||||
to add a tracing subscriber that logs to stdout, such as
|
||||
tracing_subscriber::fmt.
|
||||
|
||||
#### Context
|
||||
|
||||
- Context no longer has parent_span, which was actually never needed,
|
||||
because the context sent in an RPC is inherently the parent context.
|
||||
For purposes of distributed tracing, the client side of the RPC has all
|
||||
necessary information to link the span to its parent; the server side
|
||||
need do nothing more than export the (trace ID, span ID) tuple.
|
||||
- Context has a new field, SamplingDecision, which has two variants,
|
||||
Sampled and Unsampled. This field can be used by downstream systems to
|
||||
determine whether a trace needs to be exported. If the parent span is
|
||||
sampled, the expectation is that all child spans be exported, as well;
|
||||
to do otherwise could result in lossy traces being exported. Note that
|
||||
if an Openetelemetry tracing subscriber is not installed, the fallback
|
||||
context will still be used, but the Context's sampling decision will
|
||||
always be inherited by the parent Context's sampling decision.
|
||||
- Context::scope has been removed. Context propagation is now done via
|
||||
tracing's task-local spans. Spans can be propagated across tasks via
|
||||
Span::in_scope. When a service receives a request, it attaches an
|
||||
Opentelemetry context to the local Span created before request handling,
|
||||
and this context contains the request deadline. This span-local deadline
|
||||
is retrieved by Context::current, but it cannot be modified so that
|
||||
future Context::current calls contain a different deadline. However, the
|
||||
deadline in the context passed into an RPC call will override it, so
|
||||
users can retrieve the current context and then modify the deadline
|
||||
field, as has been historically possible.
|
||||
- Context propgation precedence changes: when an RPC is initiated, the
|
||||
current Span's Opentelemetry context takes precedence over the trace
|
||||
context passed into the RPC method. If there is no current Span, then
|
||||
the trace context argument is used as it has been historically. Note
|
||||
that Opentelemetry context propagation requires an Opentelemetry
|
||||
tracing subscriber to be installed.
|
||||
|
||||
#### Server
|
||||
|
||||
- The server::Channel trait now has an additional required associated
|
||||
type and method which returns the underlying transport. This makes it
|
||||
more ergonomic for users to retrieve transport-specific information,
|
||||
like IP Address. BaseChannel implements Channel::transport by returning
|
||||
the underlying transport, and channel decorators like Throttler just
|
||||
delegate to the Channel::transport method of the wrapped channel.
|
||||
|
||||
#### Client
|
||||
|
||||
- NewClient::spawn no longer returns a result, as spawn can't fail.
|
||||
|
||||
### References
|
||||
|
||||
1. https://github.com/tokio-rs/tracing
|
||||
2. https://opentelemetry.io
|
||||
3. https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-jaeger
|
||||
4. https://github.com/env-logger-rs/env_logger
|
||||
|
||||
## 0.25.0 (2021-03-10)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
#### Major server module refactoring
|
||||
|
||||
1. Renames
|
||||
|
||||
Some of the items in this module were renamed to be less generic:
|
||||
|
||||
- Handler => Incoming
|
||||
- ClientHandler => Requests
|
||||
- ResponseHandler => InFlightRequest
|
||||
- Channel::{respond_with => requests}
|
||||
|
||||
In the case of Handler: handler of *what*? Now it's a bit clearer that this is a stream of Channels
|
||||
(aka *incoming* connections).
|
||||
|
||||
Similarly, ClientHandler was a stream of requests over a single connection. Hopefully Requests
|
||||
better reflects that.
|
||||
|
||||
ResponseHandler was renamed InFlightRequest because it no longer contains the serving function.
|
||||
Instead, it is just the request, plus the response channel and an abort hook. As a result of this,
|
||||
Channel::respond_with underwent a big change: it used to take the serving function and return a
|
||||
ClientHandler; now it has been renamed Channel::requests and does not take any args.
|
||||
|
||||
2. Execute methods
|
||||
|
||||
All methods thats actually result in responses being generated have been consolidated into methods
|
||||
named `execute`:
|
||||
|
||||
- InFlightRequest::execute returns a future that completes when a response has been generated and
|
||||
sent to the server Channel.
|
||||
- Requests::execute automatically spawns response handlers for all requests over a single channel.
|
||||
- Channel::execute is a convenience for `channel.requests().execute()`.
|
||||
- Incoming::execute automatically spawns response handlers for all requests over all channels.
|
||||
|
||||
3. Removal of Server.
|
||||
|
||||
server::Server was removed, as it provided no value over the Incoming/Channel abstractions.
|
||||
Additionally, server::new was removed, since it just returned a Server.
|
||||
|
||||
#### Client RPC methods now take &self
|
||||
|
||||
This required the breaking change of removing the Client trait. The intent of the Client trait was
|
||||
to facilitate the decorator pattern by allowing users to create their own Clients that added
|
||||
behavior on top of the base client. Unfortunately, this trait had become a maintenance burden,
|
||||
consistently causing issues with lifetimes and the lack of generic associated types. Specifically,
|
||||
it meant that Client impls could not use async fns, which is no longer tenable today, with channel
|
||||
libraries moving to async fns.
|
||||
|
||||
#### Servers no longer send deadline-exceed responses.
|
||||
|
||||
The deadline-exceeded response was largely redundant, because the client
|
||||
shouldn't normally be waiting for such a response, anyway -- the normal
|
||||
client will automatically remove the in-flight request when it reaches
|
||||
the deadline.
|
||||
|
||||
This also allows for internalizing the expiration+cleanup logic entirely
|
||||
within BaseChannel, without having it leak into the Channel trait and
|
||||
requiring action taken by the Requests struct.
|
||||
|
||||
#### Clients no longer send cancel messages when the request deadline is exceeded.
|
||||
|
||||
The server already knows when the request deadline was exceeded, so the client didn't need to inform
|
||||
it.
|
||||
|
||||
### Fixes
|
||||
|
||||
- When a channel is dropped, all in-flight requests for that channel are now aborted.
|
||||
|
||||
## 0.24.1 (2020-12-28)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Upgrades tokio to 1.0.
|
||||
|
||||
## 0.24.0 (2020-12-28)
|
||||
|
||||
This release was yanked.
|
||||
|
||||
## 0.23.0 (2020-10-19)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Upgrades tokio to 0.3.
|
||||
|
||||
## 0.22.0 (2020-08-02)
|
||||
|
||||
This release adds some flexibility and consistency to `serde_transport`, with one new feature and
|
||||
one small breaking change.
|
||||
|
||||
### New Features
|
||||
|
||||
`serde_transport::tcp` now exposes framing configuration on `connect()` and `listen()`. This is
|
||||
useful if, for instance, you want to send requests or responses that are larger than the maximum
|
||||
payload allowed by default:
|
||||
|
||||
```rust
|
||||
let mut transport = tarpc::serde_transport::tcp::connect(server_addr, Json::default);
|
||||
transport.config_mut().max_frame_length(4294967296);
|
||||
let mut client = MyClient::new(client::Config::default(), transport.await?).spawn()?;
|
||||
```
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
The codec argument to `serde_transport::tcp::connect` changed from a Codec to impl Fn() -> Codec,
|
||||
to be consistent with `serde_transport::tcp::listen`. While only one Codec is needed, more than one
|
||||
person has been tripped up by the inconsistency between `connect` and `listen`. Unfortunately, the
|
||||
compiler errors are not much help in this case, so it was decided to simply do the more intuitive
|
||||
thing so that the compiler doesn't need to step in in the first place.
|
||||
|
||||
|
||||
## 0.21.1 (2020-08-02)
|
||||
|
||||
### New Features
|
||||
|
||||
#### #[tarpc::server] diagnostics
|
||||
|
||||
When a service impl uses #[tarpc::server], only `async fn`s are re-written. This can lead to
|
||||
confusing compiler errors about missing associated types:
|
||||
|
||||
```
|
||||
error: not all trait items implemented, missing: `HelloFut`
|
||||
--> $DIR/tarpc_server_missing_async.rs:9:1
|
||||
|
|
||||
9 | impl World for HelloServer {
|
||||
| ^^^^
|
||||
```
|
||||
|
||||
The proc macro now provides better diagnostics for this case:
|
||||
|
||||
```
|
||||
error: not all trait items implemented, missing: `HelloFut`
|
||||
--> $DIR/tarpc_server_missing_async.rs:9:1
|
||||
|
|
||||
9 | impl World for HelloServer {
|
||||
| ^^^^
|
||||
|
||||
error: hint: `#[tarpc::server]` only rewrites async fns, and `fn hello` is not async
|
||||
--> $DIR/tarpc_server_missing_async.rs:10:5
|
||||
|
|
||||
10 | fn hello(name: String) -> String {
|
||||
| ^^
|
||||
```
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
#### Fixed client hanging when server shuts down
|
||||
|
||||
Previously, clients would ignore when the read half of the transport was closed, continuing to
|
||||
write requests. This didn't make much sense, because without the ability to receive responses,
|
||||
clients have no way to know if requests were actually processed by the server. It basically just
|
||||
led to clients that would hang for a few seconds before shutting down. This has now been
|
||||
corrected: clients will immediately shut down when the read-half of the transport is closed.
|
||||
|
||||
#### More docs.rs documentation
|
||||
|
||||
Previously, docs.rs only documented items enabled by default, notably leaving out documentation
|
||||
for tokio and serde features. This has now been corrected: docs.rs should have documentation
|
||||
for all optional features.
|
||||
|
||||
## 0.21.0 (2020-06-26)
|
||||
|
||||
### New Features
|
||||
|
||||
A new proc macro, `#[tarpc::server]` was added! This enables service impls to elide the boilerplate
|
||||
of specifying associated types for each RPC. With the ubiquity of async-await, most code won't have
|
||||
nameable futures and will just be boxing the return type anyway. This macro does that for you.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Enums had `_non_exhaustive` fields replaced with the #[non_exhaustive] attribute.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- https://github.com/google/tarpc/issues/304
|
||||
|
||||
A race condition in code that limits number of connections per client caused occasional panics.
|
||||
|
||||
- https://github.com/google/tarpc/pull/295
|
||||
|
||||
Made request timeouts account for time spent in the outbound buffer. Previously, a large outbound
|
||||
queue would lead to requests not timing out correctly.
|
||||
|
||||
## 0.20.0 (2019-12-11)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
1. tarpc has updated its tokio dependency to the latest 0.2 version.
|
||||
2. The tarpc crates have been unified into just `tarpc`, with new Cargo features to enable
|
||||
functionality.
|
||||
- The bincode-transport and json-transport crates are deprecated and superseded by
|
||||
the `serde_transport` module, which unifies much of the logic present in both crates.
|
||||
|
||||
## 0.13.0 (2018-10-16)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Version 0.13 marks a significant departure from previous versions of tarpc. The
|
||||
API has changed significantly. The tokio-proto crate has been torn out and
|
||||
replaced with a homegrown rpc framework. Additionally, the crate has been
|
||||
modularized, so that the tarpc crate itself contains only the macro code.
|
||||
|
||||
### New Crates
|
||||
|
||||
- crate rpc contains the core client/server request-response framework, as well as a transport trait.
|
||||
- crate bincode-transport implements a transport that works almost exactly as tarpc works today (not to say it's wire-compatible).
|
||||
- crate trace has some foundational types for tracing. This isn't really fleshed out yet, but it's useful for in-process log tracing, at least.
|
||||
|
||||
All crates are now at the top level. e.g. tarpc-plugins is now tarpc/plugins rather than tarpc/src/plugins. tarpc itself is now a *very* small code surface, as most functionality has been moved into the other more granular crates.
|
||||
|
||||
### New Features
|
||||
- deadlines: all requests specify a deadline, and a server will stop processing a response when past its deadline.
|
||||
- client cancellation propagation: when a client drops a request, the client sends a message to the server informing it to cancel its response. This means cancellations can propagate across multiple server hops.
|
||||
- trace context stuff as mentioned above
|
||||
- more server configuration for total connection limits, per-connection request limits, etc.
|
||||
|
||||
### Removals
|
||||
- no more shutdown handle. I left it out for now because of time and not being sure what the right solution is.
|
||||
- all async now, no blocking stub or server interface. This helps with maintainability, and async/await makes async code much more usable. The service trait is thusly renamed Service, and the client is renamed Client.
|
||||
- no built-in transport. Tarpc is now transport agnostic (see bincode-transport for transitioning existing uses).
|
||||
- going along with the previous bullet, no preferred transport means no TLS support at this time. We could make a tls transport or make bincode-transport compatible with TLS.
|
||||
- a lot of examples were removed because I couldn't keep up with maintaining all of them. Hopefully the ones I kept are still illustrative.
|
||||
- no more plugins!
|
||||
|
||||
## 0.10.0 (2018-04-08)
|
||||
|
||||
### Breaking Changes
|
||||
Fixed rustc breakage in tarpc-plugins. These changes require a recent version of rustc.
|
||||
|
||||
## 0.10.0 (2018-03-26)
|
||||
|
||||
### Breaking Changes
|
||||
Updates bincode to version 1.0.
|
||||
|
||||
## 0.9.0 (2017-09-17)
|
||||
|
||||
### Breaking Changes
|
||||
Updates tarpc to use tarpc-plugins 0.2.
|
||||
|
||||
## 0.8.0 (2017-05-05)
|
||||
|
||||
### Breaking Changes
|
||||
This release updates tarpc to use serde 1.0.
|
||||
As such, users must also update to use serde 1.0.
|
||||
The serde 1.0 [release notes](https://github.com/serde-rs/serde/releases/tag/v1.0.0)
|
||||
detail migration paths.
|
||||
|
||||
## 0.7.3 (2017-04-26)
|
||||
|
||||
This release removes the `Sync` bound on RPC args for both sync and future
|
||||
clients. No breaking changes.
|
||||
|
||||
## 0.7.2 (2017-04-22)
|
||||
|
||||
### Breaking Changes
|
||||
This release updates tarpc-plugins to work with rustc master. Thus, older
|
||||
versions of rustc are no longer supported. We chose a minor version bump
|
||||
because it is still source-compatible with existing code using tarpc.
|
||||
|
||||
## 0.7.1 (2017-03-31)
|
||||
|
||||
This release was purely doc fixes. No breaking changes.
|
||||
|
||||
## 0.7 (2017-03-31)
|
||||
|
||||
### Breaking Changes
|
||||
This release is a complete overhaul to build tarpc on top of the tokio stack.
|
||||
It's safe to assume that everything broke with this release.
|
||||
|
||||
Two traits are now generated by the macro, `FutureService` and `SyncService`.
|
||||
`SyncService` is the successor to the original `Service` trait. It uses a configurable
|
||||
thread pool to serve requests. `FutureService`, as the name implies, uses futures
|
||||
to serve requests and is single-threaded by default.
|
||||
|
||||
The easiest way to upgrade from a 0.6 service impl is to `impl SyncService for MyService`.
|
||||
For more complete information, see the readme and the examples directory.
|
||||
|
||||
## 0.6 (2016-08-07)
|
||||
|
||||
### Breaking Changes
|
||||
* Updated serde to 0.8. Requires dependents to update as well.
|
||||
|
||||
## 0.5 (2016-04-24)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
40
example-service/Cargo.toml
Normal file
40
example-service/Cargo.toml
Normal file
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "tarpc-example-service"
|
||||
version = "0.10.0"
|
||||
rust-version = "1.56"
|
||||
authors = ["Tim Kuehn <tikue@google.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
documentation = "https://docs.rs/tarpc-example-service"
|
||||
homepage = "https://github.com/google/tarpc"
|
||||
repository = "https://github.com/google/tarpc"
|
||||
keywords = ["rpc", "network", "server", "microservices", "example"]
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
readme = "../README.md"
|
||||
description = "An example server built on tarpc."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
||||
log = "0.4"
|
||||
futures = "0.3"
|
||||
opentelemetry = { version = "0.16", features = ["rt-tokio"] }
|
||||
opentelemetry-jaeger = { version = "0.15", features = ["rt-tokio"] }
|
||||
rand = "0.8"
|
||||
tarpc = { version = "0.28", path = "../tarpc", features = ["full"] }
|
||||
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
|
||||
tracing = { version = "0.1" }
|
||||
tracing-opentelemetry = "0.15"
|
||||
tracing-subscriber = "0.2"
|
||||
|
||||
[lib]
|
||||
name = "service"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
path = "src/server.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "client"
|
||||
path = "src/client.rs"
|
||||
52
example-service/src/client.rs
Normal file
52
example-service/src/client.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use clap::Parser;
|
||||
use service::{init_tracing, WorldClient};
|
||||
use std::{net::SocketAddr, time::Duration};
|
||||
use tarpc::{client, context, tokio_serde::formats::Json};
|
||||
use tokio::time::sleep;
|
||||
use tracing::Instrument;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Flags {
|
||||
/// Sets the server address to connect to.
|
||||
#[clap(long)]
|
||||
server_addr: SocketAddr,
|
||||
/// Sets the name to say hello to.
|
||||
#[clap(long)]
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let flags = Flags::parse();
|
||||
init_tracing("Tarpc Example Client")?;
|
||||
|
||||
let transport = tarpc::serde_transport::tcp::connect(flags.server_addr, Json::default);
|
||||
|
||||
// WorldClient is generated by the service attribute. It has a constructor `new` that takes a
|
||||
// config and any Transport as input.
|
||||
let client = WorldClient::new(client::Config::default(), transport.await?).spawn();
|
||||
|
||||
let hello = async move {
|
||||
// Send the request twice, just to be safe! ;)
|
||||
tokio::select! {
|
||||
hello1 = client.hello(context::current(), format!("{}1", flags.name)) => { hello1 }
|
||||
hello2 = client.hello(context::current(), format!("{}2", flags.name)) => { hello2 }
|
||||
}
|
||||
}
|
||||
.instrument(tracing::info_span!("Two Hellos"))
|
||||
.await;
|
||||
|
||||
tracing::info!("{:?}", hello);
|
||||
|
||||
// Let the background span processor finish.
|
||||
sleep(Duration::from_micros(1)).await;
|
||||
opentelemetry::global::shutdown_tracer_provider();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
34
example-service/src/lib.rs
Normal file
34
example-service/src/lib.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::env;
|
||||
use tracing_subscriber::{fmt::format::FmtSpan, prelude::*};
|
||||
|
||||
/// This is the service definition. It looks a lot like a trait definition.
|
||||
/// It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
#[tarpc::service]
|
||||
pub trait World {
|
||||
/// Returns a greeting for name.
|
||||
async fn hello(name: String) -> String;
|
||||
}
|
||||
|
||||
/// Initializes an OpenTelemetry tracing subscriber with a Jaeger backend.
|
||||
pub fn init_tracing(service_name: &str) -> anyhow::Result<()> {
|
||||
env::set_var("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "12");
|
||||
|
||||
let tracer = opentelemetry_jaeger::new_pipeline()
|
||||
.with_service_name(service_name)
|
||||
.with_max_packet_size(2usize.pow(13))
|
||||
.install_batch(opentelemetry::runtime::Tokio)?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer().with_span_events(FmtSpan::NEW | FmtSpan::CLOSE))
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
76
example-service/src/server.rs
Normal file
76
example-service/src/server.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use clap::Parser;
|
||||
use futures::{future, prelude::*};
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
thread_rng,
|
||||
};
|
||||
use service::{init_tracing, World};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv6Addr, SocketAddr},
|
||||
time::Duration,
|
||||
};
|
||||
use tarpc::{
|
||||
context,
|
||||
server::{self, incoming::Incoming, Channel},
|
||||
tokio_serde::formats::Json,
|
||||
};
|
||||
use tokio::time;
|
||||
|
||||
#[derive(Parser)]
|
||||
struct Flags {
|
||||
/// Sets the port number to listen on.
|
||||
#[clap(long)]
|
||||
port: u16,
|
||||
}
|
||||
|
||||
// This is the type that implements the generated World trait. It is the business logic
|
||||
// and is used to start the server.
|
||||
#[derive(Clone)]
|
||||
struct HelloServer(SocketAddr);
|
||||
|
||||
#[tarpc::server]
|
||||
impl World for HelloServer {
|
||||
async fn hello(self, _: context::Context, name: String) -> String {
|
||||
let sleep_time =
|
||||
Duration::from_millis(Uniform::new_inclusive(1, 10).sample(&mut thread_rng()));
|
||||
time::sleep(sleep_time).await;
|
||||
format!("Hello, {name}! You are connected from {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let flags = Flags::parse();
|
||||
init_tracing("Tarpc Example Server")?;
|
||||
|
||||
let server_addr = (IpAddr::V6(Ipv6Addr::LOCALHOST), flags.port);
|
||||
|
||||
// JSON transport is provided by the json_transport tarpc module. It makes it easy
|
||||
// to start up a serde-powered json serialization strategy over TCP.
|
||||
let mut listener = tarpc::serde_transport::tcp::listen(&server_addr, Json::default).await?;
|
||||
listener.config_mut().max_frame_length(usize::MAX);
|
||||
listener
|
||||
// Ignore accept errors.
|
||||
.filter_map(|r| future::ready(r.ok()))
|
||||
.map(server::BaseChannel::with_defaults)
|
||||
// Limit channels to 1 per IP.
|
||||
.max_channels_per_key(1, |t| t.transport().peer_addr().unwrap().ip())
|
||||
// serve is generated by the service attribute. It takes as input any type implementing
|
||||
// the generated World trait.
|
||||
.map(|channel| {
|
||||
let server = HelloServer(channel.transport().peer_addr().unwrap());
|
||||
channel.execute(server.serve())
|
||||
})
|
||||
// Max 10 channels.
|
||||
.buffer_unordered(10)
|
||||
.for_each(|_| async {})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -89,13 +89,14 @@ fi
|
||||
# not only contain discrete files.
|
||||
printf "${PREFIX} Checking formatting ... "
|
||||
FMTRESULT=0
|
||||
diff=""
|
||||
for file in $(git diff --name-only --cached);
|
||||
do
|
||||
if [ ${file: -3} == ".rs" ]; then
|
||||
diff=$(rustfmt --skip-children --write-mode=diff $file)
|
||||
if grep --quiet "^Diff at line" <<< "$diff"; then
|
||||
FMTRESULT=1
|
||||
fi
|
||||
diff="$diff$(rustfmt --edition 2018 --check $file)"
|
||||
if [ $? != 0 ]; then
|
||||
FMTRESULT=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -104,7 +105,7 @@ if [ "${TARPC_SKIP_RUSTFMT}" == 1 ]; then
|
||||
elif [ ${FMTRESULT} != 0 ]; then
|
||||
FAILED=1
|
||||
printf "${FAILURE}\n"
|
||||
echo "$diff" | sed '/Using rustfmt.*$/d'
|
||||
echo "$diff"
|
||||
else
|
||||
printf "${SUCCESS}\n"
|
||||
fi
|
||||
|
||||
112
hooks/pre-push
112
hooks/pre-push
@@ -20,11 +20,6 @@
|
||||
# copy. Set to 0 by default, since the intent is to test the code that's being pushed, not changes
|
||||
# still in the working copy.
|
||||
#
|
||||
# - TARPC_USE_CURRENT_TOOLCHAIN, default = 0
|
||||
#
|
||||
# Setting this variable to 1 will just run cargo build and cargo test, rather than running
|
||||
# stable/beta/nightly.
|
||||
#
|
||||
# Note that these options are most useful for testing the hooks themselves. Use git push --no-verify
|
||||
# to skip the pre-push hook altogether.
|
||||
|
||||
@@ -42,86 +37,67 @@ SUCCESS="${GREEN}ok${NC}"
|
||||
printf "${PREFIX} Clean working copy ... "
|
||||
git diff --exit-code &>/dev/null
|
||||
if [ "$?" == 0 ]; then
|
||||
printf "${SUCCESS}\n"
|
||||
printf "${SUCCESS}\n"
|
||||
else
|
||||
if [ "${TARPC_ALLOW_DIRTY}" == "1" ]
|
||||
then
|
||||
printf "${SKIPPED}\n"
|
||||
else
|
||||
printf "${FAILURE}\n"
|
||||
exit 1
|
||||
fi
|
||||
if [ "${TARPC_ALLOW_DIRTY}" == "1" ]
|
||||
then
|
||||
printf "${SKIPPED}\n"
|
||||
else
|
||||
printf "${FAILURE}\n"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
PREPUSH_RESULT=0
|
||||
|
||||
# args:
|
||||
# 1 - cargo command to run (build/test)
|
||||
# 2 - directory name of crate to build
|
||||
# 3 - rust toolchain (nightly/stable/beta)
|
||||
run_cargo() {
|
||||
if [ "$1" == "build" ]; then
|
||||
VERB=Building
|
||||
else
|
||||
VERB=Testing
|
||||
fi
|
||||
if [ "$3" != "" ]; then
|
||||
printf "${PREFIX} $VERB $2 on $3 ... "
|
||||
rustup run $3 cargo $1 --manifest-path $2/Cargo.toml &>/dev/null
|
||||
else
|
||||
printf "${PREFIX} $VERB $2 ... "
|
||||
cargo $1 --manifest-path $2/Cargo.toml &>/dev/null
|
||||
fi
|
||||
if [ "$?" != "0" ]; then
|
||||
printf "${FAILURE}\n"
|
||||
try_run() {
|
||||
TEXT=$1
|
||||
shift
|
||||
printf "${PREFIX} ${TEXT}"
|
||||
OUTPUT=$($@ 2>&1)
|
||||
if [ "$?" != "0" ]; then
|
||||
printf "${FAILURE}, output shown below\n"
|
||||
printf "\n\n"
|
||||
printf "$OUTPUT"
|
||||
printf "\n\n"
|
||||
PREPUSH_RESULT=1
|
||||
else
|
||||
printf "${SUCCESS}\n"
|
||||
fi
|
||||
return 1
|
||||
else
|
||||
printf "${SUCCESS}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
TOOLCHAIN_RESULT=0
|
||||
check_toolchain() {
|
||||
printf "${PREFIX} Checking for $1 toolchain ... "
|
||||
if [[ $(rustup toolchain list) =~ $1 ]]; then
|
||||
printf "${SUCCESS}\n"
|
||||
else
|
||||
TOOLCHAIN_RESULT=1
|
||||
PREPUSH_RESULT=1
|
||||
printf "${FAILURE}\n"
|
||||
fi
|
||||
printf "${PREFIX} Checking for $1 toolchain ... "
|
||||
if [[ $(rustup toolchain list) =~ $1 ]]; then
|
||||
printf "${SUCCESS}\n"
|
||||
else
|
||||
TOOLCHAIN_RESULT=1
|
||||
PREPUSH_RESULT=1
|
||||
printf "${FAILURE}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
printf "${PREFIX} Checking for rustup ... "
|
||||
printf "${PREFIX} Checking for rustup or current toolchain directive... "
|
||||
command -v rustup &>/dev/null
|
||||
if [ "$?" == 0 ] && [ "${TARPC_USE_CURRENT_TOOLCHAIN}" == "" ]; then
|
||||
printf "${SUCCESS}\n"
|
||||
if [ "$?" == 0 ]; then
|
||||
printf "${SUCCESS}\n"
|
||||
|
||||
check_toolchain stable
|
||||
check_toolchain beta
|
||||
check_toolchain nightly
|
||||
if [ ${TOOLCHAIN_RESULT} == 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
try_run "Building ... " cargo +stable build --color=always
|
||||
try_run "Testing ... " cargo +stable test --color=always
|
||||
try_run "Testing with all features enabled ... " cargo +stable test --all-features --color=always
|
||||
for EXAMPLE in $(cargo +stable run --example 2>&1 | grep ' ' | awk '{print $1}')
|
||||
do
|
||||
try_run "Running example \"$EXAMPLE\" ... " cargo +stable run --example $EXAMPLE
|
||||
done
|
||||
|
||||
run_cargo build tarpc stable
|
||||
run_cargo build tarpc_examples stable
|
||||
check_toolchain nightly
|
||||
if [ ${TOOLCHAIN_RESULT} != 1 ]; then
|
||||
try_run "Running clippy ... " cargo +nightly clippy --color=always -Z unstable-options -- --deny warnings
|
||||
fi
|
||||
|
||||
run_cargo build tarpc beta
|
||||
run_cargo build tarpc_examples beta
|
||||
|
||||
run_cargo build tarpc nightly
|
||||
run_cargo build tarpc_examples nightly
|
||||
|
||||
# We still rely on some nightly stuff for tests
|
||||
run_cargo test tarpc nightly
|
||||
run_cargo test tarpc_examples nightly
|
||||
else
|
||||
printf "${YELLOW}NOT FOUND${NC}\n"
|
||||
printf "${WARNING} Falling back to current toolchain: $(rustc -V)\n"
|
||||
|
||||
run_cargo test tarpc
|
||||
run_cargo test tarpc_examples
|
||||
fi
|
||||
|
||||
exit $PREPUSH_RESULT
|
||||
|
||||
34
plugins/Cargo.toml
Normal file
34
plugins/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "tarpc-plugins"
|
||||
version = "0.12.0"
|
||||
rust-version = "1.56"
|
||||
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
documentation = "https://docs.rs/tarpc-plugins"
|
||||
homepage = "https://github.com/google/tarpc"
|
||||
repository = "https://github.com/google/tarpc"
|
||||
keywords = ["rpc", "network", "server", "api", "microservices"]
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
readme = "../README.md"
|
||||
description = "Proc macros for tarpc."
|
||||
|
||||
[features]
|
||||
serde1 = []
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "google/tarpc" }
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "1.0", features = ["full"] }
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dev-dependencies]
|
||||
assert-type-eq = "0.1.0"
|
||||
futures = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tarpc = { path = "../tarpc", features = ["serde1"] }
|
||||
1
plugins/rustfmt.toml
Normal file
1
plugins/rustfmt.toml
Normal file
@@ -0,0 +1 @@
|
||||
edition = "2018"
|
||||
825
plugins/src/lib.rs
Normal file
825
plugins/src/lib.rs
Normal file
@@ -0,0 +1,825 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
#![recursion_limit = "512"]
|
||||
|
||||
extern crate proc_macro;
|
||||
extern crate proc_macro2;
|
||||
extern crate quote;
|
||||
extern crate syn;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::{
|
||||
braced,
|
||||
ext::IdentExt,
|
||||
parenthesized,
|
||||
parse::{Parse, ParseStream},
|
||||
parse_macro_input, parse_quote, parse_str,
|
||||
spanned::Spanned,
|
||||
token::Comma,
|
||||
Attribute, FnArg, Ident, ImplItem, ImplItemMethod, ImplItemType, ItemImpl, Lit, LitBool,
|
||||
MetaNameValue, Pat, PatType, ReturnType, Token, Type, Visibility,
|
||||
};
|
||||
|
||||
/// Accumulates multiple errors into a result.
|
||||
/// Only use this for recoverable errors, i.e. non-parse errors. Fatal errors should early exit to
|
||||
/// avoid further complications.
|
||||
macro_rules! extend_errors {
|
||||
($errors: ident, $e: expr) => {
|
||||
match $errors {
|
||||
Ok(_) => $errors = Err($e),
|
||||
Err(ref mut errors) => errors.extend($e),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct Service {
|
||||
attrs: Vec<Attribute>,
|
||||
vis: Visibility,
|
||||
ident: Ident,
|
||||
rpcs: Vec<RpcMethod>,
|
||||
}
|
||||
|
||||
struct RpcMethod {
|
||||
attrs: Vec<Attribute>,
|
||||
ident: Ident,
|
||||
args: Vec<PatType>,
|
||||
output: ReturnType,
|
||||
}
|
||||
|
||||
impl Parse for Service {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
let vis = input.parse()?;
|
||||
input.parse::<Token![trait]>()?;
|
||||
let ident: Ident = input.parse()?;
|
||||
let content;
|
||||
braced!(content in input);
|
||||
let mut rpcs = Vec::<RpcMethod>::new();
|
||||
while !content.is_empty() {
|
||||
rpcs.push(content.parse()?);
|
||||
}
|
||||
let mut ident_errors = Ok(());
|
||||
for rpc in &rpcs {
|
||||
if rpc.ident == "new" {
|
||||
extend_errors!(
|
||||
ident_errors,
|
||||
syn::Error::new(
|
||||
rpc.ident.span(),
|
||||
format!(
|
||||
"method name conflicts with generated fn `{}Client::new`",
|
||||
ident.unraw()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if rpc.ident == "serve" {
|
||||
extend_errors!(
|
||||
ident_errors,
|
||||
syn::Error::new(
|
||||
rpc.ident.span(),
|
||||
format!("method name conflicts with generated fn `{ident}::serve`")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
ident_errors?;
|
||||
|
||||
Ok(Self {
|
||||
attrs,
|
||||
vis,
|
||||
ident,
|
||||
rpcs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for RpcMethod {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let attrs = input.call(Attribute::parse_outer)?;
|
||||
input.parse::<Token![async]>()?;
|
||||
input.parse::<Token![fn]>()?;
|
||||
let ident = input.parse()?;
|
||||
let content;
|
||||
parenthesized!(content in input);
|
||||
let mut args = Vec::new();
|
||||
let mut errors = Ok(());
|
||||
for arg in content.parse_terminated::<FnArg, Comma>(FnArg::parse)? {
|
||||
match arg {
|
||||
FnArg::Typed(captured) if matches!(&*captured.pat, Pat::Ident(_)) => {
|
||||
args.push(captured);
|
||||
}
|
||||
FnArg::Typed(captured) => {
|
||||
extend_errors!(
|
||||
errors,
|
||||
syn::Error::new(captured.pat.span(), "patterns aren't allowed in RPC args")
|
||||
);
|
||||
}
|
||||
FnArg::Receiver(_) => {
|
||||
extend_errors!(
|
||||
errors,
|
||||
syn::Error::new(arg.span(), "method args cannot start with self")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
errors?;
|
||||
let output = input.parse()?;
|
||||
input.parse::<Token![;]>()?;
|
||||
|
||||
Ok(Self {
|
||||
attrs,
|
||||
ident,
|
||||
args,
|
||||
output,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If `derive_serde` meta item is not present, defaults to cfg!(feature = "serde1").
|
||||
// `derive_serde` can only be true when serde1 is enabled.
|
||||
struct DeriveSerde(bool);
|
||||
|
||||
impl Parse for DeriveSerde {
|
||||
fn parse(input: ParseStream) -> syn::Result<Self> {
|
||||
let mut result = Ok(None);
|
||||
let mut derive_serde = Vec::new();
|
||||
let meta_items = input.parse_terminated::<MetaNameValue, Comma>(MetaNameValue::parse)?;
|
||||
for meta in meta_items {
|
||||
if meta.path.segments.len() != 1 {
|
||||
extend_errors!(
|
||||
result,
|
||||
syn::Error::new(
|
||||
meta.span(),
|
||||
"tarpc::service does not support this meta item"
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
let segment = meta.path.segments.first().unwrap();
|
||||
if segment.ident != "derive_serde" {
|
||||
extend_errors!(
|
||||
result,
|
||||
syn::Error::new(
|
||||
meta.span(),
|
||||
"tarpc::service does not support this meta item"
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
match meta.lit {
|
||||
Lit::Bool(LitBool { value: true, .. }) if cfg!(feature = "serde1") => {
|
||||
result = result.and(Ok(Some(true)))
|
||||
}
|
||||
Lit::Bool(LitBool { value: true, .. }) => {
|
||||
extend_errors!(
|
||||
result,
|
||||
syn::Error::new(
|
||||
meta.span(),
|
||||
"To enable serde, first enable the `serde1` feature of tarpc"
|
||||
)
|
||||
);
|
||||
}
|
||||
Lit::Bool(LitBool { value: false, .. }) => result = result.and(Ok(Some(false))),
|
||||
_ => extend_errors!(
|
||||
result,
|
||||
syn::Error::new(
|
||||
meta.lit.span(),
|
||||
"`derive_serde` expects a value of type `bool`"
|
||||
)
|
||||
),
|
||||
}
|
||||
derive_serde.push(meta);
|
||||
}
|
||||
if derive_serde.len() > 1 {
|
||||
for (i, derive_serde) in derive_serde.iter().enumerate() {
|
||||
extend_errors!(
|
||||
result,
|
||||
syn::Error::new(
|
||||
derive_serde.span(),
|
||||
format!(
|
||||
"`derive_serde` appears more than once (occurrence #{})",
|
||||
i + 1
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
let derive_serde = result?.unwrap_or(cfg!(feature = "serde1"));
|
||||
Ok(Self(derive_serde))
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper attribute to avoid a direct dependency on Serde.
|
||||
///
|
||||
/// Adds the following annotations to the annotated item:
|
||||
///
|
||||
/// ```rust
|
||||
/// #[derive(tarpc::serde::Serialize, tarpc::serde::Deserialize)]
|
||||
/// #[serde(crate = "tarpc::serde")]
|
||||
/// # struct Foo;
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn derive_serde(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut gen: proc_macro2::TokenStream = quote! {
|
||||
#[derive(tarpc::serde::Serialize, tarpc::serde::Deserialize)]
|
||||
#[serde(crate = "tarpc::serde")]
|
||||
};
|
||||
gen.extend(proc_macro2::TokenStream::from(item));
|
||||
proc_macro::TokenStream::from(gen)
|
||||
}
|
||||
|
||||
/// Generates:
|
||||
/// - service trait
|
||||
/// - serve fn
|
||||
/// - client stub struct
|
||||
/// - new_stub client factory fn
|
||||
/// - Request and Response enums
|
||||
/// - ResponseFut Future
|
||||
#[proc_macro_attribute]
|
||||
pub fn service(attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let derive_serde = parse_macro_input!(attr as DeriveSerde);
|
||||
let unit_type: &Type = &parse_quote!(());
|
||||
let Service {
|
||||
ref attrs,
|
||||
ref vis,
|
||||
ref ident,
|
||||
ref rpcs,
|
||||
} = parse_macro_input!(input as Service);
|
||||
|
||||
let camel_case_fn_names: &Vec<_> = &rpcs
|
||||
.iter()
|
||||
.map(|rpc| snake_to_camel(&rpc.ident.unraw().to_string()))
|
||||
.collect();
|
||||
let args: &[&[PatType]] = &rpcs.iter().map(|rpc| &*rpc.args).collect::<Vec<_>>();
|
||||
let response_fut_name = &format!("{}ResponseFut", ident.unraw());
|
||||
let derive_serialize = if derive_serde.0 {
|
||||
Some(
|
||||
quote! {#[derive(tarpc::serde::Serialize, tarpc::serde::Deserialize)]
|
||||
#[serde(crate = "tarpc::serde")]},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let methods = rpcs.iter().map(|rpc| &rpc.ident).collect::<Vec<_>>();
|
||||
let request_names = methods
|
||||
.iter()
|
||||
.map(|m| format!("{ident}.{m}"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
ServiceGenerator {
|
||||
response_fut_name,
|
||||
service_ident: ident,
|
||||
server_ident: &format_ident!("Serve{}", ident),
|
||||
response_fut_ident: &Ident::new(response_fut_name, ident.span()),
|
||||
client_ident: &format_ident!("{}Client", ident),
|
||||
request_ident: &format_ident!("{}Request", ident),
|
||||
response_ident: &format_ident!("{}Response", ident),
|
||||
vis,
|
||||
args,
|
||||
method_attrs: &rpcs.iter().map(|rpc| &*rpc.attrs).collect::<Vec<_>>(),
|
||||
method_idents: &methods,
|
||||
request_names: &*request_names,
|
||||
attrs,
|
||||
rpcs,
|
||||
return_types: &rpcs
|
||||
.iter()
|
||||
.map(|rpc| match rpc.output {
|
||||
ReturnType::Type(_, ref ty) => ty,
|
||||
ReturnType::Default => unit_type,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
arg_pats: &args
|
||||
.iter()
|
||||
.map(|args| args.iter().map(|arg| &*arg.pat).collect())
|
||||
.collect::<Vec<_>>(),
|
||||
camel_case_idents: &rpcs
|
||||
.iter()
|
||||
.zip(camel_case_fn_names.iter())
|
||||
.map(|(rpc, name)| Ident::new(name, rpc.ident.span()))
|
||||
.collect::<Vec<_>>(),
|
||||
future_types: &camel_case_fn_names
|
||||
.iter()
|
||||
.map(|name| parse_str(&format!("{name}Fut")).unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
derive_serialize: derive_serialize.as_ref(),
|
||||
}
|
||||
.into_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// generate an identifier consisting of the method name to CamelCase with
|
||||
/// Fut appended to it.
|
||||
fn associated_type_for_rpc(method: &ImplItemMethod) -> String {
|
||||
snake_to_camel(&method.sig.ident.unraw().to_string()) + "Fut"
|
||||
}
|
||||
|
||||
/// Transforms an async function into a sync one, returning a type declaration
|
||||
/// for the return type (a future).
|
||||
fn transform_method(method: &mut ImplItemMethod) -> ImplItemType {
|
||||
method.sig.asyncness = None;
|
||||
|
||||
// get either the return type or ().
|
||||
let ret = match &method.sig.output {
|
||||
ReturnType::Default => quote!(()),
|
||||
ReturnType::Type(_, ret) => quote!(#ret),
|
||||
};
|
||||
|
||||
let fut_name = associated_type_for_rpc(method);
|
||||
let fut_name_ident = Ident::new(&fut_name, method.sig.ident.span());
|
||||
|
||||
// generate the updated return signature.
|
||||
method.sig.output = parse_quote! {
|
||||
-> ::core::pin::Pin<Box<
|
||||
dyn ::core::future::Future<Output = #ret> + ::core::marker::Send
|
||||
>>
|
||||
};
|
||||
|
||||
// transform the body of the method into Box::pin(async move { body }).
|
||||
let block = method.block.clone();
|
||||
method.block = parse_quote! [{
|
||||
Box::pin(async move
|
||||
#block
|
||||
)
|
||||
}];
|
||||
|
||||
// generate and return type declaration for return type.
|
||||
let t: ImplItemType = parse_quote! {
|
||||
type #fut_name_ident = ::core::pin::Pin<Box<dyn ::core::future::Future<Output = #ret> + ::core::marker::Send>>;
|
||||
};
|
||||
|
||||
t
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn server(_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut item = syn::parse_macro_input!(input as ItemImpl);
|
||||
let span = item.span();
|
||||
|
||||
// the generated type declarations
|
||||
let mut types: Vec<ImplItemType> = Vec::new();
|
||||
let mut expected_non_async_types: Vec<(&ImplItemMethod, String)> = Vec::new();
|
||||
let mut found_non_async_types: Vec<&ImplItemType> = Vec::new();
|
||||
|
||||
for inner in &mut item.items {
|
||||
match inner {
|
||||
ImplItem::Method(method) => {
|
||||
if method.sig.asyncness.is_some() {
|
||||
// if this function is declared async, transform it into a regular function
|
||||
let typedecl = transform_method(method);
|
||||
types.push(typedecl);
|
||||
} else {
|
||||
// If it's not async, keep track of all required associated types for better
|
||||
// error reporting.
|
||||
expected_non_async_types.push((method, associated_type_for_rpc(method)));
|
||||
}
|
||||
}
|
||||
ImplItem::Type(typedecl) => found_non_async_types.push(typedecl),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) =
|
||||
verify_types_were_provided(span, &expected_non_async_types, &found_non_async_types)
|
||||
{
|
||||
return TokenStream::from(e.to_compile_error());
|
||||
}
|
||||
|
||||
// add the type declarations into the impl block
|
||||
for t in types.into_iter() {
|
||||
item.items.push(syn::ImplItem::Type(t));
|
||||
}
|
||||
|
||||
TokenStream::from(quote!(#item))
|
||||
}
|
||||
|
||||
fn verify_types_were_provided(
|
||||
span: Span,
|
||||
expected: &[(&ImplItemMethod, String)],
|
||||
provided: &[&ImplItemType],
|
||||
) -> syn::Result<()> {
|
||||
let mut result = Ok(());
|
||||
for (method, expected) in expected {
|
||||
if !provided.iter().any(|typedecl| typedecl.ident == expected) {
|
||||
let mut e = syn::Error::new(
|
||||
span,
|
||||
format!("not all trait items implemented, missing: `{expected}`"),
|
||||
);
|
||||
let fn_span = method.sig.fn_token.span();
|
||||
e.extend(syn::Error::new(
|
||||
fn_span.join(method.sig.ident.span()).unwrap_or(fn_span),
|
||||
format!(
|
||||
"hint: `#[tarpc::server]` only rewrites async fns, and `fn {}` is not async",
|
||||
method.sig.ident
|
||||
),
|
||||
));
|
||||
match result {
|
||||
Ok(_) => result = Err(e),
|
||||
Err(ref mut error) => error.extend(Some(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Things needed to generate the service items: trait, serve impl, request/response enums, and
|
||||
// the client stub.
|
||||
struct ServiceGenerator<'a> {
|
||||
service_ident: &'a Ident,
|
||||
server_ident: &'a Ident,
|
||||
response_fut_ident: &'a Ident,
|
||||
response_fut_name: &'a str,
|
||||
client_ident: &'a Ident,
|
||||
request_ident: &'a Ident,
|
||||
response_ident: &'a Ident,
|
||||
vis: &'a Visibility,
|
||||
attrs: &'a [Attribute],
|
||||
rpcs: &'a [RpcMethod],
|
||||
camel_case_idents: &'a [Ident],
|
||||
future_types: &'a [Type],
|
||||
method_idents: &'a [&'a Ident],
|
||||
request_names: &'a [String],
|
||||
method_attrs: &'a [&'a [Attribute]],
|
||||
args: &'a [&'a [PatType]],
|
||||
return_types: &'a [&'a Type],
|
||||
arg_pats: &'a [Vec<&'a Pat>],
|
||||
derive_serialize: Option<&'a TokenStream2>,
|
||||
}
|
||||
|
||||
impl<'a> ServiceGenerator<'a> {
|
||||
fn trait_service(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
attrs,
|
||||
rpcs,
|
||||
vis,
|
||||
future_types,
|
||||
return_types,
|
||||
service_ident,
|
||||
server_ident,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let types_and_fns = rpcs
|
||||
.iter()
|
||||
.zip(future_types.iter())
|
||||
.zip(return_types.iter())
|
||||
.map(
|
||||
|(
|
||||
(
|
||||
RpcMethod {
|
||||
attrs, ident, args, ..
|
||||
},
|
||||
future_type,
|
||||
),
|
||||
output,
|
||||
)| {
|
||||
let ty_doc = format!("The response future returned by [`{service_ident}::{ident}`].");
|
||||
quote! {
|
||||
#[doc = #ty_doc]
|
||||
type #future_type: std::future::Future<Output = #output>;
|
||||
|
||||
#( #attrs )*
|
||||
fn #ident(self, context: tarpc::context::Context, #( #args ),*) -> Self::#future_type;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
quote! {
|
||||
#( #attrs )*
|
||||
#vis trait #service_ident: Sized {
|
||||
#( #types_and_fns )*
|
||||
|
||||
/// Returns a serving function to use with
|
||||
/// [InFlightRequest::execute](tarpc::server::InFlightRequest::execute).
|
||||
fn serve(self) -> #server_ident<Self> {
|
||||
#server_ident { service: self }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_server(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
vis, server_ident, ..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
/// A serving function to use with [tarpc::server::InFlightRequest::execute].
|
||||
#[derive(Clone)]
|
||||
#vis struct #server_ident<S> {
|
||||
service: S,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_serve_for_server(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
request_ident,
|
||||
server_ident,
|
||||
service_ident,
|
||||
response_ident,
|
||||
response_fut_ident,
|
||||
camel_case_idents,
|
||||
arg_pats,
|
||||
method_idents,
|
||||
request_names,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
impl<S> tarpc::server::Serve<#request_ident> for #server_ident<S>
|
||||
where S: #service_ident
|
||||
{
|
||||
type Resp = #response_ident;
|
||||
type Fut = #response_fut_ident<S>;
|
||||
|
||||
fn method(&self, req: &#request_ident) -> Option<&'static str> {
|
||||
Some(match req {
|
||||
#(
|
||||
#request_ident::#camel_case_idents{..} => {
|
||||
#request_names
|
||||
}
|
||||
)*
|
||||
})
|
||||
}
|
||||
|
||||
fn serve(self, ctx: tarpc::context::Context, req: #request_ident) -> Self::Fut {
|
||||
match req {
|
||||
#(
|
||||
#request_ident::#camel_case_idents{ #( #arg_pats ),* } => {
|
||||
#response_fut_ident::#camel_case_idents(
|
||||
#service_ident::#method_idents(
|
||||
self.service, ctx, #( #arg_pats ),*
|
||||
)
|
||||
)
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_request(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
derive_serialize,
|
||||
vis,
|
||||
request_ident,
|
||||
camel_case_idents,
|
||||
args,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
/// The request sent over the wire from the client to the server.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug)]
|
||||
#derive_serialize
|
||||
#vis enum #request_ident {
|
||||
#( #camel_case_idents{ #( #args ),* } ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_response(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
derive_serialize,
|
||||
vis,
|
||||
response_ident,
|
||||
camel_case_idents,
|
||||
return_types,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
/// The response sent over the wire from the server to the client.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug)]
|
||||
#derive_serialize
|
||||
#vis enum #response_ident {
|
||||
#( #camel_case_idents(#return_types) ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn enum_response_future(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
vis,
|
||||
service_ident,
|
||||
response_fut_ident,
|
||||
camel_case_idents,
|
||||
future_types,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
/// A future resolving to a server response.
|
||||
#[allow(missing_docs)]
|
||||
#vis enum #response_fut_ident<S: #service_ident> {
|
||||
#( #camel_case_idents(<S as #service_ident>::#future_types) ),*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_debug_for_response_future(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
service_ident,
|
||||
response_fut_ident,
|
||||
response_fut_name,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
impl<S: #service_ident> std::fmt::Debug for #response_fut_ident<S> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(#response_fut_name).finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_future_for_response_future(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
service_ident,
|
||||
response_fut_ident,
|
||||
response_ident,
|
||||
camel_case_idents,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
impl<S: #service_ident> std::future::Future for #response_fut_ident<S> {
|
||||
type Output = #response_ident;
|
||||
|
||||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>)
|
||||
-> std::task::Poll<#response_ident>
|
||||
{
|
||||
unsafe {
|
||||
match std::pin::Pin::get_unchecked_mut(self) {
|
||||
#(
|
||||
#response_fut_ident::#camel_case_idents(resp) =>
|
||||
std::pin::Pin::new_unchecked(resp)
|
||||
.poll(cx)
|
||||
.map(#response_ident::#camel_case_idents),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn struct_client(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
vis,
|
||||
client_ident,
|
||||
request_ident,
|
||||
response_ident,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug)]
|
||||
/// The client stub that makes RPC calls to the server. All request methods return
|
||||
/// [Futures](std::future::Future).
|
||||
#vis struct #client_ident(tarpc::client::Channel<#request_ident, #response_ident>);
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_client_new(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
client_ident,
|
||||
vis,
|
||||
request_ident,
|
||||
response_ident,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
impl #client_ident {
|
||||
/// Returns a new client stub that sends requests over the given transport.
|
||||
#vis fn new<T>(config: tarpc::client::Config, transport: T)
|
||||
-> tarpc::client::NewClient<
|
||||
Self,
|
||||
tarpc::client::RequestDispatch<#request_ident, #response_ident, T>
|
||||
>
|
||||
where
|
||||
T: tarpc::Transport<tarpc::ClientMessage<#request_ident>, tarpc::Response<#response_ident>>
|
||||
{
|
||||
let new_client = tarpc::client::new(config, transport);
|
||||
tarpc::client::NewClient {
|
||||
client: #client_ident(new_client.client),
|
||||
dispatch: new_client.dispatch,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn impl_client_rpc_methods(&self) -> TokenStream2 {
|
||||
let &Self {
|
||||
client_ident,
|
||||
request_ident,
|
||||
response_ident,
|
||||
method_attrs,
|
||||
vis,
|
||||
method_idents,
|
||||
request_names,
|
||||
args,
|
||||
return_types,
|
||||
arg_pats,
|
||||
camel_case_idents,
|
||||
..
|
||||
} = self;
|
||||
|
||||
quote! {
|
||||
impl #client_ident {
|
||||
#(
|
||||
#[allow(unused)]
|
||||
#( #method_attrs )*
|
||||
#vis fn #method_idents(&self, ctx: tarpc::context::Context, #( #args ),*)
|
||||
-> impl std::future::Future<Output = Result<#return_types, tarpc::client::RpcError>> + '_ {
|
||||
let request = #request_ident::#camel_case_idents { #( #arg_pats ),* };
|
||||
let resp = self.0.call(ctx, #request_names, request);
|
||||
async move {
|
||||
match resp.await? {
|
||||
#response_ident::#camel_case_idents(msg) => std::result::Result::Ok(msg),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToTokens for ServiceGenerator<'a> {
|
||||
fn to_tokens(&self, output: &mut TokenStream2) {
|
||||
output.extend(vec![
|
||||
self.trait_service(),
|
||||
self.struct_server(),
|
||||
self.impl_serve_for_server(),
|
||||
self.enum_request(),
|
||||
self.enum_response(),
|
||||
self.enum_response_future(),
|
||||
self.impl_debug_for_response_future(),
|
||||
self.impl_future_for_response_future(),
|
||||
self.struct_client(),
|
||||
self.impl_client_new(),
|
||||
self.impl_client_rpc_methods(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn snake_to_camel(ident_str: &str) -> String {
|
||||
let mut camel_ty = String::with_capacity(ident_str.len());
|
||||
|
||||
let mut last_char_was_underscore = true;
|
||||
for c in ident_str.chars() {
|
||||
match c {
|
||||
'_' => last_char_was_underscore = true,
|
||||
c if last_char_was_underscore => {
|
||||
camel_ty.extend(c.to_uppercase());
|
||||
last_char_was_underscore = false;
|
||||
}
|
||||
c => camel_ty.extend(c.to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
camel_ty.shrink_to_fit();
|
||||
camel_ty
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snake_to_camel_basic() {
|
||||
assert_eq!(snake_to_camel("abc_def"), "AbcDef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snake_to_camel_underscore_suffix() {
|
||||
assert_eq!(snake_to_camel("abc_def_"), "AbcDef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snake_to_camel_underscore_prefix() {
|
||||
assert_eq!(snake_to_camel("_abc_def"), "AbcDef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snake_to_camel_underscore_consecutive() {
|
||||
assert_eq!(snake_to_camel("abc__def"), "AbcDef");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snake_to_camel_capital_in_middle() {
|
||||
assert_eq!(snake_to_camel("aBc_dEf"), "AbcDef");
|
||||
}
|
||||
144
plugins/tests/server.rs
Normal file
144
plugins/tests/server.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use assert_type_eq::assert_type_eq;
|
||||
use futures::Future;
|
||||
use std::pin::Pin;
|
||||
use tarpc::context;
|
||||
|
||||
// these need to be out here rather than inside the function so that the
|
||||
// assert_type_eq macro can pick them up.
|
||||
#[tarpc::service]
|
||||
trait Foo {
|
||||
async fn two_part(s: String, i: i32) -> (String, i32);
|
||||
async fn bar(s: String) -> String;
|
||||
async fn baz();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_generation_works() {
|
||||
#[tarpc::server]
|
||||
impl Foo for () {
|
||||
async fn two_part(self, _: context::Context, s: String, i: i32) -> (String, i32) {
|
||||
(s, i)
|
||||
}
|
||||
|
||||
async fn bar(self, _: context::Context, s: String) -> String {
|
||||
s
|
||||
}
|
||||
|
||||
async fn baz(self, _: context::Context) {}
|
||||
}
|
||||
|
||||
// the assert_type_eq macro can only be used once per block.
|
||||
{
|
||||
assert_type_eq!(
|
||||
<() as Foo>::TwoPartFut,
|
||||
Pin<Box<dyn Future<Output = (String, i32)> + Send>>
|
||||
);
|
||||
}
|
||||
{
|
||||
assert_type_eq!(
|
||||
<() as Foo>::BarFut,
|
||||
Pin<Box<dyn Future<Output = String> + Send>>
|
||||
);
|
||||
}
|
||||
{
|
||||
assert_type_eq!(
|
||||
<() as Foo>::BazFut,
|
||||
Pin<Box<dyn Future<Output = ()> + Send>>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[test]
|
||||
fn raw_idents_work() {
|
||||
type r#yield = String;
|
||||
|
||||
#[tarpc::service]
|
||||
trait r#trait {
|
||||
async fn r#await(r#struct: r#yield, r#enum: i32) -> (r#yield, i32);
|
||||
async fn r#fn(r#impl: r#yield) -> r#yield;
|
||||
async fn r#async();
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl r#trait for () {
|
||||
async fn r#await(
|
||||
self,
|
||||
_: context::Context,
|
||||
r#struct: r#yield,
|
||||
r#enum: i32,
|
||||
) -> (r#yield, i32) {
|
||||
(r#struct, r#enum)
|
||||
}
|
||||
|
||||
async fn r#fn(self, _: context::Context, r#impl: r#yield) -> r#yield {
|
||||
r#impl
|
||||
}
|
||||
|
||||
async fn r#async(self, _: context::Context) {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax() {
|
||||
#[tarpc::service]
|
||||
trait Syntax {
|
||||
#[deny(warnings)]
|
||||
#[allow(non_snake_case)]
|
||||
async fn TestCamelCaseDoesntConflict();
|
||||
async fn hello() -> String;
|
||||
#[doc = "attr"]
|
||||
async fn attr(s: String) -> String;
|
||||
async fn no_args_no_return();
|
||||
async fn no_args() -> ();
|
||||
async fn one_arg(one: String) -> i32;
|
||||
async fn two_args_no_return(one: String, two: u64);
|
||||
async fn two_args(one: String, two: u64) -> String;
|
||||
async fn no_args_ret_error() -> i32;
|
||||
async fn one_arg_ret_error(one: String) -> String;
|
||||
async fn no_arg_implicit_return_error();
|
||||
#[doc = "attr"]
|
||||
async fn one_arg_implicit_return_error(one: String);
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl Syntax for () {
|
||||
#[deny(warnings)]
|
||||
#[allow(non_snake_case)]
|
||||
async fn TestCamelCaseDoesntConflict(self, _: context::Context) {}
|
||||
|
||||
async fn hello(self, _: context::Context) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
async fn attr(self, _: context::Context, _s: String) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
async fn no_args_no_return(self, _: context::Context) {}
|
||||
|
||||
async fn no_args(self, _: context::Context) -> () {}
|
||||
|
||||
async fn one_arg(self, _: context::Context, _one: String) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
async fn two_args_no_return(self, _: context::Context, _one: String, _two: u64) {}
|
||||
|
||||
async fn two_args(self, _: context::Context, _one: String, _two: u64) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
async fn no_args_ret_error(self, _: context::Context) -> i32 {
|
||||
0
|
||||
}
|
||||
|
||||
async fn one_arg_ret_error(self, _: context::Context, _one: String) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
async fn no_arg_implicit_return_error(self, _: context::Context) {}
|
||||
|
||||
async fn one_arg_implicit_return_error(self, _: context::Context, _one: String) {}
|
||||
}
|
||||
}
|
||||
85
plugins/tests/service.rs
Normal file
85
plugins/tests/service.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use tarpc::context;
|
||||
|
||||
#[test]
|
||||
fn att_service_trait() {
|
||||
use futures::future::{ready, Ready};
|
||||
|
||||
#[tarpc::service]
|
||||
trait Foo {
|
||||
async fn two_part(s: String, i: i32) -> (String, i32);
|
||||
async fn bar(s: String) -> String;
|
||||
async fn baz();
|
||||
}
|
||||
|
||||
impl Foo for () {
|
||||
type TwoPartFut = Ready<(String, i32)>;
|
||||
fn two_part(self, _: context::Context, s: String, i: i32) -> Self::TwoPartFut {
|
||||
ready((s, i))
|
||||
}
|
||||
|
||||
type BarFut = Ready<String>;
|
||||
fn bar(self, _: context::Context, s: String) -> Self::BarFut {
|
||||
ready(s)
|
||||
}
|
||||
|
||||
type BazFut = Ready<()>;
|
||||
fn baz(self, _: context::Context) -> Self::BazFut {
|
||||
ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
#[test]
|
||||
fn raw_idents() {
|
||||
use futures::future::{ready, Ready};
|
||||
|
||||
type r#yield = String;
|
||||
|
||||
#[tarpc::service]
|
||||
trait r#trait {
|
||||
async fn r#await(r#struct: r#yield, r#enum: i32) -> (r#yield, i32);
|
||||
async fn r#fn(r#impl: r#yield) -> r#yield;
|
||||
async fn r#async();
|
||||
}
|
||||
|
||||
impl r#trait for () {
|
||||
type AwaitFut = Ready<(r#yield, i32)>;
|
||||
fn r#await(self, _: context::Context, r#struct: r#yield, r#enum: i32) -> Self::AwaitFut {
|
||||
ready((r#struct, r#enum))
|
||||
}
|
||||
|
||||
type FnFut = Ready<r#yield>;
|
||||
fn r#fn(self, _: context::Context, r#impl: r#yield) -> Self::FnFut {
|
||||
ready(r#impl)
|
||||
}
|
||||
|
||||
type AsyncFut = Ready<()>;
|
||||
fn r#async(self, _: context::Context) -> Self::AsyncFut {
|
||||
ready(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax() {
|
||||
#[tarpc::service]
|
||||
trait Syntax {
|
||||
#[deny(warnings)]
|
||||
#[allow(non_snake_case)]
|
||||
async fn TestCamelCaseDoesntConflict();
|
||||
async fn hello() -> String;
|
||||
#[doc = "attr"]
|
||||
async fn attr(s: String) -> String;
|
||||
async fn no_args_no_return();
|
||||
async fn no_args() -> ();
|
||||
async fn one_arg(one: String) -> i32;
|
||||
async fn two_args_no_return(one: String, two: u64);
|
||||
async fn two_args(one: String, two: u64) -> String;
|
||||
async fn no_args_ret_error() -> i32;
|
||||
async fn one_arg_ret_error(one: String) -> String;
|
||||
async fn no_arg_implicit_return_error();
|
||||
#[doc = "attr"]
|
||||
async fn one_arg_implicit_return_error(one: String);
|
||||
}
|
||||
}
|
||||
111
tarpc/Cargo.toml
111
tarpc/Cargo.toml
@@ -1,23 +1,110 @@
|
||||
[package]
|
||||
name = "tarpc"
|
||||
version = "0.5.1"
|
||||
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
|
||||
version = "0.28.0"
|
||||
rust-version = "1.58.0"
|
||||
authors = [
|
||||
"Adam Wright <adam.austin.wright@gmail.com>",
|
||||
"Tim Kuehn <timothy.j.kuehn@gmail.com>",
|
||||
]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
documentation = "https://google.github.io/tarpc"
|
||||
documentation = "https://docs.rs/tarpc"
|
||||
homepage = "https://github.com/google/tarpc"
|
||||
repository = "https://github.com/google/tarpc"
|
||||
keywords = ["rpc", "protocol", "remote", "procedure", "serialize"]
|
||||
keywords = ["rpc", "network", "server", "api", "microservices"]
|
||||
categories = ["asynchronous", "network-programming"]
|
||||
readme = "../README.md"
|
||||
description = "An RPC framework for Rust with a focus on ease of use."
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
serde1 = ["tarpc-plugins/serde1", "serde", "serde/derive"]
|
||||
tokio1 = ["tokio/rt"]
|
||||
serde-transport = ["serde1", "tokio1", "tokio-serde", "tokio-util/codec"]
|
||||
serde-transport-json = ["tokio-serde/json"]
|
||||
serde-transport-bincode = ["tokio-serde/bincode"]
|
||||
tcp = ["tokio/net"]
|
||||
|
||||
full = [
|
||||
"serde1",
|
||||
"tokio1",
|
||||
"serde-transport",
|
||||
"serde-transport-json",
|
||||
"serde-transport-bincode",
|
||||
"tcp",
|
||||
]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "google/tarpc" }
|
||||
|
||||
[dependencies]
|
||||
bincode = "0.6"
|
||||
log = "0.3"
|
||||
scoped-pool = "1.0"
|
||||
serde = "0.8"
|
||||
unix_socket = "0.5"
|
||||
anyhow = "1.0"
|
||||
fnv = "1.0"
|
||||
futures = "0.3"
|
||||
humantime = "2.0"
|
||||
pin-project = "1.0"
|
||||
rand = "0.8"
|
||||
serde = { optional = true, version = "1.0", features = ["derive"] }
|
||||
static_assertions = "1.1.0"
|
||||
tarpc-plugins = { path = "../plugins", version = "0.12" }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["time"] }
|
||||
tokio-util = { version = "0.6.9", features = ["time"] }
|
||||
tokio-serde = { optional = true, version = "0.8" }
|
||||
tracing = { version = "0.1", default-features = false, features = [
|
||||
"attributes",
|
||||
"log",
|
||||
] }
|
||||
tracing-opentelemetry = { version = "0.17.2", default-features = false }
|
||||
opentelemetry = { version = "0.17.0", default-features = false }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "0.2"
|
||||
env_logger = "0.3"
|
||||
tempdir = "0.3"
|
||||
assert_matches = "1.4"
|
||||
bincode = "1.3"
|
||||
bytes = { version = "1", features = ["serde"] }
|
||||
flate2 = "1.0"
|
||||
futures-test = "0.3"
|
||||
opentelemetry = { version = "0.17.0", default-features = false, features = [
|
||||
"rt-tokio",
|
||||
] }
|
||||
opentelemetry-jaeger = { version = "0.16.0", features = ["rt-tokio"] }
|
||||
pin-utils = "0.1.0-alpha"
|
||||
serde_bytes = "0.11"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tokio = { version = "1", features = ["full", "test-util"] }
|
||||
tokio-serde = { version = "0.8", features = ["json", "bincode"] }
|
||||
trybuild = "1.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[example]]
|
||||
name = "compression"
|
||||
required-features = ["serde-transport", "tcp"]
|
||||
|
||||
[[example]]
|
||||
name = "tracing"
|
||||
required-features = ["full"]
|
||||
|
||||
[[example]]
|
||||
name = "readme"
|
||||
required-features = ["full"]
|
||||
|
||||
[[example]]
|
||||
name = "pubsub"
|
||||
required-features = ["full"]
|
||||
|
||||
[[example]]
|
||||
name = "custom_transport"
|
||||
required-features = ["serde1", "tokio1", "serde-transport"]
|
||||
|
||||
[[test]]
|
||||
name = "service_functional"
|
||||
required-features = ["serde-transport"]
|
||||
|
||||
[[test]]
|
||||
name = "dataservice"
|
||||
required-features = ["serde-transport", "tcp"]
|
||||
|
||||
1
tarpc/README.md
Symbolic link
1
tarpc/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../README.md
|
||||
1
tarpc/clippy.toml
Normal file
1
tarpc/clippy.toml
Normal file
@@ -0,0 +1 @@
|
||||
doc-valid-idents = ["gRPC"]
|
||||
128
tarpc/examples/compression.rs
Normal file
128
tarpc/examples/compression.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use flate2::{read::DeflateDecoder, write::DeflateEncoder, Compression};
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt, TryStreamExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_bytes::ByteBuf;
|
||||
use std::{io, io::Read, io::Write};
|
||||
use tarpc::{
|
||||
client, context,
|
||||
serde_transport::tcp,
|
||||
server::{BaseChannel, Channel},
|
||||
tokio_serde::formats::Bincode,
|
||||
};
|
||||
|
||||
/// Type of compression that should be enabled on the request. The transport is free to ignore this.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||
pub enum CompressionAlgorithm {
|
||||
Deflate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum CompressedMessage<T> {
|
||||
Uncompressed(T),
|
||||
Compressed {
|
||||
algorithm: CompressionAlgorithm,
|
||||
payload: ByteBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
enum CompressionType {
|
||||
Uncompressed,
|
||||
Compressed,
|
||||
}
|
||||
|
||||
async fn compress<T>(message: T) -> io::Result<CompressedMessage<T>>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
let message = serialize(message)?;
|
||||
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
|
||||
encoder.write_all(&message).unwrap();
|
||||
let compressed = encoder.finish()?;
|
||||
Ok(CompressedMessage::Compressed {
|
||||
algorithm: CompressionAlgorithm::Deflate,
|
||||
payload: ByteBuf::from(compressed),
|
||||
})
|
||||
}
|
||||
|
||||
async fn decompress<T>(message: CompressedMessage<T>) -> io::Result<T>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
match message {
|
||||
CompressedMessage::Compressed { algorithm, payload } => {
|
||||
if algorithm != CompressionAlgorithm::Deflate {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("Compression algorithm {algorithm:?} not supported"),
|
||||
));
|
||||
}
|
||||
let mut deflater = DeflateDecoder::new(payload.as_slice());
|
||||
let mut payload = ByteBuf::new();
|
||||
deflater.read_to_end(&mut payload)?;
|
||||
let message = deserialize(payload)?;
|
||||
Ok(message)
|
||||
}
|
||||
CompressedMessage::Uncompressed(message) => Ok(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize<T: Serialize>(t: T) -> io::Result<ByteBuf> {
|
||||
bincode::serialize(&t)
|
||||
.map(ByteBuf::from)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
fn deserialize<D>(message: ByteBuf) -> io::Result<D>
|
||||
where
|
||||
for<'a> D: Deserialize<'a>,
|
||||
{
|
||||
bincode::deserialize(message.as_ref()).map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
fn add_compression<In, Out>(
|
||||
transport: impl Stream<Item = io::Result<CompressedMessage<In>>>
|
||||
+ Sink<CompressedMessage<Out>, Error = io::Error>,
|
||||
) -> impl Stream<Item = io::Result<In>> + Sink<Out, Error = io::Error>
|
||||
where
|
||||
Out: Serialize,
|
||||
for<'a> In: Deserialize<'a>,
|
||||
{
|
||||
transport.with(compress).and_then(decompress)
|
||||
}
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait World {
|
||||
async fn hello(name: String) -> String;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct HelloServer;
|
||||
|
||||
#[tarpc::server]
|
||||
impl World for HelloServer {
|
||||
async fn hello(self, _: context::Context, name: String) -> String {
|
||||
format!("Hey, {name}!")
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let mut incoming = tcp::listen("localhost:0", Bincode::default).await?;
|
||||
let addr = incoming.local_addr();
|
||||
tokio::spawn(async move {
|
||||
let transport = incoming.next().await.unwrap().unwrap();
|
||||
BaseChannel::with_defaults(add_compression(transport))
|
||||
.execute(HelloServer.serve())
|
||||
.await;
|
||||
});
|
||||
|
||||
let transport = tcp::connect(addr, Bincode::default).await?;
|
||||
let client = WorldClient::new(client::Config::default(), add_compression(transport)).spawn();
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
client.hello(context::current(), "friend".into()).await?
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
47
tarpc/examples/custom_transport.rs
Normal file
47
tarpc/examples/custom_transport.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use tarpc::serde_transport as transport;
|
||||
use tarpc::server::{BaseChannel, Channel};
|
||||
use tarpc::{context::Context, tokio_serde::formats::Bincode};
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
use tokio_util::codec::length_delimited::LengthDelimitedCodec;
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait PingService {
|
||||
async fn ping();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Service;
|
||||
|
||||
#[tarpc::server]
|
||||
impl PingService for Service {
|
||||
async fn ping(self, _: Context) {}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let bind_addr = "/tmp/tarpc_on_unix_example.sock";
|
||||
|
||||
let _ = std::fs::remove_file(bind_addr);
|
||||
|
||||
let listener = UnixListener::bind(bind_addr).unwrap();
|
||||
let codec_builder = LengthDelimitedCodec::builder();
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
let (conn, _addr) = listener.accept().await.unwrap();
|
||||
let framed = codec_builder.new_framed(conn);
|
||||
let transport = transport::new(framed, Bincode::default());
|
||||
|
||||
let fut = BaseChannel::with_defaults(transport).execute(Service.serve());
|
||||
tokio::spawn(fut);
|
||||
}
|
||||
});
|
||||
|
||||
let conn = UnixStream::connect(bind_addr).await?;
|
||||
let transport = transport::new(codec_builder.new_framed(conn), Bincode::default());
|
||||
PingServiceClient::new(Default::default(), transport)
|
||||
.spawn()
|
||||
.ping(tarpc::context::current())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
360
tarpc/examples/pubsub.rs
Normal file
360
tarpc/examples/pubsub.rs
Normal file
@@ -0,0 +1,360 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
/// - The PubSub server sets up TCP listeners on 2 ports, the "subscriber" port and the "publisher"
|
||||
/// port. Because both publishers and subscribers initiate their connections to the PubSub
|
||||
/// server, the server requires no prior knowledge of either publishers or subscribers.
|
||||
///
|
||||
/// - Subscribers connect to the server on the server's "subscriber" port. Once a connection is
|
||||
/// established, the server acts as the client of the Subscriber service, initially requesting
|
||||
/// the topics the subscriber is interested in, and subsequently sending topical messages to the
|
||||
/// subscriber.
|
||||
///
|
||||
/// - Publishers connect to the server on the "publisher" port and, once connected, they send
|
||||
/// topical messages via Publisher service to the server. The server then broadcasts each
|
||||
/// messages to all clients subscribed to the topic of that message.
|
||||
///
|
||||
/// Subscriber Publisher PubSub Server
|
||||
/// T1 | | |
|
||||
/// T2 |-----Connect------------------------------------------------------>|
|
||||
/// T3 | | |
|
||||
/// T2 |<-------------------------------------------------------Topics-----|
|
||||
/// T2 |-----(OK) Topics-------------------------------------------------->|
|
||||
/// T3 | | |
|
||||
/// T4 | |-----Connect-------------------->|
|
||||
/// T5 | | |
|
||||
/// T6 | |-----Publish-------------------->|
|
||||
/// T7 | | |
|
||||
/// T8 |<------------------------------------------------------Receive-----|
|
||||
/// T9 |-----(OK) Receive------------------------------------------------->|
|
||||
/// T10 | | |
|
||||
/// T11 | |<--------------(OK) Publish------|
|
||||
use anyhow::anyhow;
|
||||
use futures::{
|
||||
channel::oneshot,
|
||||
future::{self, AbortHandle},
|
||||
prelude::*,
|
||||
};
|
||||
use publisher::Publisher as _;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
error::Error,
|
||||
io,
|
||||
net::SocketAddr,
|
||||
sync::{Arc, Mutex, RwLock},
|
||||
};
|
||||
use subscriber::Subscriber as _;
|
||||
use tarpc::{
|
||||
client, context,
|
||||
serde_transport::tcp,
|
||||
server::{self, Channel},
|
||||
};
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tokio_serde::formats::Json;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
pub mod subscriber {
|
||||
#[tarpc::service]
|
||||
pub trait Subscriber {
|
||||
async fn topics() -> Vec<String>;
|
||||
async fn receive(topic: String, message: String);
|
||||
}
|
||||
}
|
||||
|
||||
pub mod publisher {
|
||||
#[tarpc::service]
|
||||
pub trait Publisher {
|
||||
async fn publish(topic: String, message: String);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Subscriber {
|
||||
local_addr: SocketAddr,
|
||||
topics: Vec<String>,
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl subscriber::Subscriber for Subscriber {
|
||||
async fn topics(self, _: context::Context) -> Vec<String> {
|
||||
self.topics.clone()
|
||||
}
|
||||
|
||||
async fn receive(self, _: context::Context, topic: String, message: String) {
|
||||
info!(local_addr = %self.local_addr, %topic, %message, "ReceivedMessage")
|
||||
}
|
||||
}
|
||||
|
||||
struct SubscriberHandle(AbortHandle);
|
||||
|
||||
impl Drop for SubscriberHandle {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl Subscriber {
|
||||
async fn connect(
|
||||
publisher_addr: impl ToSocketAddrs,
|
||||
topics: Vec<String>,
|
||||
) -> anyhow::Result<SubscriberHandle> {
|
||||
let publisher = tcp::connect(publisher_addr, Json::default).await?;
|
||||
let local_addr = publisher.local_addr()?;
|
||||
let mut handler = server::BaseChannel::with_defaults(publisher).requests();
|
||||
let subscriber = Subscriber { local_addr, topics };
|
||||
// The first request is for the topics being subscribed to.
|
||||
match handler.next().await {
|
||||
Some(init_topics) => init_topics?.execute(subscriber.clone().serve()).await,
|
||||
None => {
|
||||
return Err(anyhow!(
|
||||
"[{}] Server never initialized the subscriber.",
|
||||
local_addr
|
||||
))
|
||||
}
|
||||
};
|
||||
let (handler, abort_handle) = future::abortable(handler.execute(subscriber.serve()));
|
||||
tokio::spawn(async move {
|
||||
match handler.await {
|
||||
Ok(()) | Err(future::Aborted) => info!(?local_addr, "subscriber shutdown."),
|
||||
}
|
||||
});
|
||||
Ok(SubscriberHandle(abort_handle))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Subscription {
|
||||
subscriber: subscriber::SubscriberClient,
|
||||
topics: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Publisher {
|
||||
clients: Arc<Mutex<HashMap<SocketAddr, Subscription>>>,
|
||||
subscriptions: Arc<RwLock<HashMap<String, HashMap<SocketAddr, subscriber::SubscriberClient>>>>,
|
||||
}
|
||||
|
||||
struct PublisherAddrs {
|
||||
publisher: SocketAddr,
|
||||
subscriptions: SocketAddr,
|
||||
}
|
||||
|
||||
impl Publisher {
|
||||
async fn start(self) -> io::Result<PublisherAddrs> {
|
||||
let mut connecting_publishers = tcp::listen("localhost:0", Json::default).await?;
|
||||
|
||||
let publisher_addrs = PublisherAddrs {
|
||||
publisher: connecting_publishers.local_addr(),
|
||||
subscriptions: self.clone().start_subscription_manager().await?,
|
||||
};
|
||||
|
||||
info!(publisher_addr = %publisher_addrs.publisher, "listening for publishers.",);
|
||||
tokio::spawn(async move {
|
||||
// Because this is just an example, we know there will only be one publisher. In more
|
||||
// realistic code, this would be a loop to continually accept new publisher
|
||||
// connections.
|
||||
let publisher = connecting_publishers.next().await.unwrap().unwrap();
|
||||
info!(publisher.peer_addr = ?publisher.peer_addr(), "publisher connected.");
|
||||
|
||||
server::BaseChannel::with_defaults(publisher)
|
||||
.execute(self.serve())
|
||||
.await
|
||||
});
|
||||
|
||||
Ok(publisher_addrs)
|
||||
}
|
||||
|
||||
async fn start_subscription_manager(mut self) -> io::Result<SocketAddr> {
|
||||
let mut connecting_subscribers = tcp::listen("localhost:0", Json::default)
|
||||
.await?
|
||||
.filter_map(|r| future::ready(r.ok()));
|
||||
let new_subscriber_addr = connecting_subscribers.get_ref().local_addr();
|
||||
info!(?new_subscriber_addr, "listening for subscribers.");
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(conn) = connecting_subscribers.next().await {
|
||||
let subscriber_addr = conn.peer_addr().unwrap();
|
||||
|
||||
let tarpc::client::NewClient {
|
||||
client: subscriber,
|
||||
dispatch,
|
||||
} = subscriber::SubscriberClient::new(client::Config::default(), conn);
|
||||
let (ready_tx, ready) = oneshot::channel();
|
||||
self.clone()
|
||||
.start_subscriber_gc(subscriber_addr, dispatch, ready);
|
||||
|
||||
// Populate the topics
|
||||
self.initialize_subscription(subscriber_addr, subscriber)
|
||||
.await;
|
||||
|
||||
// Signal that initialization is done.
|
||||
ready_tx.send(()).unwrap();
|
||||
}
|
||||
});
|
||||
|
||||
Ok(new_subscriber_addr)
|
||||
}
|
||||
|
||||
async fn initialize_subscription(
|
||||
&mut self,
|
||||
subscriber_addr: SocketAddr,
|
||||
subscriber: subscriber::SubscriberClient,
|
||||
) {
|
||||
// Populate the topics
|
||||
if let Ok(topics) = subscriber.topics(context::current()).await {
|
||||
self.clients.lock().unwrap().insert(
|
||||
subscriber_addr,
|
||||
Subscription {
|
||||
subscriber: subscriber.clone(),
|
||||
topics: topics.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
info!(%subscriber_addr, ?topics, "subscribed to new topics");
|
||||
let mut subscriptions = self.subscriptions.write().unwrap();
|
||||
for topic in topics {
|
||||
subscriptions
|
||||
.entry(topic)
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(subscriber_addr, subscriber.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_subscriber_gc<E: Error>(
|
||||
self,
|
||||
subscriber_addr: SocketAddr,
|
||||
client_dispatch: impl Future<Output = Result<(), E>> + Send + 'static,
|
||||
subscriber_ready: oneshot::Receiver<()>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = client_dispatch.await {
|
||||
info!(
|
||||
%subscriber_addr,
|
||||
error = %e,
|
||||
"subscriber connection broken");
|
||||
}
|
||||
// Don't clean up the subscriber until initialization is done.
|
||||
let _ = subscriber_ready.await;
|
||||
if let Some(subscription) = self.clients.lock().unwrap().remove(&subscriber_addr) {
|
||||
info!(
|
||||
"[{} unsubscribing from topics: {:?}",
|
||||
subscriber_addr, subscription.topics
|
||||
);
|
||||
let mut subscriptions = self.subscriptions.write().unwrap();
|
||||
for topic in subscription.topics {
|
||||
let subscribers = subscriptions.get_mut(&topic).unwrap();
|
||||
subscribers.remove(&subscriber_addr);
|
||||
if subscribers.is_empty() {
|
||||
subscriptions.remove(&topic);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl publisher::Publisher for Publisher {
|
||||
async fn publish(self, _: context::Context, topic: String, message: String) {
|
||||
info!("received message to publish.");
|
||||
let mut subscribers = match self.subscriptions.read().unwrap().get(&topic) {
|
||||
None => return,
|
||||
Some(subscriptions) => subscriptions.clone(),
|
||||
};
|
||||
let mut publications = Vec::new();
|
||||
for client in subscribers.values_mut() {
|
||||
publications.push(client.receive(context::current(), topic.clone(), message.clone()));
|
||||
}
|
||||
// Ignore failing subscribers. In a real pubsub, you'd want to continually retry until
|
||||
// subscribers ack. Of course, a lot would be different in a real pubsub :)
|
||||
for response in future::join_all(publications).await {
|
||||
if let Err(e) = response {
|
||||
info!("failed to broadcast to subscriber: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes an OpenTelemetry tracing subscriber with a Jaeger backend.
|
||||
fn init_tracing(service_name: &str) -> anyhow::Result<()> {
|
||||
env::set_var("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "12");
|
||||
let tracer = opentelemetry_jaeger::new_pipeline()
|
||||
.with_service_name(service_name)
|
||||
.with_max_packet_size(2usize.pow(13))
|
||||
.install_batch(opentelemetry::runtime::Tokio)?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::filter::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
init_tracing("Pub/Sub")?;
|
||||
|
||||
let addrs = Publisher {
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
subscriptions: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
.start()
|
||||
.await?;
|
||||
|
||||
let _subscriber0 = Subscriber::connect(
|
||||
addrs.subscriptions,
|
||||
vec!["calculus".into(), "cool shorts".into()],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let _subscriber1 = Subscriber::connect(
|
||||
addrs.subscriptions,
|
||||
vec!["cool shorts".into(), "history".into()],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let publisher = publisher::PublisherClient::new(
|
||||
client::Config::default(),
|
||||
tcp::connect(addrs.publisher, Json::default).await?,
|
||||
)
|
||||
.spawn();
|
||||
|
||||
publisher
|
||||
.publish(context::current(), "calculus".into(), "sqrt(2)".into())
|
||||
.await?;
|
||||
|
||||
publisher
|
||||
.publish(
|
||||
context::current(),
|
||||
"cool shorts".into(),
|
||||
"hello to all".into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
publisher
|
||||
.publish(context::current(), "history".into(), "napoleon".to_string())
|
||||
.await?;
|
||||
|
||||
drop(_subscriber0);
|
||||
|
||||
publisher
|
||||
.publish(
|
||||
context::current(),
|
||||
"cool shorts".into(),
|
||||
"hello to who?".into(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
opentelemetry::global::shutdown_tracer_provider();
|
||||
info!("done.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
55
tarpc/examples/readme.rs
Normal file
55
tarpc/examples/readme.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use futures::future::{self, Ready};
|
||||
use tarpc::{
|
||||
client, context,
|
||||
server::{self, Channel},
|
||||
};
|
||||
|
||||
/// This is the service definition. It looks a lot like a trait definition.
|
||||
/// It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
#[tarpc::service]
|
||||
pub trait World {
|
||||
async fn hello(name: String) -> String;
|
||||
}
|
||||
|
||||
/// This is the type that implements the generated World trait. It is the business logic
|
||||
/// and is used to start the server.
|
||||
#[derive(Clone)]
|
||||
struct HelloServer;
|
||||
|
||||
impl World for HelloServer {
|
||||
// Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
// an associated type representing the future output by the fn.
|
||||
|
||||
type HelloFut = Ready<String>;
|
||||
|
||||
fn hello(self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
future::ready(format!("Hello, {name}!"))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
|
||||
|
||||
let server = server::BaseChannel::with_defaults(server_transport);
|
||||
tokio::spawn(server.execute(HelloServer.serve()));
|
||||
|
||||
// WorldClient is generated by the #[tarpc::service] attribute. It has a constructor `new`
|
||||
// that takes a config and any Transport as input.
|
||||
let client = WorldClient::new(client::Config::default(), client_transport).spawn();
|
||||
|
||||
// The client has an RPC method for each RPC defined in the annotated trait. It takes the same
|
||||
// args as defined, with the addition of a Context, which is always the first arg. The Context
|
||||
// specifies a deadline and trace information which can be helpful in debugging requests.
|
||||
let hello = client.hello(context::current(), "Stim".to_string()).await?;
|
||||
|
||||
println!("{hello}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
113
tarpc/examples/tracing.rs
Normal file
113
tarpc/examples/tracing.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use crate::{add::Add as AddService, double::Double as DoubleService};
|
||||
use futures::{future, prelude::*};
|
||||
use std::env;
|
||||
use tarpc::{
|
||||
client, context,
|
||||
server::{incoming::Incoming, BaseChannel},
|
||||
};
|
||||
use tokio_serde::formats::Json;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
pub mod add {
|
||||
#[tarpc::service]
|
||||
pub trait Add {
|
||||
/// Add two ints together.
|
||||
async fn add(x: i32, y: i32) -> i32;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod double {
|
||||
#[tarpc::service]
|
||||
pub trait Double {
|
||||
/// 2 * x
|
||||
async fn double(x: i32) -> Result<i32, String>;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AddServer;
|
||||
|
||||
#[tarpc::server]
|
||||
impl AddService for AddServer {
|
||||
async fn add(self, _: context::Context, x: i32, y: i32) -> i32 {
|
||||
x + y
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DoubleServer {
|
||||
add_client: add::AddClient,
|
||||
}
|
||||
|
||||
#[tarpc::server]
|
||||
impl DoubleService for DoubleServer {
|
||||
async fn double(self, _: context::Context, x: i32) -> Result<i32, String> {
|
||||
self.add_client
|
||||
.add(context::current(), x, x)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracing(service_name: &str) -> anyhow::Result<()> {
|
||||
env::set_var("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", "12");
|
||||
let tracer = opentelemetry_jaeger::new_pipeline()
|
||||
.with_service_name(service_name)
|
||||
.with_max_packet_size(2usize.pow(13))
|
||||
.install_batch(opentelemetry::runtime::Tokio)?;
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.with(tracing_opentelemetry::layer().with_tracer(tracer))
|
||||
.try_init()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
init_tracing("tarpc_tracing_example")?;
|
||||
|
||||
let add_listener = tarpc::serde_transport::tcp::listen("localhost:0", Json::default)
|
||||
.await?
|
||||
.filter_map(|r| future::ready(r.ok()));
|
||||
let addr = add_listener.get_ref().local_addr();
|
||||
let add_server = add_listener
|
||||
.map(BaseChannel::with_defaults)
|
||||
.take(1)
|
||||
.execute(AddServer.serve());
|
||||
tokio::spawn(add_server);
|
||||
|
||||
let to_add_server = tarpc::serde_transport::tcp::connect(addr, Json::default).await?;
|
||||
let add_client = add::AddClient::new(client::Config::default(), to_add_server).spawn();
|
||||
|
||||
let double_listener = tarpc::serde_transport::tcp::listen("localhost:0", Json::default)
|
||||
.await?
|
||||
.filter_map(|r| future::ready(r.ok()));
|
||||
let addr = double_listener.get_ref().local_addr();
|
||||
let double_server = double_listener
|
||||
.map(BaseChannel::with_defaults)
|
||||
.take(1)
|
||||
.execute(DoubleServer { add_client }.serve());
|
||||
tokio::spawn(double_server);
|
||||
|
||||
let to_double_server = tarpc::serde_transport::tcp::connect(addr, Json::default).await?;
|
||||
let double_client =
|
||||
double::DoubleClient::new(client::Config::default(), to_double_server).spawn();
|
||||
|
||||
let ctx = context::current();
|
||||
for _ in 1..=5 {
|
||||
tracing::info!("{:?}", double_client.double(ctx, 1).await?);
|
||||
}
|
||||
|
||||
opentelemetry::global::shutdown_tracer_provider();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
ideal_width = 100
|
||||
reorder_imports = true
|
||||
edition = "2018"
|
||||
|
||||
916
tarpc/src/client.rs
Normal file
916
tarpc/src/client.rs
Normal file
@@ -0,0 +1,916 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! Provides a client that connects to a server and sends multiplexed requests.
|
||||
|
||||
mod in_flight_requests;
|
||||
|
||||
use crate::{context, trace, ClientMessage, Request, Response, ServerError, Transport};
|
||||
use futures::{prelude::*, ready, stream::Fuse, task::*};
|
||||
use in_flight_requests::{DeadlineExceededError, InFlightRequests};
|
||||
use pin_project::pin_project;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
error::Error,
|
||||
fmt, mem,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::Span;
|
||||
|
||||
/// Settings that control the behavior of the client.
|
||||
#[derive(Clone, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub struct Config {
|
||||
/// The number of requests that can be in flight at once.
|
||||
/// `max_in_flight_requests` controls the size of the map used by the client
|
||||
/// for storing pending requests.
|
||||
pub max_in_flight_requests: usize,
|
||||
/// The number of requests that can be buffered client-side before being sent.
|
||||
/// `pending_requests_buffer` controls the size of the channel clients use
|
||||
/// to communicate with the request dispatch task.
|
||||
pub pending_request_buffer: usize,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
max_in_flight_requests: 1_000,
|
||||
pending_request_buffer: 100,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A channel and dispatch pair. The dispatch drives the sending and receiving of requests
|
||||
/// and must be polled continuously or spawned.
|
||||
pub struct NewClient<C, D> {
|
||||
/// The new client.
|
||||
pub client: C,
|
||||
/// The client's dispatch.
|
||||
pub dispatch: D,
|
||||
}
|
||||
|
||||
impl<C, D, E> NewClient<C, D>
|
||||
where
|
||||
D: Future<Output = Result<(), E>> + Send + 'static,
|
||||
E: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
/// Helper method to spawn the dispatch on the default executor.
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub fn spawn(self) -> C {
|
||||
let dispatch = self.dispatch.unwrap_or_else(move |e| {
|
||||
let e = anyhow::Error::new(e);
|
||||
tracing::warn!("Connection broken: {:?}", e);
|
||||
});
|
||||
tokio::spawn(dispatch);
|
||||
self.client
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, D> fmt::Debug for NewClient<C, D> {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(fmt, "NewClient")
|
||||
}
|
||||
}
|
||||
|
||||
const _CHECK_USIZE: () = assert!(
|
||||
std::mem::size_of::<usize>() <= std::mem::size_of::<u64>(),
|
||||
"usize is too big to fit in u64"
|
||||
);
|
||||
|
||||
/// Handles communication from the client to request dispatch.
|
||||
#[derive(Debug)]
|
||||
pub struct Channel<Req, Resp> {
|
||||
to_dispatch: mpsc::Sender<DispatchRequest<Req, Resp>>,
|
||||
/// Channel to send a cancel message to the dispatcher.
|
||||
cancellation: RequestCancellation,
|
||||
/// The ID to use for the next request to stage.
|
||||
next_request_id: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
impl<Req, Resp> Clone for Channel<Req, Resp> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
to_dispatch: self.to_dispatch.clone(),
|
||||
cancellation: self.cancellation.clone(),
|
||||
next_request_id: self.next_request_id.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Resp> Channel<Req, Resp> {
|
||||
/// Sends a request to the dispatch task to forward to the server, returning a [`Future`] that
|
||||
/// resolves to the response.
|
||||
#[tracing::instrument(
|
||||
name = "RPC",
|
||||
skip(self, ctx, request_name, request),
|
||||
fields(
|
||||
rpc.trace_id = tracing::field::Empty,
|
||||
otel.kind = "client",
|
||||
otel.name = request_name)
|
||||
)]
|
||||
pub async fn call(
|
||||
&self,
|
||||
mut ctx: context::Context,
|
||||
request_name: &str,
|
||||
request: Req,
|
||||
) -> Result<Resp, RpcError> {
|
||||
let span = Span::current();
|
||||
ctx.trace_context = trace::Context::try_from(&span).unwrap_or_else(|_| {
|
||||
tracing::warn!(
|
||||
"OpenTelemetry subscriber not installed; making unsampled child context."
|
||||
);
|
||||
ctx.trace_context.new_child()
|
||||
});
|
||||
span.record("rpc.trace_id", &tracing::field::display(ctx.trace_id()));
|
||||
let (response_completion, mut response) = oneshot::channel();
|
||||
let request_id =
|
||||
u64::try_from(self.next_request_id.fetch_add(1, Ordering::Relaxed)).unwrap();
|
||||
|
||||
// ResponseGuard impls Drop to cancel in-flight requests. It should be created before
|
||||
// sending out the request; otherwise, the response future could be dropped after the
|
||||
// request is sent out but before ResponseGuard is created, rendering the cancellation
|
||||
// logic inactive.
|
||||
let response_guard = ResponseGuard {
|
||||
response: &mut response,
|
||||
request_id,
|
||||
cancellation: &self.cancellation,
|
||||
};
|
||||
self.to_dispatch
|
||||
.send(DispatchRequest {
|
||||
ctx,
|
||||
span,
|
||||
request_id,
|
||||
request,
|
||||
response_completion,
|
||||
})
|
||||
.await
|
||||
.map_err(|mpsc::error::SendError(_)| RpcError::Disconnected)?;
|
||||
response_guard.response().await
|
||||
}
|
||||
}
|
||||
|
||||
/// A server response that is completed by request dispatch when the corresponding response
|
||||
/// arrives off the wire.
|
||||
struct ResponseGuard<'a, Resp> {
|
||||
response: &'a mut oneshot::Receiver<Result<Response<Resp>, DeadlineExceededError>>,
|
||||
cancellation: &'a RequestCancellation,
|
||||
request_id: u64,
|
||||
}
|
||||
|
||||
/// An error that can occur in the processing of an RPC. This is not request-specific errors but
|
||||
/// rather cross-cutting errors that can always occur.
|
||||
#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RpcError {
|
||||
/// The client disconnected from the server.
|
||||
#[error("the client disconnected from the server")]
|
||||
Disconnected,
|
||||
/// The request exceeded its deadline.
|
||||
#[error("the request exceeded its deadline")]
|
||||
DeadlineExceeded,
|
||||
/// The server aborted request processing.
|
||||
#[error("the server aborted request processing")]
|
||||
Server(#[from] ServerError),
|
||||
}
|
||||
|
||||
impl From<DeadlineExceededError> for RpcError {
|
||||
fn from(_: DeadlineExceededError) -> Self {
|
||||
RpcError::DeadlineExceeded
|
||||
}
|
||||
}
|
||||
|
||||
impl<Resp> ResponseGuard<'_, Resp> {
|
||||
async fn response(mut self) -> Result<Resp, RpcError> {
|
||||
let response = (&mut self.response).await;
|
||||
// Cancel drop logic once a response has been received.
|
||||
mem::forget(self);
|
||||
match response {
|
||||
Ok(resp) => Ok(resp?.message?),
|
||||
Err(oneshot::error::RecvError { .. }) => {
|
||||
// The oneshot is Canceled when the dispatch task ends. In that case,
|
||||
// there's nothing listening on the other side, so there's no point in
|
||||
// propagating cancellation.
|
||||
Err(RpcError::Disconnected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancels the request when dropped, if not already complete.
|
||||
impl<Resp> Drop for ResponseGuard<'_, Resp> {
|
||||
fn drop(&mut self) {
|
||||
// The receiver needs to be closed to handle the edge case that the request has not
|
||||
// yet been received by the dispatch task. It is possible for the cancel message to
|
||||
// arrive before the request itself, in which case the request could get stuck in the
|
||||
// dispatch map forever if the server never responds (e.g. if the server dies while
|
||||
// responding). Even if the server does respond, it will have unnecessarily done work
|
||||
// for a client no longer waiting for a response. To avoid this, the dispatch task
|
||||
// checks if the receiver is closed before inserting the request in the map. By
|
||||
// closing the receiver before sending the cancel message, it is guaranteed that if the
|
||||
// dispatch task misses an early-arriving cancellation message, then it will see the
|
||||
// receiver as closed.
|
||||
self.response.close();
|
||||
self.cancellation.cancel(self.request_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a channel and dispatcher that manages the lifecycle of requests initiated by the
|
||||
/// channel.
|
||||
pub fn new<Req, Resp, C>(
|
||||
config: Config,
|
||||
transport: C,
|
||||
) -> NewClient<Channel<Req, Resp>, RequestDispatch<Req, Resp, C>>
|
||||
where
|
||||
C: Transport<ClientMessage<Req>, Response<Resp>>,
|
||||
{
|
||||
let (to_dispatch, pending_requests) = mpsc::channel(config.pending_request_buffer);
|
||||
let (cancellation, canceled_requests) = cancellations();
|
||||
let canceled_requests = canceled_requests;
|
||||
|
||||
NewClient {
|
||||
client: Channel {
|
||||
to_dispatch,
|
||||
cancellation,
|
||||
next_request_id: Arc::new(AtomicUsize::new(0)),
|
||||
},
|
||||
dispatch: RequestDispatch {
|
||||
config,
|
||||
canceled_requests,
|
||||
transport: transport.fuse(),
|
||||
in_flight_requests: InFlightRequests::default(),
|
||||
pending_requests,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the lifecycle of requests, writing requests to the wire, managing cancellations,
|
||||
/// and dispatching responses to the appropriate channel.
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct RequestDispatch<Req, Resp, C> {
|
||||
/// Writes requests to the wire and reads responses off the wire.
|
||||
#[pin]
|
||||
transport: Fuse<C>,
|
||||
/// Requests waiting to be written to the wire.
|
||||
pending_requests: mpsc::Receiver<DispatchRequest<Req, Resp>>,
|
||||
/// Requests that were dropped.
|
||||
canceled_requests: CanceledRequests,
|
||||
/// Requests already written to the wire that haven't yet received responses.
|
||||
in_flight_requests: InFlightRequests<Resp>,
|
||||
/// Configures limits to prevent unlimited resource usage.
|
||||
config: Config,
|
||||
}
|
||||
|
||||
/// Critical errors that result in a Channel disconnecting.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ChannelError<E>
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
/// Could not read from the transport.
|
||||
#[error("could not read from the transport")]
|
||||
Read(#[source] E),
|
||||
/// Could not ready the transport for writes.
|
||||
#[error("could not ready the transport for writes")]
|
||||
Ready(#[source] E),
|
||||
/// Could not write to the transport.
|
||||
#[error("could not write to the transport")]
|
||||
Write(#[source] E),
|
||||
/// Could not flush the transport.
|
||||
#[error("could not flush the transport")]
|
||||
Flush(#[source] E),
|
||||
/// Could not close the write end of the transport.
|
||||
#[error("could not close the write end of the transport")]
|
||||
Close(#[source] E),
|
||||
/// Could not poll expired requests.
|
||||
#[error("could not poll expired requests")]
|
||||
Timer(#[source] tokio::time::error::Error),
|
||||
}
|
||||
|
||||
impl<Req, Resp, C> RequestDispatch<Req, Resp, C>
|
||||
where
|
||||
C: Transport<ClientMessage<Req>, Response<Resp>>,
|
||||
{
|
||||
fn in_flight_requests<'a>(self: &'a mut Pin<&mut Self>) -> &'a mut InFlightRequests<Resp> {
|
||||
self.as_mut().project().in_flight_requests
|
||||
}
|
||||
|
||||
fn transport_pin_mut<'a>(self: &'a mut Pin<&mut Self>) -> Pin<&'a mut Fuse<C>> {
|
||||
self.as_mut().project().transport
|
||||
}
|
||||
|
||||
fn poll_ready<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), ChannelError<C::Error>>> {
|
||||
self.transport_pin_mut()
|
||||
.poll_ready(cx)
|
||||
.map_err(ChannelError::Ready)
|
||||
}
|
||||
|
||||
fn start_send(
|
||||
self: &mut Pin<&mut Self>,
|
||||
message: ClientMessage<Req>,
|
||||
) -> Result<(), ChannelError<C::Error>> {
|
||||
self.transport_pin_mut()
|
||||
.start_send(message)
|
||||
.map_err(ChannelError::Write)
|
||||
}
|
||||
|
||||
fn poll_flush<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), ChannelError<C::Error>>> {
|
||||
self.transport_pin_mut()
|
||||
.poll_flush(cx)
|
||||
.map_err(ChannelError::Flush)
|
||||
}
|
||||
|
||||
fn poll_close<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), ChannelError<C::Error>>> {
|
||||
self.transport_pin_mut()
|
||||
.poll_close(cx)
|
||||
.map_err(ChannelError::Close)
|
||||
}
|
||||
|
||||
fn canceled_requests_mut<'a>(self: &'a mut Pin<&mut Self>) -> &'a mut CanceledRequests {
|
||||
self.as_mut().project().canceled_requests
|
||||
}
|
||||
|
||||
fn pending_requests_mut<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
) -> &'a mut mpsc::Receiver<DispatchRequest<Req, Resp>> {
|
||||
self.as_mut().project().pending_requests
|
||||
}
|
||||
|
||||
fn pump_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(), ChannelError<C::Error>>>> {
|
||||
self.transport_pin_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(ChannelError::Read)
|
||||
.map_ok(|response| {
|
||||
self.complete(response);
|
||||
})
|
||||
}
|
||||
|
||||
fn pump_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(), ChannelError<C::Error>>>> {
|
||||
enum ReceiverStatus {
|
||||
Pending,
|
||||
Closed,
|
||||
}
|
||||
|
||||
let pending_requests_status = match self.as_mut().poll_write_request(cx)? {
|
||||
Poll::Ready(Some(())) => return Poll::Ready(Some(Ok(()))),
|
||||
Poll::Ready(None) => ReceiverStatus::Closed,
|
||||
Poll::Pending => ReceiverStatus::Pending,
|
||||
};
|
||||
|
||||
let canceled_requests_status = match self.as_mut().poll_write_cancel(cx)? {
|
||||
Poll::Ready(Some(())) => return Poll::Ready(Some(Ok(()))),
|
||||
Poll::Ready(None) => ReceiverStatus::Closed,
|
||||
Poll::Pending => ReceiverStatus::Pending,
|
||||
};
|
||||
|
||||
// Receiving Poll::Ready(None) when polling expired requests never indicates "Closed",
|
||||
// because there can temporarily be zero in-flight rquests. Therefore, there is no need to
|
||||
// track the status like is done with pending and cancelled requests.
|
||||
if let Poll::Ready(Some(_)) = self
|
||||
.in_flight_requests()
|
||||
.poll_expired(cx)
|
||||
.map_err(ChannelError::Timer)?
|
||||
{
|
||||
// Expired requests are considered complete; there is no compelling reason to send a
|
||||
// cancellation message to the server, since it will have already exhausted its
|
||||
// allotted processing time.
|
||||
return Poll::Ready(Some(Ok(())));
|
||||
}
|
||||
|
||||
match (pending_requests_status, canceled_requests_status) {
|
||||
(ReceiverStatus::Closed, ReceiverStatus::Closed) => {
|
||||
ready!(self.poll_close(cx)?);
|
||||
Poll::Ready(None)
|
||||
}
|
||||
(ReceiverStatus::Pending, _) | (_, ReceiverStatus::Pending) => {
|
||||
// No more messages to process, so flush any messages buffered in the transport.
|
||||
ready!(self.poll_flush(cx)?);
|
||||
|
||||
// Even if we fully-flush, we return Pending, because we have no more requests
|
||||
// or cancellations right now.
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields the next pending request, if one is ready to be sent.
|
||||
///
|
||||
/// Note that a request will only be yielded if the transport is *ready* to be written to (i.e.
|
||||
/// start_send would succeed).
|
||||
fn poll_next_request(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<DispatchRequest<Req, Resp>, ChannelError<C::Error>>>> {
|
||||
if self.in_flight_requests().len() >= self.config.max_in_flight_requests {
|
||||
tracing::info!(
|
||||
"At in-flight request capacity ({}/{}).",
|
||||
self.in_flight_requests().len(),
|
||||
self.config.max_in_flight_requests
|
||||
);
|
||||
|
||||
// No need to schedule a wakeup, because timers and responses are responsible
|
||||
// for clearing out in-flight requests.
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
ready!(self.ensure_writeable(cx)?);
|
||||
|
||||
loop {
|
||||
match ready!(self.pending_requests_mut().poll_recv(cx)) {
|
||||
Some(request) => {
|
||||
if request.response_completion.is_closed() {
|
||||
let _entered = request.span.enter();
|
||||
tracing::info!("AbortRequest");
|
||||
continue;
|
||||
}
|
||||
|
||||
return Poll::Ready(Some(Ok(request)));
|
||||
}
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields the next pending cancellation, and, if one is ready, cancels the associated request.
|
||||
///
|
||||
/// Note that a request to cancel will only be yielded if the transport is *ready* to be
|
||||
/// written to (i.e. start_send would succeed).
|
||||
fn poll_next_cancellation(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(context::Context, Span, u64), ChannelError<C::Error>>>> {
|
||||
ready!(self.ensure_writeable(cx)?);
|
||||
|
||||
loop {
|
||||
match ready!(self.canceled_requests_mut().poll_next_unpin(cx)) {
|
||||
Some(request_id) => {
|
||||
if let Some((ctx, span)) = self.in_flight_requests().cancel_request(request_id)
|
||||
{
|
||||
return Poll::Ready(Some(Ok((ctx, span, request_id))));
|
||||
}
|
||||
}
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ready if writing a message to the transport (i.e. via write_request or
|
||||
/// write_cancel) would not fail due to a full buffer. If the transport is not ready to be
|
||||
/// written to, flushes it until it is ready.
|
||||
fn ensure_writeable<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(), ChannelError<C::Error>>>> {
|
||||
while self.poll_ready(cx)?.is_pending() {
|
||||
ready!(self.poll_flush(cx)?);
|
||||
}
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
|
||||
fn poll_write_request<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(), ChannelError<C::Error>>>> {
|
||||
let DispatchRequest {
|
||||
ctx,
|
||||
span,
|
||||
request_id,
|
||||
request,
|
||||
response_completion,
|
||||
} = match ready!(self.as_mut().poll_next_request(cx)?) {
|
||||
Some(dispatch_request) => dispatch_request,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
let entered = span.enter();
|
||||
// poll_next_request only returns Ready if there is room to buffer another request.
|
||||
// Therefore, we can call write_request without fear of erroring due to a full
|
||||
// buffer.
|
||||
let request_id = request_id;
|
||||
let request = ClientMessage::Request(Request {
|
||||
id: request_id,
|
||||
message: request,
|
||||
context: context::Context {
|
||||
deadline: ctx.deadline,
|
||||
trace_context: ctx.trace_context,
|
||||
},
|
||||
});
|
||||
self.start_send(request)?;
|
||||
let deadline = ctx.deadline;
|
||||
tracing::info!(
|
||||
tarpc.deadline = %humantime::format_rfc3339(deadline),
|
||||
"SendRequest"
|
||||
);
|
||||
drop(entered);
|
||||
|
||||
self.in_flight_requests()
|
||||
.insert_request(request_id, ctx, span, response_completion)
|
||||
.expect("Request IDs should be unique");
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
|
||||
fn poll_write_cancel<'a>(
|
||||
self: &'a mut Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<(), ChannelError<C::Error>>>> {
|
||||
let (context, span, request_id) = match ready!(self.as_mut().poll_next_cancellation(cx)?) {
|
||||
Some(triple) => triple,
|
||||
None => return Poll::Ready(None),
|
||||
};
|
||||
let _entered = span.enter();
|
||||
|
||||
let cancel = ClientMessage::Cancel {
|
||||
trace_context: context.trace_context,
|
||||
request_id,
|
||||
};
|
||||
self.start_send(cancel)?;
|
||||
tracing::info!("CancelRequest");
|
||||
Poll::Ready(Some(Ok(())))
|
||||
}
|
||||
|
||||
/// Sends a server response to the client task that initiated the associated request.
|
||||
fn complete(mut self: Pin<&mut Self>, response: Response<Resp>) -> bool {
|
||||
self.in_flight_requests().complete_request(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Resp, C> Future for RequestDispatch<Req, Resp, C>
|
||||
where
|
||||
C: Transport<ClientMessage<Req>, Response<Resp>>,
|
||||
{
|
||||
type Output = Result<(), ChannelError<C::Error>>;
|
||||
|
||||
fn poll(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), ChannelError<C::Error>>> {
|
||||
loop {
|
||||
match (self.as_mut().pump_read(cx)?, self.as_mut().pump_write(cx)?) {
|
||||
(Poll::Ready(None), _) => {
|
||||
tracing::info!("Shutdown: read half closed, so shutting down.");
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
(read, Poll::Ready(None)) => {
|
||||
if self.in_flight_requests.is_empty() {
|
||||
tracing::info!("Shutdown: write half closed, and no requests in flight.");
|
||||
return Poll::Ready(Ok(()));
|
||||
}
|
||||
tracing::info!(
|
||||
"Shutdown: write half closed, and {} requests in flight.",
|
||||
self.in_flight_requests().len()
|
||||
);
|
||||
match read {
|
||||
Poll::Ready(Some(())) => continue,
|
||||
_ => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
(Poll::Ready(Some(())), _) | (_, Poll::Ready(Some(()))) => {}
|
||||
_ => return Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A server-bound request sent from a [`Channel`] to request dispatch, which will then manage
|
||||
/// the lifecycle of the request.
|
||||
#[derive(Debug)]
|
||||
struct DispatchRequest<Req, Resp> {
|
||||
pub ctx: context::Context,
|
||||
pub span: Span,
|
||||
pub request_id: u64,
|
||||
pub request: Req,
|
||||
pub response_completion: oneshot::Sender<Result<Response<Resp>, DeadlineExceededError>>,
|
||||
}
|
||||
|
||||
/// Sends request cancellation signals.
|
||||
#[derive(Debug, Clone)]
|
||||
struct RequestCancellation(mpsc::UnboundedSender<u64>);
|
||||
|
||||
/// A stream of IDs of requests that have been canceled.
|
||||
#[derive(Debug)]
|
||||
struct CanceledRequests(mpsc::UnboundedReceiver<u64>);
|
||||
|
||||
/// Returns a channel to send request cancellation messages.
|
||||
fn cancellations() -> (RequestCancellation, CanceledRequests) {
|
||||
// Unbounded because messages are sent in the drop fn. This is fine, because it's still
|
||||
// bounded by the number of in-flight requests.
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
(RequestCancellation(tx), CanceledRequests(rx))
|
||||
}
|
||||
|
||||
impl RequestCancellation {
|
||||
/// Cancels the request with ID `request_id`.
|
||||
fn cancel(&self, request_id: u64) {
|
||||
let _ = self.0.send(request_id);
|
||||
}
|
||||
}
|
||||
|
||||
impl CanceledRequests {
|
||||
fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Option<u64>> {
|
||||
self.0.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for CanceledRequests {
|
||||
type Item = u64;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<u64>> {
|
||||
self.poll_recv(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
cancellations, CanceledRequests, Channel, DispatchRequest, RequestCancellation,
|
||||
RequestDispatch, ResponseGuard,
|
||||
};
|
||||
use crate::{
|
||||
client::{
|
||||
in_flight_requests::{DeadlineExceededError, InFlightRequests},
|
||||
Config,
|
||||
},
|
||||
context,
|
||||
transport::{self, channel::UnboundedChannel},
|
||||
ClientMessage, Response,
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{prelude::*, task::*};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
pin::Pin,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::Span;
|
||||
|
||||
#[tokio::test]
|
||||
async fn response_completes_request_future() {
|
||||
let (mut dispatch, mut _channel, mut server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
dispatch
|
||||
.in_flight_requests
|
||||
.insert_request(0, context::current(), Span::current(), tx)
|
||||
.unwrap();
|
||||
server_channel
|
||||
.send(Response {
|
||||
request_id: 0,
|
||||
message: Ok("Resp".into()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_matches!(dispatch.as_mut().poll(cx), Poll::Pending);
|
||||
assert_matches!(rx.try_recv(), Ok(Ok(Response { request_id: 0, message: Ok(resp) })) if resp == "Resp");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_response_cancels_on_drop() {
|
||||
let (cancellation, mut canceled_requests) = cancellations();
|
||||
let (_, mut response) = oneshot::channel();
|
||||
drop(ResponseGuard::<u32> {
|
||||
response: &mut response,
|
||||
cancellation: &cancellation,
|
||||
request_id: 3,
|
||||
});
|
||||
// resp's drop() is run, which should send a cancel message.
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
assert_eq!(canceled_requests.0.poll_recv(cx), Poll::Ready(Some(3)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dispatch_response_doesnt_cancel_after_complete() {
|
||||
let (cancellation, mut canceled_requests) = cancellations();
|
||||
let (tx, mut response) = oneshot::channel();
|
||||
tx.send(Ok(Response {
|
||||
request_id: 0,
|
||||
message: Ok("well done"),
|
||||
}))
|
||||
.unwrap();
|
||||
// resp's drop() is run, but should not send a cancel message.
|
||||
ResponseGuard {
|
||||
response: &mut response,
|
||||
cancellation: &cancellation,
|
||||
request_id: 3,
|
||||
}
|
||||
.response()
|
||||
.await
|
||||
.unwrap();
|
||||
drop(cancellation);
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
assert_eq!(canceled_requests.0.poll_recv(cx), Poll::Ready(None));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stage_request() {
|
||||
let (mut dispatch, mut channel, _server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
let _resp = send_request(&mut channel, "hi", tx, &mut rx).await;
|
||||
|
||||
let req = dispatch.as_mut().poll_next_request(cx).ready();
|
||||
assert!(req.is_some());
|
||||
|
||||
let req = req.unwrap();
|
||||
assert_eq!(req.request_id, 0);
|
||||
assert_eq!(req.request, "hi".to_string());
|
||||
}
|
||||
|
||||
// Regression test for https://github.com/google/tarpc/issues/220
|
||||
#[tokio::test]
|
||||
async fn stage_request_channel_dropped_doesnt_panic() {
|
||||
let (mut dispatch, mut channel, mut server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
let _ = send_request(&mut channel, "hi", tx, &mut rx).await;
|
||||
drop(channel);
|
||||
|
||||
assert!(dispatch.as_mut().poll(cx).is_ready());
|
||||
send_response(
|
||||
&mut server_channel,
|
||||
Response {
|
||||
request_id: 0,
|
||||
message: Ok("hello".into()),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
dispatch.await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stage_request_response_future_dropped_is_canceled_before_sending() {
|
||||
let (mut dispatch, mut channel, _server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
let _ = send_request(&mut channel, "hi", tx, &mut rx).await;
|
||||
|
||||
// Drop the channel so polling returns none if no requests are currently ready.
|
||||
drop(channel);
|
||||
// Test that a request future dropped before it's processed by dispatch will cause the request
|
||||
// to not be added to the in-flight request map.
|
||||
assert!(dispatch.as_mut().poll_next_request(cx).ready().is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stage_request_response_future_dropped_is_canceled_after_sending() {
|
||||
let (mut dispatch, mut channel, _server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
let req = send_request(&mut channel, "hi", tx, &mut rx).await;
|
||||
|
||||
assert!(dispatch.as_mut().pump_write(cx).ready().is_some());
|
||||
assert!(!dispatch.in_flight_requests.is_empty());
|
||||
|
||||
// Test that a request future dropped after it's processed by dispatch will cause the request
|
||||
// to be removed from the in-flight request map.
|
||||
drop(req);
|
||||
assert_matches!(
|
||||
dispatch.as_mut().poll_next_cancellation(cx),
|
||||
Poll::Ready(Some(Ok(_)))
|
||||
);
|
||||
assert!(dispatch.in_flight_requests.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stage_request_response_closed_skipped() {
|
||||
let (mut dispatch, mut channel, _server_channel) = set_up();
|
||||
let cx = &mut Context::from_waker(&noop_waker_ref());
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
|
||||
// Test that a request future that's closed its receiver but not yet canceled its request --
|
||||
// i.e. still in `drop fn` -- will cause the request to not be added to the in-flight request
|
||||
// map.
|
||||
let resp = send_request(&mut channel, "hi", tx, &mut rx).await;
|
||||
resp.response.close();
|
||||
|
||||
assert!(dispatch.as_mut().poll_next_request(cx).is_pending());
|
||||
}
|
||||
|
||||
fn set_up() -> (
|
||||
Pin<
|
||||
Box<
|
||||
RequestDispatch<
|
||||
String,
|
||||
String,
|
||||
UnboundedChannel<Response<String>, ClientMessage<String>>,
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
Channel<String, String>,
|
||||
UnboundedChannel<ClientMessage<String>, Response<String>>,
|
||||
) {
|
||||
let _ = tracing_subscriber::fmt().with_test_writer().try_init();
|
||||
|
||||
let (to_dispatch, pending_requests) = mpsc::channel(1);
|
||||
let (cancel_tx, canceled_requests) = mpsc::unbounded_channel();
|
||||
let (client_channel, server_channel) = transport::channel::unbounded();
|
||||
|
||||
let dispatch = RequestDispatch::<String, String, _> {
|
||||
transport: client_channel.fuse(),
|
||||
pending_requests: pending_requests,
|
||||
canceled_requests: CanceledRequests(canceled_requests),
|
||||
in_flight_requests: InFlightRequests::default(),
|
||||
config: Config::default(),
|
||||
};
|
||||
|
||||
let cancellation = RequestCancellation(cancel_tx);
|
||||
let channel = Channel {
|
||||
to_dispatch,
|
||||
cancellation,
|
||||
next_request_id: Arc::new(AtomicUsize::new(0)),
|
||||
};
|
||||
|
||||
(Box::pin(dispatch), channel, server_channel)
|
||||
}
|
||||
|
||||
async fn send_request<'a>(
|
||||
channel: &'a mut Channel<String, String>,
|
||||
request: &str,
|
||||
response_completion: oneshot::Sender<Result<Response<String>, DeadlineExceededError>>,
|
||||
response: &'a mut oneshot::Receiver<Result<Response<String>, DeadlineExceededError>>,
|
||||
) -> ResponseGuard<'a, String> {
|
||||
let request_id =
|
||||
u64::try_from(channel.next_request_id.fetch_add(1, Ordering::Relaxed)).unwrap();
|
||||
let request = DispatchRequest {
|
||||
ctx: context::current(),
|
||||
span: Span::current(),
|
||||
request_id,
|
||||
request: request.to_string(),
|
||||
response_completion,
|
||||
};
|
||||
channel.to_dispatch.send(request).await.unwrap();
|
||||
|
||||
ResponseGuard {
|
||||
response,
|
||||
cancellation: &channel.cancellation,
|
||||
request_id,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_response(
|
||||
channel: &mut UnboundedChannel<ClientMessage<String>, Response<String>>,
|
||||
response: Response<String>,
|
||||
) {
|
||||
channel.send(response).await.unwrap();
|
||||
}
|
||||
|
||||
trait PollTest {
|
||||
type T;
|
||||
fn unwrap(self) -> Poll<Self::T>;
|
||||
fn ready(self) -> Self::T;
|
||||
}
|
||||
|
||||
impl<T, E> PollTest for Poll<Option<Result<T, E>>>
|
||||
where
|
||||
E: ::std::fmt::Display,
|
||||
{
|
||||
type T = Option<T>;
|
||||
|
||||
fn unwrap(self) -> Poll<Option<T>> {
|
||||
match self {
|
||||
Poll::Ready(Some(Ok(t))) => Poll::Ready(Some(t)),
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Ready(Some(Err(e))) => panic!("{}", e.to_string()),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn ready(self) -> Option<T> {
|
||||
match self {
|
||||
Poll::Ready(Some(Ok(t))) => Some(t),
|
||||
Poll::Ready(None) => None,
|
||||
Poll::Ready(Some(Err(e))) => panic!("{}", e.to_string()),
|
||||
Poll::Pending => panic!("Pending"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
137
tarpc/src/client/in_flight_requests.rs
Normal file
137
tarpc/src/client/in_flight_requests.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::{
|
||||
context,
|
||||
util::{Compact, TimeUntil},
|
||||
Response,
|
||||
};
|
||||
use fnv::FnvHashMap;
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio_util::time::delay_queue::{self, DelayQueue};
|
||||
use tracing::Span;
|
||||
|
||||
/// Requests already written to the wire that haven't yet received responses.
|
||||
#[derive(Debug)]
|
||||
pub struct InFlightRequests<Resp> {
|
||||
request_data: FnvHashMap<u64, RequestData<Resp>>,
|
||||
deadlines: DelayQueue<u64>,
|
||||
}
|
||||
|
||||
impl<Resp> Default for InFlightRequests<Resp> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
request_data: Default::default(),
|
||||
deadlines: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The request exceeded its deadline.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[error("the request exceeded its deadline")]
|
||||
pub struct DeadlineExceededError;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RequestData<Resp> {
|
||||
ctx: context::Context,
|
||||
span: Span,
|
||||
response_completion: oneshot::Sender<Result<Response<Resp>, DeadlineExceededError>>,
|
||||
/// The key to remove the timer for the request's deadline.
|
||||
deadline_key: delay_queue::Key,
|
||||
}
|
||||
|
||||
/// An error returned when an attempt is made to insert a request with an ID that is already in
|
||||
/// use.
|
||||
#[derive(Debug)]
|
||||
pub struct AlreadyExistsError;
|
||||
|
||||
impl<Resp> InFlightRequests<Resp> {
|
||||
/// Returns the number of in-flight requests.
|
||||
pub fn len(&self) -> usize {
|
||||
self.request_data.len()
|
||||
}
|
||||
|
||||
/// Returns true iff there are no requests in flight.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.request_data.is_empty()
|
||||
}
|
||||
|
||||
/// Starts a request, unless a request with the same ID is already in flight.
|
||||
pub fn insert_request(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
ctx: context::Context,
|
||||
span: Span,
|
||||
response_completion: oneshot::Sender<Result<Response<Resp>, DeadlineExceededError>>,
|
||||
) -> Result<(), AlreadyExistsError> {
|
||||
match self.request_data.entry(request_id) {
|
||||
hash_map::Entry::Vacant(vacant) => {
|
||||
let timeout = ctx.deadline.time_until();
|
||||
let deadline_key = self.deadlines.insert(request_id, timeout);
|
||||
vacant.insert(RequestData {
|
||||
ctx,
|
||||
span,
|
||||
response_completion,
|
||||
deadline_key,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
hash_map::Entry::Occupied(_) => Err(AlreadyExistsError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a request without aborting. Returns true iff the request was found.
|
||||
pub fn complete_request(&mut self, response: Response<Resp>) -> bool {
|
||||
if let Some(request_data) = self.request_data.remove(&response.request_id) {
|
||||
let _entered = request_data.span.enter();
|
||||
tracing::info!("ReceiveResponse");
|
||||
self.request_data.compact(0.1);
|
||||
self.deadlines.remove(&request_data.deadline_key);
|
||||
let _ = request_data.response_completion.send(Ok(response));
|
||||
return true;
|
||||
}
|
||||
|
||||
tracing::debug!(
|
||||
"No in-flight request found for request_id = {}.",
|
||||
response.request_id
|
||||
);
|
||||
|
||||
// If the response completion was absent, then the request was already canceled.
|
||||
false
|
||||
}
|
||||
|
||||
/// Cancels a request without completing (typically used when a request handle was dropped
|
||||
/// before the request completed).
|
||||
pub fn cancel_request(&mut self, request_id: u64) -> Option<(context::Context, Span)> {
|
||||
if let Some(request_data) = self.request_data.remove(&request_id) {
|
||||
self.request_data.compact(0.1);
|
||||
self.deadlines.remove(&request_data.deadline_key);
|
||||
Some((request_data.ctx, request_data.span))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields a request that has expired, completing it with a TimedOut error.
|
||||
/// The caller should send cancellation messages for any yielded request ID.
|
||||
pub fn poll_expired(
|
||||
&mut self,
|
||||
cx: &mut Context,
|
||||
) -> Poll<Option<Result<u64, tokio::time::error::Error>>> {
|
||||
self.deadlines.poll_expired(cx).map_ok(|expired| {
|
||||
let request_id = expired.into_inner();
|
||||
if let Some(request_data) = self.request_data.remove(&request_id) {
|
||||
let _entered = request_data.span.enter();
|
||||
tracing::error!("DeadlineExceeded");
|
||||
self.request_data.compact(0.1);
|
||||
let _ = request_data
|
||||
.response_completion
|
||||
.send(Err(DeadlineExceededError));
|
||||
}
|
||||
request_id
|
||||
})
|
||||
}
|
||||
}
|
||||
102
tarpc/src/context.rs
Normal file
102
tarpc/src/context.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! Provides a request context that carries a deadline and trace context. This context is sent from
|
||||
//! client to server and is used by the server to enforce response deadlines.
|
||||
|
||||
use crate::trace::{self, TraceId};
|
||||
use opentelemetry::trace::TraceContextExt;
|
||||
use static_assertions::assert_impl_all;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
/// A request context that carries request-scoped information like deadlines and trace information.
|
||||
/// It is sent from client to server and is used by the server to enforce response deadlines.
|
||||
///
|
||||
/// The context should not be stored directly in a server implementation, because the context will
|
||||
/// be different for each request in scope.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Context {
|
||||
/// When the client expects the request to be complete by. The server should cancel the request
|
||||
/// if it is not complete by this time.
|
||||
#[cfg_attr(feature = "serde1", serde(default = "ten_seconds_from_now"))]
|
||||
pub deadline: SystemTime,
|
||||
/// Uniquely identifies requests originating from the same source.
|
||||
/// When a service handles a request by making requests itself, those requests should
|
||||
/// include the same `trace_id` as that included on the original request. This way,
|
||||
/// users can trace related actions across a distributed system.
|
||||
pub trace_context: trace::Context,
|
||||
}
|
||||
|
||||
assert_impl_all!(Context: Send, Sync);
|
||||
|
||||
fn ten_seconds_from_now() -> SystemTime {
|
||||
SystemTime::now() + Duration::from_secs(10)
|
||||
}
|
||||
|
||||
/// Returns the context for the current request, or a default Context if no request is active.
|
||||
pub fn current() -> Context {
|
||||
Context::current()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Deadline(SystemTime);
|
||||
|
||||
impl Default for Deadline {
|
||||
fn default() -> Self {
|
||||
Self(ten_seconds_from_now())
|
||||
}
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Returns the context for the current request, or a default Context if no request is active.
|
||||
pub fn current() -> Self {
|
||||
let span = tracing::Span::current();
|
||||
Self {
|
||||
trace_context: trace::Context::try_from(&span)
|
||||
.unwrap_or_else(|_| trace::Context::default()),
|
||||
deadline: span
|
||||
.context()
|
||||
.get::<Deadline>()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ID of the request-scoped trace.
|
||||
pub fn trace_id(&self) -> &TraceId {
|
||||
&self.trace_context.trace_id
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension trait for [`tracing::Span`] for propagating tarpc Contexts.
|
||||
pub(crate) trait SpanExt {
|
||||
/// Sets the given context on this span. Newly-created spans will be children of the given
|
||||
/// context's trace context.
|
||||
fn set_context(&self, context: &Context);
|
||||
}
|
||||
|
||||
impl SpanExt for tracing::Span {
|
||||
fn set_context(&self, context: &Context) {
|
||||
self.set_parent(
|
||||
opentelemetry::Context::new()
|
||||
.with_remote_span_context(opentelemetry::trace::SpanContext::new(
|
||||
opentelemetry::trace::TraceId::from(context.trace_context.trace_id),
|
||||
opentelemetry::trace::SpanId::from(context.trace_context.span_id),
|
||||
opentelemetry::trace::TraceFlags::from(context.trace_context.sampling_decision),
|
||||
true,
|
||||
opentelemetry::trace::TraceState::default(),
|
||||
))
|
||||
.with_value(Deadline(context.deadline)),
|
||||
);
|
||||
}
|
||||
}
|
||||
453
tarpc/src/lib.rs
453
tarpc/src/lib.rs
@@ -1,66 +1,421 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
//! An RPC library for Rust.
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
//! *Disclaimer*: This is not an official Google product.
|
||||
//!
|
||||
//! Example usage:
|
||||
//! tarpc is an RPC framework for rust with a focus on ease of use. Defining a
|
||||
//! service can be done in just a few lines of code, and most of the boilerplate of
|
||||
//! writing a server is taken care of for you.
|
||||
//!
|
||||
//! [Documentation](https://docs.rs/crate/tarpc/)
|
||||
//!
|
||||
//! ## What is an RPC framework?
|
||||
//! "RPC" stands for "Remote Procedure Call," a function call where the work of
|
||||
//! producing the return value is being done somewhere else. When an rpc function is
|
||||
//! invoked, behind the scenes the function contacts some other process somewhere
|
||||
//! and asks them to evaluate the function instead. The original function then
|
||||
//! returns the value produced by the other process.
|
||||
//!
|
||||
//! RPC frameworks are a fundamental building block of most microservices-oriented
|
||||
//! architectures. Two well-known ones are [gRPC](http://www.grpc.io) and
|
||||
//! [Cap'n Proto](https://capnproto.org/).
|
||||
//!
|
||||
//! tarpc differentiates itself from other RPC frameworks by defining the schema in code,
|
||||
//! rather than in a separate language such as .proto. This means there's no separate compilation
|
||||
//! process, and no context switching between different languages.
|
||||
//!
|
||||
//! Some other features of tarpc:
|
||||
//! - Pluggable transport: any type implementing `Stream<Item = Request> + Sink<Response>` can be
|
||||
//! used as a transport to connect the client and server.
|
||||
//! - `Send + 'static` optional: if the transport doesn't require it, neither does tarpc!
|
||||
//! - Cascading cancellation: dropping a request will send a cancellation message to the server.
|
||||
//! The server will cease any unfinished work on the request, subsequently cancelling any of its
|
||||
//! own requests, repeating for the entire chain of transitive dependencies.
|
||||
//! - Configurable deadlines and deadline propagation: request deadlines default to 10s if
|
||||
//! unspecified. The server will automatically cease work when the deadline has passed. Any
|
||||
//! requests sent by the server that use the request context will propagate the request deadline.
|
||||
//! For example, if a server is handling a request with a 10s deadline, does 2s of work, then
|
||||
//! sends a request to another server, that server will see an 8s deadline.
|
||||
//! - Distributed tracing: tarpc is instrumented with
|
||||
//! [tracing](https://github.com/tokio-rs/tracing) primitives extended with
|
||||
//! [OpenTelemetry](https://opentelemetry.io/) traces. Using a compatible tracing subscriber like
|
||||
//! [Jaeger](https://github.com/open-telemetry/opentelemetry-rust/tree/main/opentelemetry-jaeger),
|
||||
//! each RPC can be traced through the client, server, and other dependencies downstream of the
|
||||
//! server. Even for applications not connected to a distributed tracing collector, the
|
||||
//! instrumentation can also be ingested by regular loggers like
|
||||
//! [env_logger](https://github.com/env-logger-rs/env_logger/).
|
||||
//! - Serde serialization: enabling the `serde1` Cargo feature will make service requests and
|
||||
//! responses `Serialize + Deserialize`. It's entirely optional, though: in-memory transports can
|
||||
//! be used, as well, so the price of serialization doesn't have to be paid when it's not needed.
|
||||
//!
|
||||
//! ## Usage
|
||||
//! Add to your `Cargo.toml` dependencies:
|
||||
//!
|
||||
//! ```toml
|
||||
//! tarpc = "0.28"
|
||||
//! ```
|
||||
//! #[macro_use] extern crate tarpc;
|
||||
//! mod my_server {
|
||||
//! service! {
|
||||
//! rpc hello(name: String) -> String;
|
||||
//! rpc add(x: i32, y: i32) -> i32;
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! use self::my_server::*;
|
||||
//! use std::time::Duration;
|
||||
//! The `tarpc::service` attribute expands to a collection of items that form an rpc service.
|
||||
//! These generated types make it easy and ergonomic to write servers with less boilerplate.
|
||||
//! Simply implement the generated service trait, and you're off to the races!
|
||||
//!
|
||||
//! struct Server;
|
||||
//! impl my_server::Service for Server {
|
||||
//! fn hello(&self, s: String) -> String {
|
||||
//! format!("Hello, {}!", s)
|
||||
//! }
|
||||
//! fn add(&self, x: i32, y: i32) -> i32 {
|
||||
//! x + y
|
||||
//! }
|
||||
//! }
|
||||
//! ## Example
|
||||
//!
|
||||
//! fn main() {
|
||||
//! let serve_handle = Server.spawn("localhost:0").unwrap();
|
||||
//! let client = Client::new(serve_handle.dialer()).unwrap();
|
||||
//! assert_eq!(3, client.add(1, 2).unwrap());
|
||||
//! assert_eq!("Hello, Mom!".to_string(),
|
||||
//! client.hello("Mom".to_string()).unwrap());
|
||||
//! drop(client);
|
||||
//! serve_handle.shutdown();
|
||||
//! This example uses [tokio](https://tokio.rs), so add the following dependencies to
|
||||
//! your `Cargo.toml`:
|
||||
//!
|
||||
//! ```toml
|
||||
//! anyhow = "1.0"
|
||||
//! futures = "0.3"
|
||||
//! tarpc = { version = "0.28", features = ["tokio1"] }
|
||||
//! tokio = { version = "1.0", features = ["macros"] }
|
||||
//! ```
|
||||
//!
|
||||
//! In the following example, we use an in-process channel for communication between
|
||||
//! client and server. In real code, you will likely communicate over the network.
|
||||
//! For a more real-world example, see [example-service](example-service).
|
||||
//!
|
||||
//! First, let's set up the dependencies and service definition.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate futures;
|
||||
//!
|
||||
//! use futures::{
|
||||
//! future::{self, Ready},
|
||||
//! prelude::*,
|
||||
//! };
|
||||
//! use tarpc::{
|
||||
//! client, context,
|
||||
//! server::{self, incoming::Incoming, Channel},
|
||||
//! };
|
||||
//!
|
||||
//! // This is the service definition. It looks a lot like a trait definition.
|
||||
//! // It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
//! #[tarpc::service]
|
||||
//! trait World {
|
||||
//! /// Returns a greeting for name.
|
||||
//! async fn hello(name: String) -> String;
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
//!
|
||||
//! This service definition generates a trait called `World`. Next we need to
|
||||
//! implement it for our Server struct.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate futures;
|
||||
//! # use futures::{
|
||||
//! # future::{self, Ready},
|
||||
//! # prelude::*,
|
||||
//! # };
|
||||
//! # use tarpc::{
|
||||
//! # client, context,
|
||||
//! # server::{self, incoming::Incoming},
|
||||
//! # };
|
||||
//! # // This is the service definition. It looks a lot like a trait definition.
|
||||
//! # // It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
//! # #[tarpc::service]
|
||||
//! # trait World {
|
||||
//! # /// Returns a greeting for name.
|
||||
//! # async fn hello(name: String) -> String;
|
||||
//! # }
|
||||
//! // This is the type that implements the generated World trait. It is the business logic
|
||||
//! // and is used to start the server.
|
||||
//! #[derive(Clone)]
|
||||
//! struct HelloServer;
|
||||
//!
|
||||
//! impl World for HelloServer {
|
||||
//! // Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
//! // an associated type representing the future output by the fn.
|
||||
//!
|
||||
//! type HelloFut = Ready<String>;
|
||||
//!
|
||||
//! fn hello(self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
//! future::ready(format!("Hello, {name}!"))
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! Lastly let's write our `main` that will start the server. While this example uses an
|
||||
//! [in-process channel](transport::channel), tarpc also ships a generic [`serde_transport`]
|
||||
//! behind the `serde-transport` feature, with additional [TCP](serde_transport::tcp) functionality
|
||||
//! available behind the `tcp` feature.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate futures;
|
||||
//! # use futures::{
|
||||
//! # future::{self, Ready},
|
||||
//! # prelude::*,
|
||||
//! # };
|
||||
//! # use tarpc::{
|
||||
//! # client, context,
|
||||
//! # server::{self, Channel},
|
||||
//! # };
|
||||
//! # // This is the service definition. It looks a lot like a trait definition.
|
||||
//! # // It defines one RPC, hello, which takes one arg, name, and returns a String.
|
||||
//! # #[tarpc::service]
|
||||
//! # trait World {
|
||||
//! # /// Returns a greeting for name.
|
||||
//! # async fn hello(name: String) -> String;
|
||||
//! # }
|
||||
//! # // This is the type that implements the generated World trait. It is the business logic
|
||||
//! # // and is used to start the server.
|
||||
//! # #[derive(Clone)]
|
||||
//! # struct HelloServer;
|
||||
//! # impl World for HelloServer {
|
||||
//! # // Each defined rpc generates two items in the trait, a fn that serves the RPC, and
|
||||
//! # // an associated type representing the future output by the fn.
|
||||
//! # type HelloFut = Ready<String>;
|
||||
//! # fn hello(self, _: context::Context, name: String) -> Self::HelloFut {
|
||||
//! # future::ready(format!("Hello, {name}!"))
|
||||
//! # }
|
||||
//! # }
|
||||
//! # #[cfg(not(feature = "tokio1"))]
|
||||
//! # fn main() {}
|
||||
//! # #[cfg(feature = "tokio1")]
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> anyhow::Result<()> {
|
||||
//! let (client_transport, server_transport) = tarpc::transport::channel::unbounded();
|
||||
//!
|
||||
//! let server = server::BaseChannel::with_defaults(server_transport);
|
||||
//! tokio::spawn(server.execute(HelloServer.serve()));
|
||||
//!
|
||||
//! // WorldClient is generated by the #[tarpc::service] attribute. It has a constructor `new`
|
||||
//! // that takes a config and any Transport as input.
|
||||
//! let mut client = WorldClient::new(client::Config::default(), client_transport).spawn();
|
||||
//!
|
||||
//! // The client has an RPC method for each RPC defined in the annotated trait. It takes the same
|
||||
//! // args as defined, with the addition of a Context, which is always the first arg. The Context
|
||||
//! // specifies a deadline and trace information which can be helpful in debugging requests.
|
||||
//! let hello = client.hello(context::current(), "Stim".to_string()).await?;
|
||||
//!
|
||||
//! println!("{hello}");
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Service Documentation
|
||||
//!
|
||||
//! Use `cargo doc` as you normally would to see the documentation created for all
|
||||
//! items expanded by a `service!` invocation.
|
||||
#![deny(missing_docs)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
extern crate serde;
|
||||
extern crate bincode;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate scoped_pool;
|
||||
extern crate unix_socket;
|
||||
#[cfg(feature = "serde1")]
|
||||
#[doc(hidden)]
|
||||
pub use serde;
|
||||
|
||||
macro_rules! pos {
|
||||
() => (concat!(file!(), ":", line!()))
|
||||
#[cfg(feature = "serde-transport")]
|
||||
pub use tokio_serde;
|
||||
|
||||
#[cfg(feature = "serde-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "serde-transport")))]
|
||||
pub mod serde_transport;
|
||||
|
||||
pub mod trace;
|
||||
|
||||
#[cfg(feature = "serde1")]
|
||||
pub use tarpc_plugins::derive_serde;
|
||||
|
||||
/// The main macro that creates RPC services.
|
||||
///
|
||||
/// Rpc methods are specified, mirroring trait syntax:
|
||||
///
|
||||
/// ```
|
||||
/// #[tarpc::service]
|
||||
/// trait Service {
|
||||
/// /// Say hello
|
||||
/// async fn hello(name: String) -> String;
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Attributes can be attached to each rpc. These attributes
|
||||
/// will then be attached to the generated service traits'
|
||||
/// corresponding `fn`s, as well as to the client stubs' RPCs.
|
||||
///
|
||||
/// The following items are expanded in the enclosing module:
|
||||
///
|
||||
/// * `trait Service` -- defines the RPC service.
|
||||
/// * `fn serve` -- turns a service impl into a request handler.
|
||||
/// * `Client` -- a client stub with a fn for each RPC.
|
||||
/// * `fn new_stub` -- creates a new Client stub.
|
||||
pub use tarpc_plugins::service;
|
||||
|
||||
/// A utility macro that can be used for RPC server implementations.
|
||||
///
|
||||
/// Syntactic sugar to make using async functions in the server implementation
|
||||
/// easier. It does this by rewriting code like this, which would normally not
|
||||
/// compile because async functions are disallowed in trait implementations:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tarpc::context;
|
||||
/// # use std::net::SocketAddr;
|
||||
/// #[tarpc::service]
|
||||
/// trait World {
|
||||
/// async fn hello(name: String) -> String;
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Clone)]
|
||||
/// struct HelloServer(SocketAddr);
|
||||
///
|
||||
/// #[tarpc::server]
|
||||
/// impl World for HelloServer {
|
||||
/// async fn hello(self, _: context::Context, name: String) -> String {
|
||||
/// format!("Hello, {name}! You are connected from {:?}.", self.0)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Into code like this, which matches the service trait definition:
|
||||
///
|
||||
/// ```rust
|
||||
/// # use tarpc::context;
|
||||
/// # use std::pin::Pin;
|
||||
/// # use futures::Future;
|
||||
/// # use std::net::SocketAddr;
|
||||
/// #[derive(Clone)]
|
||||
/// struct HelloServer(SocketAddr);
|
||||
///
|
||||
/// #[tarpc::service]
|
||||
/// trait World {
|
||||
/// async fn hello(name: String) -> String;
|
||||
/// }
|
||||
///
|
||||
/// impl World for HelloServer {
|
||||
/// type HelloFut = Pin<Box<dyn Future<Output = String> + Send>>;
|
||||
///
|
||||
/// fn hello(self, _: context::Context, name: String) -> Pin<Box<dyn Future<Output = String>
|
||||
/// + Send>> {
|
||||
/// Box::pin(async move {
|
||||
/// format!("Hello, {name}! You are connected from {:?}.", self.0)
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note that this won't touch functions unless they have been annotated with
|
||||
/// `async`, meaning that this should not break existing code.
|
||||
pub use tarpc_plugins::server;
|
||||
|
||||
pub mod client;
|
||||
pub mod context;
|
||||
pub mod server;
|
||||
pub mod transport;
|
||||
pub(crate) mod util;
|
||||
|
||||
pub use crate::transport::sealed::Transport;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use futures::task::*;
|
||||
use std::{error::Error, fmt::Display, io, time::SystemTime};
|
||||
|
||||
/// A message from a client to a server.
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientMessage<T> {
|
||||
/// A request initiated by a user. The server responds to a request by invoking a
|
||||
/// service-provided request handler. The handler completes with a [`response`](Response), which
|
||||
/// the server sends back to the client.
|
||||
Request(Request<T>),
|
||||
/// A command to cancel an in-flight request, automatically sent by the client when a response
|
||||
/// future is dropped.
|
||||
///
|
||||
/// When received, the server will immediately cancel the main task (top-level future) of the
|
||||
/// request handler for the associated request. Any tasks spawned by the request handler will
|
||||
/// not be canceled, because the framework layer does not
|
||||
/// know about them.
|
||||
Cancel {
|
||||
/// The trace context associates the message with a specific chain of causally-related actions,
|
||||
/// possibly orchestrated across many distributed systems.
|
||||
#[cfg_attr(feature = "serde1", serde(default))]
|
||||
trace_context: trace::Context,
|
||||
/// The ID of the request to cancel.
|
||||
request_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// Provides the tarpc client and server, which implements the tarpc protocol.
|
||||
/// The protocol is defined by the implementation.
|
||||
pub mod protocol;
|
||||
/// A request from a client to a server.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Request<T> {
|
||||
/// Trace context, deadline, and other cross-cutting concerns.
|
||||
pub context: context::Context,
|
||||
/// Uniquely identifies the request across all requests sent over a single channel.
|
||||
pub id: u64,
|
||||
/// The request body.
|
||||
pub message: T,
|
||||
}
|
||||
|
||||
/// Provides the macro used for constructing rpc services and client stubs.
|
||||
pub mod macros;
|
||||
/// A response from a server to a client.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Response<T> {
|
||||
/// The ID of the request being responded to.
|
||||
pub request_id: u64,
|
||||
/// The response body, or an error if the request failed.
|
||||
pub message: Result<T, ServerError>,
|
||||
}
|
||||
|
||||
/// Provides transport traits and implementations.
|
||||
pub mod transport;
|
||||
/// An error indicating the server aborted the request early, e.g., due to request throttling.
|
||||
#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
#[error("{kind:?}: {detail}")]
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ServerError {
|
||||
#[cfg_attr(
|
||||
feature = "serde1",
|
||||
serde(serialize_with = "util::serde::serialize_io_error_kind_as_u32")
|
||||
)]
|
||||
#[cfg_attr(
|
||||
feature = "serde1",
|
||||
serde(deserialize_with = "util::serde::deserialize_io_error_kind_from_u32")
|
||||
)]
|
||||
/// The type of error that occurred to fail the request.
|
||||
pub kind: io::ErrorKind,
|
||||
/// A message describing more detail about the error that occurred.
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
pub use protocol::{Config, Error, Result, ServeHandle};
|
||||
impl<T> Request<T> {
|
||||
/// Returns the deadline for this request.
|
||||
pub fn deadline(&self) -> &SystemTime {
|
||||
&self.context.deadline
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait PollContext<T> {
|
||||
fn context<C>(self, context: C) -> Poll<Option<anyhow::Result<T>>>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Poll<Option<anyhow::Result<T>>>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T, E> PollContext<T> for Poll<Option<Result<T, E>>>
|
||||
where
|
||||
E: Error + Send + Sync + 'static,
|
||||
{
|
||||
fn context<C>(self, context: C) -> Poll<Option<anyhow::Result<T>>>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map(|o| o.map(|r| r.context(context)))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Poll<Option<anyhow::Result<T>>>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map(|o| o.map(|r| r.with_context(f)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,632 +0,0 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
/// Serde re-exports required by macros. Not for general use.
|
||||
pub mod serde {
|
||||
pub use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
/// Deserialization re-exports required by macros. Not for general use.
|
||||
pub mod de {
|
||||
pub use serde::de::{EnumVisitor, Error, VariantVisitor, Visitor};
|
||||
}
|
||||
}
|
||||
|
||||
// Required because if-let can't be used with irrefutable patterns, so it needs
|
||||
// to be special cased.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! client_methods {
|
||||
(
|
||||
{ $(#[$attr:meta])* }
|
||||
$fn_name:ident( ($($arg:ident,)*) : ($($in_:ty,)*) ) -> $out:ty
|
||||
) => (
|
||||
#[allow(unused)]
|
||||
$(#[$attr])*
|
||||
pub fn $fn_name(&self, $($arg: $in_),*) -> $crate::Result<$out> {
|
||||
let reply = try!((self.0).rpc(__Request::$fn_name(($($arg,)*))));
|
||||
let __Reply::$fn_name(reply) = reply;
|
||||
::std::result::Result::Ok(reply)
|
||||
}
|
||||
);
|
||||
($(
|
||||
{ $(#[$attr:meta])* }
|
||||
$fn_name:ident( ($( $arg:ident,)*) : ($($in_:ty, )*) ) -> $out:ty
|
||||
)*) => ( $(
|
||||
#[allow(unused)]
|
||||
$(#[$attr])*
|
||||
pub fn $fn_name(&self, $($arg: $in_),*) -> $crate::Result<$out> {
|
||||
let reply = try!((self.0).rpc(__Request::$fn_name(($($arg,)*))));
|
||||
if let __Reply::$fn_name(reply) = reply {
|
||||
::std::result::Result::Ok(reply)
|
||||
} else {
|
||||
panic!("Incorrect reply variant returned from rpc; expected `{}`, \
|
||||
but got {:?}",
|
||||
stringify!($fn_name),
|
||||
reply);
|
||||
}
|
||||
}
|
||||
)*);
|
||||
}
|
||||
|
||||
// Required because if-let can't be used with irrefutable patterns, so it needs
|
||||
// to be special cased.
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! async_client_methods {
|
||||
(
|
||||
{ $(#[$attr:meta])* }
|
||||
$fn_name:ident( ($( $arg:ident, )*) : ($( $in_:ty, )*) ) -> $out:ty
|
||||
) => (
|
||||
#[allow(unused)]
|
||||
$(#[$attr])*
|
||||
pub fn $fn_name(&self, $($arg: $in_),*) -> Future<$out> {
|
||||
fn mapper(reply: __Reply) -> $out {
|
||||
let __Reply::$fn_name(reply) = reply;
|
||||
reply
|
||||
}
|
||||
let reply = (self.0).rpc_async(__Request::$fn_name(($($arg,)*)));
|
||||
Future {
|
||||
future: reply,
|
||||
mapper: mapper,
|
||||
}
|
||||
}
|
||||
);
|
||||
($(
|
||||
{ $(#[$attr:meta])* }
|
||||
$fn_name:ident( ($( $arg:ident, )*) : ($( $in_:ty, )*) ) -> $out:ty
|
||||
)*) => ( $(
|
||||
#[allow(unused)]
|
||||
$(#[$attr])*
|
||||
pub fn $fn_name(&self, $($arg: $in_),*) -> Future<$out> {
|
||||
fn mapper(reply: __Reply) -> $out {
|
||||
if let __Reply::$fn_name(reply) = reply {
|
||||
reply
|
||||
} else {
|
||||
panic!("Incorrect reply variant returned from rpc; expected `{}`, but got \
|
||||
{:?}",
|
||||
stringify!($fn_name),
|
||||
reply);
|
||||
}
|
||||
}
|
||||
let reply = (self.0).rpc_async(__Request::$fn_name(($($arg,)*)));
|
||||
Future {
|
||||
future: reply,
|
||||
mapper: mapper,
|
||||
}
|
||||
}
|
||||
)*);
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! impl_serialize {
|
||||
($impler:ident, $(@($name:ident $n:expr))* -- #($_n:expr) ) => (
|
||||
impl $crate::macros::serde::Serialize for $impler {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: &mut S) -> ::std::result::Result<(), S::Error>
|
||||
where S: $crate::macros::serde::Serializer
|
||||
{
|
||||
match *self {
|
||||
$(
|
||||
$impler::$name(ref field) =>
|
||||
$crate::macros::serde::Serializer::serialize_newtype_variant(
|
||||
serializer,
|
||||
stringify!($impler),
|
||||
$n,
|
||||
stringify!($name),
|
||||
field,
|
||||
)
|
||||
),*
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
// All args are wrapped in a tuple so we can use the newtype variant for each one.
|
||||
($impler:ident, $(@$finished:tt)* -- #($n:expr) $name:ident($field:ty) $($req:tt)*) => (
|
||||
impl_serialize!($impler, $(@$finished)* @($name $n) -- #($n + 1) $($req)*);
|
||||
);
|
||||
// Entry
|
||||
($impler:ident, $($started:tt)*) => (impl_serialize!($impler, -- #(0) $($started)*););
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_export]
|
||||
macro_rules! impl_deserialize {
|
||||
($impler:ident, $(@($name:ident $n:expr))* -- #($_n:expr) ) => (
|
||||
impl $crate::macros::serde::Deserialize for $impler {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: &mut D)
|
||||
-> ::std::result::Result<$impler, D::Error>
|
||||
where D: $crate::macros::serde::Deserializer
|
||||
{
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
enum __Field {
|
||||
$($name),*
|
||||
}
|
||||
impl $crate::macros::serde::Deserialize for __Field {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: &mut D)
|
||||
-> ::std::result::Result<__Field, D::Error>
|
||||
where D: $crate::macros::serde::Deserializer
|
||||
{
|
||||
struct __FieldVisitor;
|
||||
impl $crate::macros::serde::de::Visitor for __FieldVisitor {
|
||||
type Value = __Field;
|
||||
|
||||
#[inline]
|
||||
fn visit_usize<E>(&mut self, value: usize)
|
||||
-> ::std::result::Result<__Field, E>
|
||||
where E: $crate::macros::serde::de::Error,
|
||||
{
|
||||
$(
|
||||
if value == $n {
|
||||
return ::std::result::Result::Ok(__Field::$name);
|
||||
}
|
||||
)*
|
||||
return ::std::result::Result::Err(
|
||||
$crate::macros::serde::de::Error::custom(
|
||||
format!("No variants have a value of {}!", value))
|
||||
);
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_struct_field(__FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct __Visitor;
|
||||
impl $crate::macros::serde::de::EnumVisitor for __Visitor {
|
||||
type Value = $impler;
|
||||
|
||||
#[inline]
|
||||
fn visit<__V>(&mut self, mut visitor: __V)
|
||||
-> ::std::result::Result<$impler, __V::Error>
|
||||
where __V: $crate::macros::serde::de::VariantVisitor
|
||||
{
|
||||
match try!(visitor.visit_variant()) {
|
||||
$(
|
||||
__Field::$name => {
|
||||
let val = try!(visitor.visit_newtype());
|
||||
Ok($impler::$name(val))
|
||||
}
|
||||
),*
|
||||
}
|
||||
}
|
||||
}
|
||||
const VARIANTS: &'static [&'static str] = &[
|
||||
$(
|
||||
stringify!($name)
|
||||
),*
|
||||
];
|
||||
deserializer.deserialize_enum(stringify!($impler), VARIANTS, __Visitor)
|
||||
}
|
||||
}
|
||||
);
|
||||
// All args are wrapped in a tuple so we can use the newtype variant for each one.
|
||||
($impler:ident, $(@$finished:tt)* -- #($n:expr) $name:ident($field:ty) $($req:tt)*) => (
|
||||
impl_deserialize!($impler, $(@$finished)* @($name $n) -- #($n + 1) $($req)*);
|
||||
);
|
||||
// Entry
|
||||
($impler:ident, $($started:tt)*) => (impl_deserialize!($impler, -- #(0) $($started)*););
|
||||
}
|
||||
|
||||
/// The main macro that creates RPC services.
|
||||
///
|
||||
/// Rpc methods are specified, mirroring trait syntax:
|
||||
///
|
||||
/// ```
|
||||
/// # #[macro_use] extern crate tarpc;
|
||||
/// # fn main() {}
|
||||
/// # service! {
|
||||
/// #[doc="Say hello"]
|
||||
/// rpc hello(name: String) -> String;
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// There are two rpc names reserved for the default fns `spawn` and `spawn_with_config`.
|
||||
///
|
||||
/// Attributes can be attached to each rpc. These attributes
|
||||
/// will then be attached to the generated `Service` trait's
|
||||
/// corresponding method, as well as to the `Client` stub's rpcs methods.
|
||||
///
|
||||
/// The following items are expanded in the enclosing module:
|
||||
///
|
||||
/// * `Service` -- the trait defining the RPC service. It comes with two default methods for
|
||||
/// starting the server:
|
||||
/// 1. `spawn` starts the service in another thread using default configuration.
|
||||
/// 2. `spawn_with_config` starts the service in another thread using the specified
|
||||
/// `Config`.
|
||||
/// * `Client` -- a client that makes synchronous requests to the RPC server
|
||||
/// * `AsyncClient` -- a client that makes asynchronous requests to the RPC server
|
||||
/// * `Future` -- a handle for asynchronously retrieving the result of an RPC
|
||||
///
|
||||
/// **Warning**: In addition to the above items, there are a few expanded items that
|
||||
/// are considered implementation details. As with the above items, shadowing
|
||||
/// these item names in the enclosing module is likely to break things in confusing
|
||||
/// ways:
|
||||
///
|
||||
/// * `__Server` -- an implementation detail
|
||||
/// * `__Request` -- an implementation detail
|
||||
/// * `__Reply` -- an implementation detail
|
||||
#[macro_export]
|
||||
macro_rules! service {
|
||||
// Entry point
|
||||
(
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) $(-> $out:ty)*;
|
||||
)*
|
||||
) => {
|
||||
service! {{
|
||||
$(
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) $(-> $out)*;
|
||||
)*
|
||||
}}
|
||||
};
|
||||
// Pattern for when the next rpc has an implicit unit return type
|
||||
(
|
||||
{
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* );
|
||||
|
||||
$( $unexpanded:tt )*
|
||||
}
|
||||
$( $expanded:tt )*
|
||||
) => {
|
||||
service! {
|
||||
{ $( $unexpanded )* }
|
||||
|
||||
$( $expanded )*
|
||||
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) -> ();
|
||||
}
|
||||
};
|
||||
// Pattern for when the next rpc has an explicit return type
|
||||
(
|
||||
{
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
|
||||
|
||||
$( $unexpanded:tt )*
|
||||
}
|
||||
$( $expanded:tt )*
|
||||
) => {
|
||||
service! {
|
||||
{ $( $unexpanded )* }
|
||||
|
||||
$( $expanded )*
|
||||
|
||||
$(#[$attr])*
|
||||
rpc $fn_name( $( $arg : $in_ ),* ) -> $out;
|
||||
}
|
||||
};
|
||||
// Pattern for when all return types have been expanded
|
||||
(
|
||||
{ } // none left to expand
|
||||
$(
|
||||
$(#[$attr:meta])*
|
||||
rpc $fn_name:ident ( $( $arg:ident : $in_:ty ),* ) -> $out:ty;
|
||||
)*
|
||||
) => {
|
||||
#[doc="Defines the RPC service"]
|
||||
pub trait Service: Send + Sync + Sized {
|
||||
$(
|
||||
$(#[$attr])*
|
||||
fn $fn_name(&self, $($arg:$in_),*) -> $out;
|
||||
)*
|
||||
|
||||
#[doc="Spawn a running service."]
|
||||
fn spawn<T>(self,
|
||||
transport: T)
|
||||
-> $crate::Result<
|
||||
$crate::protocol::ServeHandle<
|
||||
<T::Listener as $crate::transport::Listener>::Dialer>>
|
||||
where T: $crate::transport::Transport,
|
||||
Self: 'static,
|
||||
{
|
||||
self.spawn_with_config(transport, $crate::Config::default())
|
||||
}
|
||||
|
||||
#[doc="Spawn a running service."]
|
||||
fn spawn_with_config<T>(self,
|
||||
transport: T,
|
||||
config: $crate::Config)
|
||||
-> $crate::Result<
|
||||
$crate::protocol::ServeHandle<
|
||||
<T::Listener as $crate::transport::Listener>::Dialer>>
|
||||
where T: $crate::transport::Transport,
|
||||
Self: 'static,
|
||||
{
|
||||
let server = __Server(self);
|
||||
let result = $crate::protocol::Serve::spawn_with_config(server, transport, config);
|
||||
let handle = try!(result);
|
||||
::std::result::Result::Ok(handle)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, S> Service for P
|
||||
where P: Send + Sync + Sized + 'static + ::std::ops::Deref<Target=S>,
|
||||
S: Service
|
||||
{
|
||||
$(
|
||||
$(#[$attr])*
|
||||
fn $fn_name(&self, $($arg:$in_),*) -> $out {
|
||||
Service::$fn_name(&**self, $($arg),*)
|
||||
}
|
||||
)*
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
#[derive(Debug)]
|
||||
enum __Request {
|
||||
$(
|
||||
$fn_name(( $($in_,)* ))
|
||||
),*
|
||||
}
|
||||
|
||||
impl_serialize!(__Request, $($fn_name(($($in_),*)))*);
|
||||
impl_deserialize!(__Request, $($fn_name(($($in_),*)))*);
|
||||
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
#[derive(Debug)]
|
||||
enum __Reply {
|
||||
$(
|
||||
$fn_name($out),
|
||||
)*
|
||||
}
|
||||
|
||||
impl_serialize!(__Reply, $($fn_name($out))*);
|
||||
impl_deserialize!(__Reply, $($fn_name($out))*);
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="An asynchronous RPC call"]
|
||||
pub struct Future<T> {
|
||||
future: $crate::protocol::Future<__Reply>,
|
||||
mapper: fn(__Reply) -> T,
|
||||
}
|
||||
|
||||
impl<T> Future<T> {
|
||||
#[allow(unused)]
|
||||
#[doc="Block until the result of the RPC call is available"]
|
||||
pub fn get(self) -> $crate::Result<T> {
|
||||
self.future.get().map(self.mapper)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="The client stub that makes RPC calls to the server."]
|
||||
pub struct Client<S = ::std::net::TcpStream>(
|
||||
$crate::protocol::Client<__Request, __Reply, S>
|
||||
) where S: $crate::transport::Stream;
|
||||
|
||||
impl<S> Client<S>
|
||||
where S: $crate::transport::Stream
|
||||
{
|
||||
#[allow(unused)]
|
||||
#[doc="Create a new client with default configuration that connects to the given \
|
||||
address."]
|
||||
pub fn new<D>(dialer: D) -> $crate::Result<Self>
|
||||
where D: $crate::transport::Dialer<Stream=S>,
|
||||
{
|
||||
Self::with_config(dialer, $crate::Config::default())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="Create a new client with the specified configuration that connects to the \
|
||||
given address."]
|
||||
pub fn with_config<D>(dialer: D, config: $crate::Config) -> $crate::Result<Self>
|
||||
where D: $crate::transport::Dialer<Stream=S>,
|
||||
{
|
||||
let inner = try!($crate::protocol::Client::with_config(dialer, config));
|
||||
::std::result::Result::Ok(Client(inner))
|
||||
}
|
||||
|
||||
client_methods!(
|
||||
$(
|
||||
{ $(#[$attr])* }
|
||||
$fn_name(($($arg,)*) : ($($in_,)*)) -> $out
|
||||
)*
|
||||
);
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="Attempt to clone the client object. This might fail if the underlying TcpStream \
|
||||
clone fails."]
|
||||
pub fn try_clone(&self) -> ::std::io::Result<Self> {
|
||||
::std::result::Result::Ok(Client(try!(self.0.try_clone())))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="The client stub that makes asynchronous RPC calls to the server."]
|
||||
pub struct AsyncClient<S = ::std::net::TcpStream>(
|
||||
$crate::protocol::Client<__Request, __Reply, S>
|
||||
) where S: $crate::transport::Stream;
|
||||
|
||||
impl<S> AsyncClient<S>
|
||||
where S: $crate::transport::Stream {
|
||||
#[allow(unused)]
|
||||
#[doc="Create a new asynchronous client with default configuration that connects to \
|
||||
the given address."]
|
||||
pub fn new<D>(dialer: D) -> $crate::Result<Self>
|
||||
where D: $crate::transport::Dialer<Stream=S>,
|
||||
{
|
||||
Self::with_config(dialer, $crate::Config::default())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="Create a new asynchronous client that connects to the given address."]
|
||||
pub fn with_config<D>(dialer: D, config: $crate::Config) -> $crate::Result<Self>
|
||||
where D: $crate::transport::Dialer<Stream=S>,
|
||||
{
|
||||
let inner = try!($crate::protocol::Client::with_config(dialer, config));
|
||||
::std::result::Result::Ok(AsyncClient(inner))
|
||||
}
|
||||
|
||||
async_client_methods!(
|
||||
$(
|
||||
{ $(#[$attr])* }
|
||||
$fn_name(($($arg,)*): ($($in_,)*)) -> $out
|
||||
)*
|
||||
);
|
||||
|
||||
#[allow(unused)]
|
||||
#[doc="Attempt to clone the client object. This might fail if the underlying TcpStream \
|
||||
clone fails."]
|
||||
pub fn try_clone(&self) -> ::std::io::Result<Self> {
|
||||
::std::result::Result::Ok(AsyncClient(try!(self.0.try_clone())))
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
struct __Server<S>(S)
|
||||
where S: 'static + Service;
|
||||
|
||||
impl<S> $crate::protocol::Serve for __Server<S>
|
||||
where S: 'static + Service
|
||||
{
|
||||
type Request = __Request;
|
||||
type Reply = __Reply;
|
||||
fn serve(&self, request: __Request) -> __Reply {
|
||||
match request {
|
||||
$(
|
||||
__Request::$fn_name(( $($arg,)* )) =>
|
||||
__Reply::$fn_name((self.0).$fn_name($($arg),*)),
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
// because we're just testing that the macro expansion compiles
|
||||
#[cfg(test)]
|
||||
mod syntax_test {
|
||||
// Tests a service definition with a fn that takes no args
|
||||
mod qux {
|
||||
service! {
|
||||
rpc hello() -> String;
|
||||
}
|
||||
}
|
||||
// Tests a service definition with an attribute.
|
||||
mod bar {
|
||||
service! {
|
||||
#[doc="Hello bob"]
|
||||
rpc baz(s: String) -> String;
|
||||
}
|
||||
}
|
||||
|
||||
// Tests a service with implicit return types.
|
||||
mod no_return {
|
||||
service! {
|
||||
rpc ack();
|
||||
rpc apply(foo: String) -> i32;
|
||||
rpc bi_consume(bar: String, baz: u64);
|
||||
rpc bi_fn(bar: String, baz: u64) -> String;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod functional_test {
|
||||
extern crate env_logger;
|
||||
extern crate tempdir;
|
||||
use transport::unix::UnixTransport;
|
||||
|
||||
service! {
|
||||
rpc add(x: i32, y: i32) -> i32;
|
||||
rpc hey(name: String) -> String;
|
||||
}
|
||||
|
||||
struct Server;
|
||||
|
||||
impl Service for Server {
|
||||
fn add(&self, x: i32, y: i32) -> i32 {
|
||||
x + y
|
||||
}
|
||||
fn hey(&self, name: String) -> String {
|
||||
format!("Hey, {}.", name)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let _ = env_logger::init();
|
||||
let handle = Server.spawn("localhost:0").unwrap();
|
||||
let client = Client::new(handle.dialer()).unwrap();
|
||||
assert_eq!(3, client.add(1, 2).unwrap());
|
||||
assert_eq!("Hey, Tim.", client.hey("Tim".into()).unwrap());
|
||||
drop(client);
|
||||
handle.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_async() {
|
||||
let _ = env_logger::init();
|
||||
let handle = Server.spawn("localhost:0").unwrap();
|
||||
let client = AsyncClient::new(handle.dialer()).unwrap();
|
||||
assert_eq!(3, client.add(1, 2).get().unwrap());
|
||||
assert_eq!("Hey, Adam.", client.hey("Adam".into()).get().unwrap());
|
||||
drop(client);
|
||||
handle.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn try_clone() {
|
||||
let handle = Server.spawn("localhost:0").unwrap();
|
||||
let client1 = Client::new(handle.dialer()).unwrap();
|
||||
let client2 = client1.try_clone().unwrap();
|
||||
assert_eq!(3, client1.add(1, 2).unwrap());
|
||||
assert_eq!(3, client2.add(1, 2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_try_clone() {
|
||||
let handle = Server.spawn("localhost:0").unwrap();
|
||||
let client1 = AsyncClient::new(handle.dialer()).unwrap();
|
||||
let client2 = client1.try_clone().unwrap();
|
||||
assert_eq!(3, client1.add(1, 2).get().unwrap());
|
||||
assert_eq!(3, client2.add(1, 2).get().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async_try_clone_unix() {
|
||||
let temp_dir = tempdir::TempDir::new("tarpc").unwrap();
|
||||
let temp_file = temp_dir.path()
|
||||
.join("async_try_clone_unix.tmp");
|
||||
let handle = Server.spawn(UnixTransport(temp_file)).unwrap();
|
||||
let client1 = AsyncClient::new(handle.dialer()).unwrap();
|
||||
let client2 = client1.try_clone().unwrap();
|
||||
assert_eq!(3, client1.add(1, 2).get().unwrap());
|
||||
assert_eq!(3, client2.add(1, 2).get().unwrap());
|
||||
}
|
||||
|
||||
// Tests that a server can be wrapped in an Arc; no need to run, just compile
|
||||
#[allow(dead_code)]
|
||||
fn serve_arc_server() {
|
||||
let _ = ::std::sync::Arc::new(Server).spawn("localhost:0");
|
||||
}
|
||||
|
||||
// Tests that a tcp client can be created from &str
|
||||
#[allow(dead_code)]
|
||||
fn test_client_str() {
|
||||
let _ = Client::new("localhost:0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
use bincode;
|
||||
let _ = env_logger::init();
|
||||
|
||||
let request = __Request::add((1, 2));
|
||||
let ser = bincode::serde::serialize(&request, bincode::SizeLimit::Infinite).unwrap();
|
||||
let de = bincode::serde::deserialize(&ser).unwrap();
|
||||
if let __Request::add((1, 2)) = de {
|
||||
// success
|
||||
} else {
|
||||
panic!("Expected __Request::add, got {:?}", de);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
use serde;
|
||||
use std::fmt;
|
||||
use std::io::{self, BufReader, BufWriter};
|
||||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::thread;
|
||||
|
||||
use super::{Config, Deserialize, Error, Packet, Result, Serialize};
|
||||
use transport::{Dialer, Stream};
|
||||
|
||||
/// A client stub that connects to a server to run rpcs.
|
||||
pub struct Client<Request, Reply, S>
|
||||
where Request: serde::ser::Serialize,
|
||||
S: Stream
|
||||
{
|
||||
// The guard is in an option so it can be joined in the drop fn
|
||||
reader_guard: Arc<Option<thread::JoinHandle<()>>>,
|
||||
outbound: Sender<(Request, Sender<Result<Reply>>)>,
|
||||
requests: Arc<Mutex<RpcFutures<Reply>>>,
|
||||
shutdown: S,
|
||||
}
|
||||
|
||||
impl<Request, Reply, S> Client<Request, Reply, S>
|
||||
where Request: serde::ser::Serialize + Send + 'static,
|
||||
Reply: serde::de::Deserialize + Send + 'static,
|
||||
S: Stream
|
||||
{
|
||||
/// Create a new client that connects to `addr`. The client uses the given timeout
|
||||
/// for both reads and writes.
|
||||
pub fn new<D>(dialer: D) -> io::Result<Self>
|
||||
where D: Dialer<Stream = S>
|
||||
{
|
||||
Self::with_config(dialer, Config::default())
|
||||
}
|
||||
|
||||
/// Create a new client that connects to `addr`. The client uses the given timeout
|
||||
/// for both reads and writes.
|
||||
pub fn with_config<D>(dialer: D, config: Config) -> io::Result<Self>
|
||||
where D: Dialer<Stream = S>
|
||||
{
|
||||
let stream = try!(dialer.dial());
|
||||
try!(stream.set_read_timeout(config.timeout));
|
||||
try!(stream.set_write_timeout(config.timeout));
|
||||
let reader_stream = try!(stream.try_clone());
|
||||
let writer_stream = try!(stream.try_clone());
|
||||
let requests = Arc::new(Mutex::new(RpcFutures::new()));
|
||||
let reader_requests = requests.clone();
|
||||
let writer_requests = requests.clone();
|
||||
let (tx, rx) = channel();
|
||||
let reader_guard = thread::spawn(move || read(reader_requests, reader_stream));
|
||||
thread::spawn(move || write(rx, writer_requests, writer_stream));
|
||||
Ok(Client {
|
||||
reader_guard: Arc::new(Some(reader_guard)),
|
||||
outbound: tx,
|
||||
requests: requests,
|
||||
shutdown: stream,
|
||||
})
|
||||
}
|
||||
|
||||
/// Clones the Client so that it can be shared across threads.
|
||||
pub fn try_clone(&self) -> io::Result<Self> {
|
||||
Ok(Client {
|
||||
reader_guard: self.reader_guard.clone(),
|
||||
outbound: self.outbound.clone(),
|
||||
requests: self.requests.clone(),
|
||||
shutdown: try!(self.shutdown.try_clone()),
|
||||
})
|
||||
}
|
||||
|
||||
fn rpc_internal(&self, request: Request) -> Receiver<Result<Reply>>
|
||||
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
|
||||
{
|
||||
let (tx, rx) = channel();
|
||||
self.outbound.send((request, tx)).expect(pos!());
|
||||
rx
|
||||
}
|
||||
|
||||
/// Run the specified rpc method on the server this client is connected to
|
||||
pub fn rpc(&self, request: Request) -> Result<Reply>
|
||||
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
|
||||
{
|
||||
self.rpc_internal(request)
|
||||
.recv()
|
||||
.map_err(|_| self.requests.lock().expect(pos!()).get_error())
|
||||
.and_then(|reply| reply)
|
||||
}
|
||||
|
||||
/// Asynchronously run the specified rpc method on the server this client is connected to
|
||||
pub fn rpc_async(&self, request: Request) -> Future<Reply>
|
||||
where Request: serde::ser::Serialize + fmt::Debug + Send + 'static
|
||||
{
|
||||
Future {
|
||||
rx: self.rpc_internal(request),
|
||||
requests: self.requests.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Request, Reply, S> Drop for Client<Request, Reply, S>
|
||||
where Request: serde::ser::Serialize,
|
||||
S: Stream
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
debug!("Dropping Client.");
|
||||
if let Some(reader_guard) = Arc::get_mut(&mut self.reader_guard) {
|
||||
debug!("Attempting to shut down writer and reader threads.");
|
||||
if let Err(e) = self.shutdown.shutdown() {
|
||||
warn!("Client: couldn't shutdown writer and reader threads: {:?}",
|
||||
e);
|
||||
} else {
|
||||
// We only join if we know the TcpStream was shut down. Otherwise we might never
|
||||
// finish.
|
||||
debug!("Joining writer and reader.");
|
||||
reader_guard.take()
|
||||
.expect(pos!())
|
||||
.join()
|
||||
.expect(pos!());
|
||||
debug!("Successfully joined writer and reader.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An asynchronous RPC call
|
||||
pub struct Future<T> {
|
||||
rx: Receiver<Result<T>>,
|
||||
requests: Arc<Mutex<RpcFutures<T>>>,
|
||||
}
|
||||
|
||||
impl<T> Future<T> {
|
||||
/// Block until the result of the RPC call is available
|
||||
pub fn get(self) -> Result<T> {
|
||||
let requests = self.requests;
|
||||
self.rx
|
||||
.recv()
|
||||
.map_err(|_| requests.lock().expect(pos!()).get_error())
|
||||
.and_then(|reply| reply)
|
||||
}
|
||||
}
|
||||
|
||||
struct RpcFutures<Reply>(Result<HashMap<u64, Sender<Result<Reply>>>>);
|
||||
|
||||
impl<Reply> RpcFutures<Reply> {
|
||||
fn new() -> RpcFutures<Reply> {
|
||||
RpcFutures(Ok(HashMap::new()))
|
||||
}
|
||||
|
||||
fn insert_tx(&mut self, id: u64, tx: Sender<Result<Reply>>) -> Result<()> {
|
||||
match self.0 {
|
||||
Ok(ref mut requests) => {
|
||||
requests.insert(id, tx);
|
||||
Ok(())
|
||||
}
|
||||
Err(ref e) => Err(e.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_tx(&mut self, id: u64) -> Result<()> {
|
||||
match self.0 {
|
||||
Ok(ref mut requests) => {
|
||||
requests.remove(&id);
|
||||
Ok(())
|
||||
}
|
||||
Err(ref e) => Err(e.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn complete_reply(&mut self, packet: Packet<Reply>) {
|
||||
if let Some(tx) = self.0.as_mut().expect(pos!()).remove(&packet.rpc_id) {
|
||||
if let Err(e) = tx.send(Ok(packet.message)) {
|
||||
info!("Reader: could not complete reply: {:?}", e);
|
||||
}
|
||||
} else {
|
||||
warn!("RpcFutures: expected sender for id {} but got None!",
|
||||
packet.rpc_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, err: Error) {
|
||||
let _ = mem::replace(&mut self.0, Err(err));
|
||||
}
|
||||
|
||||
fn get_error(&self) -> Error {
|
||||
self.0.as_ref().err().expect(pos!()).clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn write<Request, Reply, S>(outbound: Receiver<(Request, Sender<Result<Reply>>)>,
|
||||
requests: Arc<Mutex<RpcFutures<Reply>>>,
|
||||
stream: S)
|
||||
where Request: serde::Serialize,
|
||||
Reply: serde::Deserialize,
|
||||
S: Stream
|
||||
{
|
||||
let mut next_id = 0;
|
||||
let mut stream = BufWriter::new(stream);
|
||||
loop {
|
||||
let (request, tx) = match outbound.recv() {
|
||||
Err(e) => {
|
||||
debug!("Writer: all senders have exited ({:?}). Returning.", e);
|
||||
return;
|
||||
}
|
||||
Ok(request) => request,
|
||||
};
|
||||
if let Err(e) = requests.lock().expect(pos!()).insert_tx(next_id, tx.clone()) {
|
||||
report_error(&tx, e);
|
||||
// Once insert_tx returns Err, it will continue to do so. However, continue here so
|
||||
// that any other clients who sent requests will also recv the Err.
|
||||
continue;
|
||||
}
|
||||
let id = next_id;
|
||||
next_id += 1;
|
||||
let packet = Packet {
|
||||
rpc_id: id,
|
||||
message: request,
|
||||
};
|
||||
debug!("Writer: writing rpc, id={:?}", id);
|
||||
if let Err(e) = stream.serialize(&packet) {
|
||||
report_error(&tx, e.into());
|
||||
// Typically we'd want to notify the client of any Err returned by remove_tx, but in
|
||||
// this case the client already hit an Err, and doesn't need to know about this one, as
|
||||
// well.
|
||||
let _ = requests.lock().expect(pos!()).remove_tx(id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
fn report_error<Reply>(tx: &Sender<Result<Reply>>, e: Error)
|
||||
where Reply: serde::Deserialize
|
||||
{
|
||||
// Clone the err so we can log it if sending fails
|
||||
if let Err(e2) = tx.send(Err(e.clone())) {
|
||||
debug!("Error encountered while trying to send an error. Initial error: {:?}; Send \
|
||||
error: {:?}",
|
||||
e,
|
||||
e2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn read<Reply, S>(requests: Arc<Mutex<RpcFutures<Reply>>>, stream: S)
|
||||
where Reply: serde::Deserialize,
|
||||
S: Stream
|
||||
{
|
||||
let mut stream = BufReader::new(stream);
|
||||
loop {
|
||||
match stream.deserialize::<Packet<Reply>>() {
|
||||
Ok(packet) => {
|
||||
debug!("Client: received message, id={}", packet.rpc_id);
|
||||
requests.lock().expect(pos!()).complete_reply(packet);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Client: reader thread encountered an unexpected error while parsing; \
|
||||
returning now. Error: {:?}",
|
||||
err);
|
||||
requests.lock().expect(pos!()).set_error(err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,249 +0,0 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
use bincode::{self, SizeLimit};
|
||||
use bincode::serde::{deserialize_from, serialize_into};
|
||||
use serde;
|
||||
use serde::de::value::Error::EndOfStream;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::convert;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
mod client;
|
||||
mod server;
|
||||
mod packet;
|
||||
|
||||
pub use self::packet::Packet;
|
||||
pub use self::client::{Client, Future};
|
||||
pub use self::server::{Serve, ServeHandle};
|
||||
|
||||
/// Client errors that can occur during rpc calls
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
/// An IO-related error
|
||||
Io(Arc<io::Error>),
|
||||
/// The server hung up.
|
||||
ConnectionBroken,
|
||||
}
|
||||
|
||||
impl convert::From<bincode::serde::SerializeError> for Error {
|
||||
fn from(err: bincode::serde::SerializeError) -> Error {
|
||||
match err {
|
||||
bincode::serde::SerializeError::IoError(err) => Error::Io(Arc::new(err)),
|
||||
err => panic!("Unexpected error during serialization: {:?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<bincode::serde::DeserializeError> for Error {
|
||||
fn from(err: bincode::serde::DeserializeError) -> Error {
|
||||
match err {
|
||||
bincode::serde::DeserializeError::Serde(EndOfStream) => Error::ConnectionBroken,
|
||||
bincode::serde::DeserializeError::IoError(err) => {
|
||||
match err.kind() {
|
||||
io::ErrorKind::ConnectionReset |
|
||||
io::ErrorKind::UnexpectedEof => Error::ConnectionBroken,
|
||||
_ => Error::Io(Arc::new(err)),
|
||||
}
|
||||
}
|
||||
err => panic!("Unexpected error during deserialization: {:?}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl convert::From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(Arc::new(err))
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for client and server.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Config {
|
||||
/// Request/Response timeout between packet delivery.
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Return type of rpc calls: either the successful return value, or a client error.
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
|
||||
trait Deserialize: Read + Sized {
|
||||
fn deserialize<T: serde::Deserialize>(&mut self) -> Result<T> {
|
||||
deserialize_from(self, SizeLimit::Infinite).map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Deserialize for R {}
|
||||
|
||||
trait Serialize: Write + Sized {
|
||||
fn serialize<T: serde::Serialize>(&mut self, value: &T) -> Result<()> {
|
||||
try!(serialize_into(self, value, SizeLimit::Infinite));
|
||||
try!(self.flush());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Write> Serialize for W {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
extern crate env_logger;
|
||||
use super::{Client, Config, Serve};
|
||||
use scoped_pool::Pool;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::{Arc, Barrier, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn test_timeout() -> Option<Duration> {
|
||||
Some(Duration::from_secs(1))
|
||||
}
|
||||
|
||||
struct Server {
|
||||
counter: Mutex<u64>,
|
||||
}
|
||||
|
||||
impl Serve for Server {
|
||||
type Request = ();
|
||||
type Reply = u64;
|
||||
|
||||
fn serve(&self, _: ()) -> u64 {
|
||||
let mut counter = self.counter.lock().unwrap();
|
||||
let reply = *counter;
|
||||
*counter += 1;
|
||||
reply
|
||||
}
|
||||
}
|
||||
|
||||
impl Server {
|
||||
fn new() -> Server {
|
||||
Server { counter: Mutex::new(0) }
|
||||
}
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
*self.counter.lock().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle() {
|
||||
let _ = env_logger::init();
|
||||
let server = Arc::new(Server::new());
|
||||
let serve_handle = server.spawn("localhost:0").unwrap();
|
||||
let client: Client<(), u64, TcpStream> = Client::new(serve_handle.dialer()).unwrap();
|
||||
drop(client);
|
||||
serve_handle.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let _ = env_logger::init();
|
||||
let server = Arc::new(Server::new());
|
||||
let serve_handle = server.clone().spawn("localhost:0").unwrap();
|
||||
// The explicit type is required so that it doesn't deserialize a u32 instead of u64
|
||||
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
|
||||
assert_eq!(0, client.rpc(()).unwrap());
|
||||
assert_eq!(1, server.count());
|
||||
assert_eq!(1, client.rpc(()).unwrap());
|
||||
assert_eq!(2, server.count());
|
||||
drop(client);
|
||||
serve_handle.shutdown();
|
||||
}
|
||||
|
||||
struct BarrierServer {
|
||||
barrier: Barrier,
|
||||
inner: Server,
|
||||
}
|
||||
|
||||
impl Serve for BarrierServer {
|
||||
type Request = ();
|
||||
type Reply = u64;
|
||||
fn serve(&self, request: ()) -> u64 {
|
||||
self.barrier.wait();
|
||||
self.inner.serve(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarrierServer {
|
||||
fn new(n: usize) -> BarrierServer {
|
||||
BarrierServer {
|
||||
barrier: Barrier::new(n),
|
||||
inner: Server::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn count(&self) -> u64 {
|
||||
self.inner.count()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_shutdown() {
|
||||
let _ = env_logger::init();
|
||||
let server = Arc::new(Server::new());
|
||||
let serve_handle = server.spawn_with_config("localhost:0",
|
||||
Config { timeout: Some(Duration::new(0, 10)) })
|
||||
.unwrap();
|
||||
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
|
||||
let thread = thread::spawn(move || serve_handle.shutdown());
|
||||
info!("force_shutdown:: rpc1: {:?}", client.rpc(()));
|
||||
thread.join().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_failed_rpc() {
|
||||
let _ = env_logger::init();
|
||||
let server = Arc::new(Server::new());
|
||||
let serve_handle =
|
||||
server.spawn_with_config("localhost:0", Config { timeout: test_timeout() })
|
||||
.unwrap();
|
||||
let client: Arc<Client<(), u64, _>> = Arc::new(Client::new(serve_handle.dialer()).unwrap());
|
||||
client.rpc(()).unwrap();
|
||||
serve_handle.shutdown();
|
||||
match client.rpc(()) {
|
||||
Err(super::Error::ConnectionBroken) => {} // success
|
||||
otherwise => panic!("Expected Err(ConnectionBroken), got {:?}", otherwise),
|
||||
}
|
||||
let _ = client.rpc(()); // Test whether second failure hangs
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent() {
|
||||
let _ = env_logger::init();
|
||||
let concurrency = 10;
|
||||
let pool = Pool::new(concurrency);
|
||||
let server = Arc::new(BarrierServer::new(concurrency));
|
||||
let serve_handle = server.clone().spawn("localhost:0").unwrap();
|
||||
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
|
||||
pool.scoped(|scope| {
|
||||
for _ in 0..concurrency {
|
||||
let client = client.try_clone().unwrap();
|
||||
scope.execute(move || {
|
||||
client.rpc(()).unwrap();
|
||||
});
|
||||
}
|
||||
});
|
||||
assert_eq!(concurrency as u64, server.count());
|
||||
drop(client);
|
||||
serve_handle.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn async() {
|
||||
let _ = env_logger::init();
|
||||
let server = Arc::new(Server::new());
|
||||
let serve_handle = server.spawn("localhost:0").unwrap();
|
||||
let client: Client<(), u64, _> = Client::new(serve_handle.dialer()).unwrap();
|
||||
|
||||
// Drop future immediately; does the reader channel panic when sending?
|
||||
client.rpc_async(());
|
||||
// If the reader panicked, this won't succeed
|
||||
client.rpc_async(());
|
||||
|
||||
drop(client);
|
||||
serve_handle.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
/// Packet shared between client and server.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Packet<T> {
|
||||
/// Packet id to map response to request.
|
||||
pub rpc_id: u64,
|
||||
/// Packet payload.
|
||||
pub message: T,
|
||||
}
|
||||
|
||||
const PACKET: &'static str = "Packet";
|
||||
const RPC_ID: &'static str = "rpc_id";
|
||||
const MESSAGE: &'static str = "message";
|
||||
|
||||
impl<T: Serialize> Serialize for Packet<T> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
|
||||
where S: Serializer
|
||||
{
|
||||
let mut state = try!(serializer.serialize_struct(PACKET, 2));
|
||||
try!(serializer.serialize_struct_elt(&mut state, RPC_ID, &self.rpc_id));
|
||||
try!(serializer.serialize_struct_elt(&mut state, MESSAGE, &self.message));
|
||||
serializer.serialize_struct_end(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deserialize> Deserialize for Packet<T> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: &mut D) -> Result<Self, D::Error>
|
||||
where D: Deserializer
|
||||
{
|
||||
const FIELDS: &'static [&'static str] = &[RPC_ID, MESSAGE];
|
||||
deserializer.deserialize_struct(PACKET, FIELDS, Visitor(PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
struct Visitor<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Deserialize> de::Visitor for Visitor<T> {
|
||||
type Value = Packet<T>;
|
||||
|
||||
#[inline]
|
||||
fn visit_seq<V>(&mut self, mut visitor: V) -> Result<Packet<T>, V::Error>
|
||||
where V: de::SeqVisitor
|
||||
{
|
||||
let packet = Packet {
|
||||
rpc_id: match try!(visitor.visit()) {
|
||||
Some(rpc_id) => rpc_id,
|
||||
None => return Err(de::Error::end_of_stream()),
|
||||
},
|
||||
message: match try!(visitor.visit()) {
|
||||
Some(message) => message,
|
||||
None => return Err(de::Error::end_of_stream()),
|
||||
},
|
||||
};
|
||||
try!(visitor.end());
|
||||
Ok(packet)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
extern crate env_logger;
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
use bincode;
|
||||
let _ = env_logger::init();
|
||||
|
||||
let packet = Packet {
|
||||
rpc_id: 1,
|
||||
message: (),
|
||||
};
|
||||
let ser = bincode::serde::serialize(&packet, bincode::SizeLimit::Infinite).unwrap();
|
||||
let de = bincode::serde::deserialize(&ser);
|
||||
assert_eq!(packet, de.unwrap());
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
use serde;
|
||||
use scoped_pool::{Pool, Scope};
|
||||
use std::fmt;
|
||||
use std::io::{self, BufReader, BufWriter};
|
||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError, channel};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use std::thread::{self, JoinHandle};
|
||||
use super::{Config, Deserialize, Error, Packet, Result, Serialize};
|
||||
use transport::{Dialer, Listener, Stream, Transport};
|
||||
use transport::tcp::TcpDialer;
|
||||
|
||||
struct ConnectionHandler<'a, S, St>
|
||||
where S: Serve,
|
||||
St: Stream
|
||||
{
|
||||
read_stream: BufReader<St>,
|
||||
write_stream: BufWriter<St>,
|
||||
server: S,
|
||||
shutdown: &'a AtomicBool,
|
||||
}
|
||||
|
||||
impl<'a, S, St> ConnectionHandler<'a, S, St>
|
||||
where S: Serve,
|
||||
St: Stream
|
||||
{
|
||||
fn handle_conn<'b>(&'b mut self, scope: &Scope<'b>) -> Result<()> {
|
||||
let ConnectionHandler {
|
||||
ref mut read_stream,
|
||||
ref mut write_stream,
|
||||
ref server,
|
||||
shutdown,
|
||||
} = *self;
|
||||
trace!("ConnectionHandler: serving client...");
|
||||
let (tx, rx) = channel();
|
||||
scope.execute(move || Self::write(rx, write_stream));
|
||||
loop {
|
||||
match read_stream.deserialize() {
|
||||
Ok(Packet { rpc_id, message }) => {
|
||||
let tx = tx.clone();
|
||||
scope.execute(move || {
|
||||
let reply = server.serve(message);
|
||||
let reply_packet = Packet {
|
||||
rpc_id: rpc_id,
|
||||
message: reply,
|
||||
};
|
||||
tx.send(reply_packet).expect(pos!());
|
||||
});
|
||||
if shutdown.load(Ordering::SeqCst) {
|
||||
info!("ConnectionHandler: server shutdown, so closing connection.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(Error::Io(ref err)) if Self::timed_out(err.kind()) => {
|
||||
if !shutdown.load(Ordering::SeqCst) {
|
||||
info!("ConnectionHandler: read timed out ({:?}). Server not shutdown, so \
|
||||
retrying read.",
|
||||
err);
|
||||
continue;
|
||||
} else {
|
||||
info!("ConnectionHandler: read timed out ({:?}). Server shutdown, so \
|
||||
closing connection.",
|
||||
err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("ConnectionHandler: closing client connection due to {:?}",
|
||||
e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn timed_out(error_kind: io::ErrorKind) -> bool {
|
||||
match error_kind {
|
||||
io::ErrorKind::TimedOut |
|
||||
io::ErrorKind::WouldBlock => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn write(rx: Receiver<Packet<<S as Serve>::Reply>>, stream: &mut BufWriter<St>) {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Err(e) => {
|
||||
debug!("Write thread: returning due to {:?}", e);
|
||||
return;
|
||||
}
|
||||
Ok(reply_packet) => {
|
||||
if let Err(e) = stream.serialize(&reply_packet) {
|
||||
warn!("Writer: failed to write reply to Client: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides methods for blocking until the server completes,
|
||||
pub struct ServeHandle<D = TcpDialer>
|
||||
where D: Dialer
|
||||
{
|
||||
tx: Sender<()>,
|
||||
join_handle: JoinHandle<()>,
|
||||
dialer: D,
|
||||
}
|
||||
|
||||
impl<D> ServeHandle<D>
|
||||
where D: Dialer
|
||||
{
|
||||
/// Block until the server completes
|
||||
pub fn wait(self) {
|
||||
self.join_handle.join().expect(pos!());
|
||||
}
|
||||
|
||||
/// Returns the dialer to the server.
|
||||
pub fn dialer(&self) -> &D {
|
||||
&self.dialer
|
||||
}
|
||||
|
||||
/// Shutdown the server. Gracefully shuts down the serve thread but currently does not
|
||||
/// gracefully close open connections.
|
||||
pub fn shutdown(self) {
|
||||
info!("ServeHandle: attempting to shut down the server.");
|
||||
self.tx.send(()).expect(pos!());
|
||||
if let Ok(_) = self.dialer.dial() {
|
||||
self.join_handle.join().expect(pos!());
|
||||
} else {
|
||||
warn!("ServeHandle: best effort shutdown of serve thread failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Server<'a, S: 'a, L>
|
||||
where L: Listener
|
||||
{
|
||||
server: &'a S,
|
||||
listener: L,
|
||||
read_timeout: Option<Duration>,
|
||||
die_rx: Receiver<()>,
|
||||
shutdown: &'a AtomicBool,
|
||||
}
|
||||
|
||||
impl<'a, S, L> Server<'a, S, L>
|
||||
where S: Serve + 'static,
|
||||
L: Listener
|
||||
{
|
||||
fn serve<'b>(self, scope: &Scope<'b>)
|
||||
where 'a: 'b
|
||||
{
|
||||
for conn in self.listener.incoming() {
|
||||
match self.die_rx.try_recv() {
|
||||
Ok(_) => {
|
||||
info!("serve: shutdown received.");
|
||||
return;
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
info!("serve: shutdown sender disconnected.");
|
||||
return;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
let conn = match conn {
|
||||
Err(err) => {
|
||||
error!("serve: failed to accept connection: {:?}", err);
|
||||
return;
|
||||
}
|
||||
Ok(c) => c,
|
||||
};
|
||||
if let Err(err) = conn.set_read_timeout(self.read_timeout) {
|
||||
info!("serve: could not set read timeout: {:?}", err);
|
||||
continue;
|
||||
}
|
||||
let read_conn = match conn.try_clone() {
|
||||
Err(err) => {
|
||||
error!("serve: could not clone tcp stream; possibly out of file descriptors? \
|
||||
Err: {:?}",
|
||||
err);
|
||||
continue;
|
||||
}
|
||||
Ok(conn) => conn,
|
||||
};
|
||||
let mut handler = ConnectionHandler {
|
||||
read_stream: BufReader::new(read_conn),
|
||||
write_stream: BufWriter::new(conn),
|
||||
server: self.server,
|
||||
shutdown: self.shutdown,
|
||||
};
|
||||
scope.recurse(move |scope| {
|
||||
scope.zoom(|scope| {
|
||||
if let Err(err) = handler.handle_conn(scope) {
|
||||
info!("ConnectionHandler: err in connection handling: {:?}", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S, L> Drop for Server<'a, S, L>
|
||||
where L: Listener
|
||||
{
|
||||
fn drop(&mut self) {
|
||||
debug!("Shutting down connection handlers.");
|
||||
self.shutdown.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// A service provided by a server
|
||||
pub trait Serve: Send + Sync + Sized {
|
||||
/// The type of request received by the server
|
||||
type Request: 'static + fmt::Debug + serde::ser::Serialize + serde::de::Deserialize + Send;
|
||||
/// The type of reply sent by the server
|
||||
type Reply: 'static + fmt::Debug + serde::ser::Serialize + serde::de::Deserialize + Send;
|
||||
|
||||
/// Return a reply for a given request
|
||||
fn serve(&self, request: Self::Request) -> Self::Reply;
|
||||
|
||||
/// spawn
|
||||
fn spawn<T>(self, transport: T) -> io::Result<ServeHandle<<T::Listener as Listener>::Dialer>>
|
||||
where T: Transport,
|
||||
Self: 'static
|
||||
{
|
||||
self.spawn_with_config(transport, Config::default())
|
||||
}
|
||||
|
||||
/// spawn
|
||||
fn spawn_with_config<T>(self,
|
||||
transport: T,
|
||||
config: Config)
|
||||
-> io::Result<ServeHandle<<T::Listener as Listener>::Dialer>>
|
||||
where T: Transport,
|
||||
Self: 'static
|
||||
{
|
||||
let listener = try!(transport.bind());
|
||||
let dialer = try!(listener.dialer());
|
||||
info!("spawn_with_config: spinning up server.");
|
||||
let (die_tx, die_rx) = channel();
|
||||
let timeout = config.timeout;
|
||||
let join_handle = thread::spawn(move || {
|
||||
let pool = Pool::new(100); // TODO(tjk): make this configurable, and expire idle threads
|
||||
let shutdown = AtomicBool::new(false);
|
||||
let server = Server {
|
||||
server: &self,
|
||||
listener: listener,
|
||||
read_timeout: timeout,
|
||||
die_rx: die_rx,
|
||||
shutdown: &shutdown,
|
||||
};
|
||||
pool.scoped(|scope| {
|
||||
server.serve(scope);
|
||||
});
|
||||
});
|
||||
Ok(ServeHandle {
|
||||
tx: die_tx,
|
||||
join_handle: join_handle,
|
||||
dialer: dialer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<P, S> Serve for P
|
||||
where P: Send + Sync + ::std::ops::Deref<Target = S>,
|
||||
S: Serve
|
||||
{
|
||||
type Request = S::Request;
|
||||
type Reply = S::Reply;
|
||||
|
||||
fn serve(&self, request: S::Request) -> S::Reply {
|
||||
S::serve(self, request)
|
||||
}
|
||||
}
|
||||
395
tarpc/src/serde_transport.rs
Normal file
395
tarpc/src/serde_transport.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
// Copyright 2019 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! A generic Serde-based `Transport` that can serialize anything supported by `tokio-serde` via any medium that implements `AsyncRead` and `AsyncWrite`.
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use futures::{prelude::*, task::*};
|
||||
use pin_project::pin_project;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{error::Error, io, pin::Pin};
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
use tokio_serde::{Framed as SerdeFramed, *};
|
||||
use tokio_util::codec::{length_delimited::LengthDelimitedCodec, Framed};
|
||||
|
||||
/// A transport that serializes to, and deserializes from, a byte stream.
|
||||
#[pin_project]
|
||||
pub struct Transport<S, Item, SinkItem, Codec> {
|
||||
#[pin]
|
||||
inner: SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>,
|
||||
}
|
||||
|
||||
impl<S, Item, SinkItem, Codec> Transport<S, Item, SinkItem, Codec> {
|
||||
/// Returns the inner transport over which messages are sent and received.
|
||||
pub fn get_ref(&self) -> &S {
|
||||
self.inner.get_ref().get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Item, SinkItem, Codec, CodecError> Stream for Transport<S, Item, SinkItem, Codec>
|
||||
where
|
||||
S: AsyncWrite + AsyncRead,
|
||||
Item: for<'a> Deserialize<'a>,
|
||||
Codec: Deserializer<Item>,
|
||||
CodecError: Into<Box<dyn std::error::Error + Send + Sync>>,
|
||||
SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>:
|
||||
Stream<Item = Result<Item, CodecError>>,
|
||||
{
|
||||
type Item = io::Result<Item>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<io::Result<Item>>> {
|
||||
self.project()
|
||||
.inner
|
||||
.poll_next(cx)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Item, SinkItem, Codec, CodecError> Sink<SinkItem> for Transport<S, Item, SinkItem, Codec>
|
||||
where
|
||||
S: AsyncWrite,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem>,
|
||||
CodecError: Into<Box<dyn Error + Send + Sync>>,
|
||||
SerdeFramed<Framed<S, LengthDelimitedCodec>, Item, SinkItem, Codec>:
|
||||
Sink<SinkItem, Error = CodecError>,
|
||||
{
|
||||
type Error = io::Error;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.project()
|
||||
.inner
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> io::Result<()> {
|
||||
self.project()
|
||||
.inner
|
||||
.start_send(item)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.project()
|
||||
.inner
|
||||
.poll_flush(cx)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
self.project()
|
||||
.inner
|
||||
.poll_close(cx)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a new transport from a framed transport and a serialization codec.
|
||||
pub fn new<S, Item, SinkItem, Codec>(
|
||||
framed_io: Framed<S, LengthDelimitedCodec>,
|
||||
codec: Codec,
|
||||
) -> Transport<S, Item, SinkItem, Codec>
|
||||
where
|
||||
S: AsyncWrite + AsyncRead,
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
{
|
||||
Transport {
|
||||
inner: SerdeFramed::new(framed_io, codec),
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, Item, SinkItem, Codec> From<(S, Codec)> for Transport<S, Item, SinkItem, Codec>
|
||||
where
|
||||
S: AsyncWrite + AsyncRead,
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
{
|
||||
fn from((io, codec): (S, Codec)) -> Self {
|
||||
new(Framed::new(io, LengthDelimitedCodec::new()), codec)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tcp")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tcp")))]
|
||||
/// TCP support for generic transport using Tokio.
|
||||
pub mod tcp {
|
||||
use {
|
||||
super::*,
|
||||
futures::ready,
|
||||
std::{marker::PhantomData, net::SocketAddr},
|
||||
tokio::net::{TcpListener, TcpStream, ToSocketAddrs},
|
||||
tokio_util::codec::length_delimited,
|
||||
};
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
impl<Item, SinkItem, Codec> Sealed for Transport<TcpStream, Item, SinkItem, Codec> {}
|
||||
}
|
||||
|
||||
impl<Item, SinkItem, Codec> Transport<TcpStream, Item, SinkItem, Codec> {
|
||||
/// Returns the peer address of the underlying TcpStream.
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
self.inner.get_ref().get_ref().peer_addr()
|
||||
}
|
||||
/// Returns the local address of the underlying TcpStream.
|
||||
pub fn local_addr(&self) -> io::Result<SocketAddr> {
|
||||
self.inner.get_ref().get_ref().local_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// A connection Future that also exposes the length-delimited framing config.
|
||||
#[pin_project]
|
||||
pub struct Connect<T, Item, SinkItem, CodecFn> {
|
||||
#[pin]
|
||||
inner: T,
|
||||
codec_fn: CodecFn,
|
||||
config: length_delimited::Builder,
|
||||
ghost: PhantomData<(fn(SinkItem), fn() -> Item)>,
|
||||
}
|
||||
|
||||
impl<T, Item, SinkItem, Codec, CodecFn> Future for Connect<T, Item, SinkItem, CodecFn>
|
||||
where
|
||||
T: Future<Output = io::Result<TcpStream>>,
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
CodecFn: Fn() -> Codec,
|
||||
{
|
||||
type Output = io::Result<Transport<TcpStream, Item, SinkItem, Codec>>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
let io = ready!(self.as_mut().project().inner.poll(cx))?;
|
||||
Poll::Ready(Ok(new(self.config.new_framed(io), (self.codec_fn)())))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Item, SinkItem, CodecFn> Connect<T, Item, SinkItem, CodecFn> {
|
||||
/// Returns an immutable reference to the length-delimited codec's config.
|
||||
pub fn config(&self) -> &length_delimited::Builder {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the length-delimited codec's config.
|
||||
pub fn config_mut(&mut self) -> &mut length_delimited::Builder {
|
||||
&mut self.config
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to `addr`, wrapping the connection in a TCP transport.
|
||||
pub fn connect<A, Item, SinkItem, Codec, CodecFn>(
|
||||
addr: A,
|
||||
codec_fn: CodecFn,
|
||||
) -> Connect<impl Future<Output = io::Result<TcpStream>>, Item, SinkItem, CodecFn>
|
||||
where
|
||||
A: ToSocketAddrs,
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
CodecFn: Fn() -> Codec,
|
||||
{
|
||||
Connect {
|
||||
inner: TcpStream::connect(addr),
|
||||
codec_fn,
|
||||
config: LengthDelimitedCodec::builder(),
|
||||
ghost: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Listens on `addr`, wrapping accepted connections in TCP transports.
|
||||
pub async fn listen<A, Item, SinkItem, Codec, CodecFn>(
|
||||
addr: A,
|
||||
codec_fn: CodecFn,
|
||||
) -> io::Result<Incoming<Item, SinkItem, Codec, CodecFn>>
|
||||
where
|
||||
A: ToSocketAddrs,
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
CodecFn: Fn() -> Codec,
|
||||
{
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
let local_addr = listener.local_addr()?;
|
||||
Ok(Incoming {
|
||||
listener,
|
||||
codec_fn,
|
||||
local_addr,
|
||||
config: LengthDelimitedCodec::builder(),
|
||||
ghost: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// A [`TcpListener`] that wraps connections in [transports](Transport).
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct Incoming<Item, SinkItem, Codec, CodecFn> {
|
||||
listener: TcpListener,
|
||||
local_addr: SocketAddr,
|
||||
codec_fn: CodecFn,
|
||||
config: length_delimited::Builder,
|
||||
ghost: PhantomData<(fn() -> Item, fn(SinkItem), Codec)>,
|
||||
}
|
||||
|
||||
impl<Item, SinkItem, Codec, CodecFn> Incoming<Item, SinkItem, Codec, CodecFn> {
|
||||
/// Returns the address being listened on.
|
||||
pub fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
|
||||
/// Returns an immutable reference to the length-delimited codec's config.
|
||||
pub fn config(&self) -> &length_delimited::Builder {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the length-delimited codec's config.
|
||||
pub fn config_mut(&mut self) -> &mut length_delimited::Builder {
|
||||
&mut self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, SinkItem, Codec, CodecFn> Stream for Incoming<Item, SinkItem, Codec, CodecFn>
|
||||
where
|
||||
Item: for<'de> Deserialize<'de>,
|
||||
SinkItem: Serialize,
|
||||
Codec: Serializer<SinkItem> + Deserializer<Item>,
|
||||
CodecFn: Fn() -> Codec,
|
||||
{
|
||||
type Item = io::Result<Transport<TcpStream, Item, SinkItem, Codec>>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let conn: TcpStream =
|
||||
ready!(Pin::new(&mut self.as_mut().project().listener).poll_accept(cx)?).0;
|
||||
Poll::Ready(Some(Ok(new(
|
||||
self.config.new_framed(conn),
|
||||
(self.codec_fn)(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Transport;
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{task::*, Sink, Stream};
|
||||
use pin_utils::pin_mut;
|
||||
use std::{
|
||||
io::{self, Cursor},
|
||||
pin::Pin,
|
||||
};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio_serde::formats::SymmetricalJson;
|
||||
|
||||
fn ctx() -> Context<'static> {
|
||||
Context::from_waker(&noop_waker_ref())
|
||||
}
|
||||
|
||||
struct TestIo(Cursor<Vec<u8>>);
|
||||
|
||||
impl AsyncRead for TestIo {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> Poll<io::Result<()>> {
|
||||
AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for TestIo {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<io::Result<usize>> {
|
||||
AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_flush(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
|
||||
AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn close() {
|
||||
let (tx, _rx) = crate::transport::channel::bounded::<(), ()>(0);
|
||||
pin_mut!(tx);
|
||||
assert_matches!(tx.as_mut().poll_close(&mut ctx()), Poll::Ready(Ok(())));
|
||||
assert_matches!(tx.as_mut().start_send(()), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stream() {
|
||||
let data: &[u8] = b"\x00\x00\x00\x18\"Test one, check check.\"";
|
||||
let transport = Transport::from((
|
||||
TestIo(Cursor::new(Vec::from(data))),
|
||||
SymmetricalJson::<String>::default(),
|
||||
));
|
||||
pin_mut!(transport);
|
||||
|
||||
assert_matches!(
|
||||
transport.as_mut().poll_next(&mut ctx()),
|
||||
Poll::Ready(Some(Ok(ref s))) if s == "Test one, check check.");
|
||||
assert_matches!(transport.as_mut().poll_next(&mut ctx()), Poll::Ready(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sink() {
|
||||
let writer = Cursor::new(vec![]);
|
||||
let mut transport = Box::pin(Transport::from((
|
||||
TestIo(writer),
|
||||
SymmetricalJson::<String>::default(),
|
||||
)));
|
||||
|
||||
assert_matches!(
|
||||
transport.as_mut().poll_ready(&mut ctx()),
|
||||
Poll::Ready(Ok(()))
|
||||
);
|
||||
assert_matches!(
|
||||
transport
|
||||
.as_mut()
|
||||
.start_send("Test one, check check.".into()),
|
||||
Ok(())
|
||||
);
|
||||
assert_matches!(
|
||||
transport.as_mut().poll_flush(&mut ctx()),
|
||||
Poll::Ready(Ok(()))
|
||||
);
|
||||
assert_eq!(
|
||||
transport.get_ref().0.get_ref(),
|
||||
b"\x00\x00\x00\x18\"Test one, check check.\""
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(tcp)]
|
||||
#[tokio::test]
|
||||
async fn tcp() -> io::Result<()> {
|
||||
use super::tcp;
|
||||
|
||||
let mut listener = tcp::listen("0.0.0.0:0", SymmetricalJson::<String>::default).await?;
|
||||
let addr = listener.local_addr();
|
||||
tokio::spawn(async move {
|
||||
let mut transport = listener.next().await.unwrap().unwrap();
|
||||
let message = transport.next().await.unwrap().unwrap();
|
||||
transport.send(message).await.unwrap();
|
||||
});
|
||||
let mut transport = tcp::connect(addr, SymmetricalJson::<String>::default).await?;
|
||||
transport.send(String::from("test")).await?;
|
||||
assert_matches!(transport.next().await, Some(Ok(s)) if s == "test");
|
||||
assert_matches!(transport.next().await, None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
1053
tarpc/src/server.rs
Normal file
1053
tarpc/src/server.rs
Normal file
File diff suppressed because it is too large
Load Diff
223
tarpc/src/server/in_flight_requests.rs
Normal file
223
tarpc/src/server/in_flight_requests.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use crate::util::{Compact, TimeUntil};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::future::{AbortHandle, AbortRegistration};
|
||||
use std::{
|
||||
collections::hash_map,
|
||||
task::{Context, Poll},
|
||||
time::SystemTime,
|
||||
};
|
||||
use tokio_util::time::delay_queue::{self, DelayQueue};
|
||||
use tracing::Span;
|
||||
|
||||
/// A data structure that tracks in-flight requests. It aborts requests,
|
||||
/// either on demand or when a request deadline expires.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InFlightRequests {
|
||||
request_data: FnvHashMap<u64, RequestData>,
|
||||
deadlines: DelayQueue<u64>,
|
||||
}
|
||||
|
||||
/// Data needed to clean up a single in-flight request.
|
||||
#[derive(Debug)]
|
||||
struct RequestData {
|
||||
/// Aborts the response handler for the associated request.
|
||||
abort_handle: AbortHandle,
|
||||
/// The key to remove the timer for the request's deadline.
|
||||
deadline_key: delay_queue::Key,
|
||||
/// The client span.
|
||||
span: Span,
|
||||
}
|
||||
|
||||
/// An error returned when a request attempted to start with the same ID as a request already
|
||||
/// in flight.
|
||||
#[derive(Debug)]
|
||||
pub struct AlreadyExistsError;
|
||||
|
||||
impl InFlightRequests {
|
||||
/// Returns the number of in-flight requests.
|
||||
pub fn len(&self) -> usize {
|
||||
self.request_data.len()
|
||||
}
|
||||
|
||||
/// Starts a request, unless a request with the same ID is already in flight.
|
||||
pub fn start_request(
|
||||
&mut self,
|
||||
request_id: u64,
|
||||
deadline: SystemTime,
|
||||
span: Span,
|
||||
) -> Result<AbortRegistration, AlreadyExistsError> {
|
||||
match self.request_data.entry(request_id) {
|
||||
hash_map::Entry::Vacant(vacant) => {
|
||||
let timeout = deadline.time_until();
|
||||
let (abort_handle, abort_registration) = AbortHandle::new_pair();
|
||||
let deadline_key = self.deadlines.insert(request_id, timeout);
|
||||
vacant.insert(RequestData {
|
||||
abort_handle,
|
||||
deadline_key,
|
||||
span,
|
||||
});
|
||||
Ok(abort_registration)
|
||||
}
|
||||
hash_map::Entry::Occupied(_) => Err(AlreadyExistsError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels an in-flight request. Returns true iff the request was found.
|
||||
pub fn cancel_request(&mut self, request_id: u64) -> bool {
|
||||
if let Some(RequestData {
|
||||
span,
|
||||
abort_handle,
|
||||
deadline_key,
|
||||
}) = self.request_data.remove(&request_id)
|
||||
{
|
||||
let _entered = span.enter();
|
||||
self.request_data.compact(0.1);
|
||||
abort_handle.abort();
|
||||
self.deadlines.remove(&deadline_key);
|
||||
tracing::info!("ReceiveCancel");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a request without aborting. Returns true iff the request was found.
|
||||
/// This method should be used when a response is being sent.
|
||||
pub fn remove_request(&mut self, request_id: u64) -> Option<Span> {
|
||||
if let Some(request_data) = self.request_data.remove(&request_id) {
|
||||
self.request_data.compact(0.1);
|
||||
self.deadlines.remove(&request_data.deadline_key);
|
||||
Some(request_data.span)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields a request that has expired, aborting any ongoing processing of that request.
|
||||
pub fn poll_expired(
|
||||
&mut self,
|
||||
cx: &mut Context,
|
||||
) -> Poll<Option<Result<u64, tokio::time::error::Error>>> {
|
||||
if self.deadlines.is_empty() {
|
||||
// TODO(https://github.com/tokio-rs/tokio/issues/4161)
|
||||
// This is a workaround for DelayQueue not always treating this case correctly.
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
self.deadlines.poll_expired(cx).map_ok(|expired| {
|
||||
if let Some(RequestData {
|
||||
abort_handle, span, ..
|
||||
}) = self.request_data.remove(expired.get_ref())
|
||||
{
|
||||
let _entered = span.enter();
|
||||
self.request_data.compact(0.1);
|
||||
abort_handle.abort();
|
||||
tracing::error!("DeadlineExceeded");
|
||||
}
|
||||
expired.into_inner()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// When InFlightRequests is dropped, any outstanding requests are aborted.
|
||||
impl Drop for InFlightRequests {
|
||||
fn drop(&mut self) {
|
||||
self.request_data
|
||||
.values()
|
||||
.for_each(|request_data| request_data.abort_handle.abort())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{
|
||||
future::{pending, Abortable},
|
||||
FutureExt,
|
||||
};
|
||||
use futures_test::task::noop_context;
|
||||
|
||||
#[tokio::test]
|
||||
async fn start_request_increases_len() {
|
||||
let mut in_flight_requests = InFlightRequests::default();
|
||||
assert_eq!(in_flight_requests.len(), 0);
|
||||
in_flight_requests
|
||||
.start_request(0, SystemTime::now(), Span::current())
|
||||
.unwrap();
|
||||
assert_eq!(in_flight_requests.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn polling_expired_aborts() {
|
||||
let mut in_flight_requests = InFlightRequests::default();
|
||||
let abort_registration = in_flight_requests
|
||||
.start_request(0, SystemTime::now(), Span::current())
|
||||
.unwrap();
|
||||
let mut abortable_future = Box::new(Abortable::new(pending::<()>(), abort_registration));
|
||||
|
||||
tokio::time::pause();
|
||||
tokio::time::advance(std::time::Duration::from_secs(1000)).await;
|
||||
|
||||
assert_matches!(
|
||||
in_flight_requests.poll_expired(&mut noop_context()),
|
||||
Poll::Ready(Some(Ok(_)))
|
||||
);
|
||||
assert_matches!(
|
||||
abortable_future.poll_unpin(&mut noop_context()),
|
||||
Poll::Ready(Err(_))
|
||||
);
|
||||
assert_eq!(in_flight_requests.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cancel_request_aborts() {
|
||||
let mut in_flight_requests = InFlightRequests::default();
|
||||
let abort_registration = in_flight_requests
|
||||
.start_request(0, SystemTime::now(), Span::current())
|
||||
.unwrap();
|
||||
let mut abortable_future = Box::new(Abortable::new(pending::<()>(), abort_registration));
|
||||
|
||||
assert_eq!(in_flight_requests.cancel_request(0), true);
|
||||
assert_matches!(
|
||||
abortable_future.poll_unpin(&mut noop_context()),
|
||||
Poll::Ready(Err(_))
|
||||
);
|
||||
assert_eq!(in_flight_requests.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_request_doesnt_abort() {
|
||||
let mut in_flight_requests = InFlightRequests::default();
|
||||
assert!(in_flight_requests.deadlines.is_empty());
|
||||
|
||||
let abort_registration = in_flight_requests
|
||||
.start_request(
|
||||
0,
|
||||
SystemTime::now() + std::time::Duration::from_secs(10),
|
||||
Span::current(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut abortable_future = Box::new(Abortable::new(pending::<()>(), abort_registration));
|
||||
|
||||
// Precondition: Pending expiration
|
||||
assert_matches!(
|
||||
in_flight_requests.poll_expired(&mut noop_context()),
|
||||
Poll::Pending
|
||||
);
|
||||
assert!(!in_flight_requests.deadlines.is_empty());
|
||||
|
||||
assert_matches!(in_flight_requests.remove_request(0), Some(_));
|
||||
// Postcondition: No pending expirations
|
||||
assert!(in_flight_requests.deadlines.is_empty());
|
||||
assert_matches!(
|
||||
in_flight_requests.poll_expired(&mut noop_context()),
|
||||
Poll::Ready(None)
|
||||
);
|
||||
assert_matches!(
|
||||
abortable_future.poll_unpin(&mut noop_context()),
|
||||
Poll::Pending
|
||||
);
|
||||
assert_eq!(in_flight_requests.len(), 0);
|
||||
}
|
||||
}
|
||||
49
tarpc/src/server/incoming.rs
Normal file
49
tarpc/src/server/incoming.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use super::{
|
||||
limits::{channels_per_key::MaxChannelsPerKey, requests_per_channel::MaxRequestsPerChannel},
|
||||
Channel,
|
||||
};
|
||||
use futures::prelude::*;
|
||||
use std::{fmt, hash::Hash};
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
use super::{tokio::TokioServerExecutor, Serve};
|
||||
|
||||
/// An extension trait for [streams](futures::prelude::Stream) of [`Channels`](Channel).
|
||||
pub trait Incoming<C>
|
||||
where
|
||||
Self: Sized + Stream<Item = C>,
|
||||
C: Channel,
|
||||
{
|
||||
/// Enforces channel per-key limits.
|
||||
fn max_channels_per_key<K, KF>(self, n: u32, keymaker: KF) -> MaxChannelsPerKey<Self, K, KF>
|
||||
where
|
||||
K: fmt::Display + Eq + Hash + Clone + Unpin,
|
||||
KF: Fn(&C) -> K,
|
||||
{
|
||||
MaxChannelsPerKey::new(self, n, keymaker)
|
||||
}
|
||||
|
||||
/// Caps the number of concurrent requests per channel.
|
||||
fn max_concurrent_requests_per_channel(self, n: usize) -> MaxRequestsPerChannel<Self> {
|
||||
MaxRequestsPerChannel::new(self, n)
|
||||
}
|
||||
|
||||
/// [Executes](Channel::execute) each incoming channel. Each channel will be handled
|
||||
/// concurrently by spawning on tokio's default executor, and each request will be also
|
||||
/// be spawned on tokio's default executor.
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
fn execute<S>(self, serve: S) -> TokioServerExecutor<Self, S>
|
||||
where
|
||||
S: Serve<C::Req, Resp = C::Resp>,
|
||||
{
|
||||
TokioServerExecutor::new(self, serve)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, C> Incoming<C> for S
|
||||
where
|
||||
S: Sized + Stream<Item = C>,
|
||||
C: Channel,
|
||||
{
|
||||
}
|
||||
5
tarpc/src/server/limits.rs
Normal file
5
tarpc/src/server/limits.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
/// Provides functionality to limit the number of active channels.
|
||||
pub mod channels_per_key;
|
||||
|
||||
/// Provides a [channel](crate::server::Channel) that limits the number of in-flight requests.
|
||||
pub mod requests_per_channel;
|
||||
480
tarpc/src/server/limits/channels_per_key.rs
Normal file
480
tarpc/src/server/limits/channels_per_key.rs
Normal file
@@ -0,0 +1,480 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use crate::{
|
||||
server::{self, Channel},
|
||||
util::Compact,
|
||||
};
|
||||
use fnv::FnvHashMap;
|
||||
use futures::{prelude::*, ready, stream::Fuse, task::*};
|
||||
use pin_project::pin_project;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::{
|
||||
collections::hash_map::Entry, convert::TryFrom, fmt, hash::Hash, marker::Unpin, pin::Pin,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{debug, info, trace};
|
||||
|
||||
/// An [`Incoming`](crate::server::incoming::Incoming) stream that drops new channels based on
|
||||
/// per-key limits.
|
||||
///
|
||||
/// The decision to drop a Channel is made once at the time the Channel materializes. Once a
|
||||
/// Channel is yielded, it will not be prematurely dropped.
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct MaxChannelsPerKey<S, K, F>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
#[pin]
|
||||
listener: Fuse<S>,
|
||||
channels_per_key: u32,
|
||||
dropped_keys: mpsc::UnboundedReceiver<K>,
|
||||
dropped_keys_tx: mpsc::UnboundedSender<K>,
|
||||
key_counts: FnvHashMap<K, Weak<Tracker<K>>>,
|
||||
keymaker: F,
|
||||
}
|
||||
|
||||
/// A channel that is tracked by [`MaxChannelsPerKey`].
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct TrackedChannel<C, K> {
|
||||
#[pin]
|
||||
inner: C,
|
||||
tracker: Arc<Tracker<K>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Tracker<K> {
|
||||
key: Option<K>,
|
||||
dropped_keys: mpsc::UnboundedSender<K>,
|
||||
}
|
||||
|
||||
impl<K> Drop for Tracker<K> {
|
||||
fn drop(&mut self) {
|
||||
// Don't care if the listener is dropped.
|
||||
let _ = self.dropped_keys.send(self.key.take().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, K> Stream for TrackedChannel<C, K>
|
||||
where
|
||||
C: Stream,
|
||||
{
|
||||
type Item = <C as Stream>::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
self.inner_pin_mut().poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, I, K> Sink<I> for TrackedChannel<C, K>
|
||||
where
|
||||
C: Sink<I>,
|
||||
{
|
||||
type Error = C::Error;
|
||||
|
||||
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner_pin_mut().poll_ready(cx)
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, item: I) -> Result<(), Self::Error> {
|
||||
self.inner_pin_mut().start_send(item)
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner_pin_mut().poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner_pin_mut().poll_close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, K> AsRef<C> for TrackedChannel<C, K> {
|
||||
fn as_ref(&self) -> &C {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, K> Channel for TrackedChannel<C, K>
|
||||
where
|
||||
C: Channel,
|
||||
{
|
||||
type Req = C::Req;
|
||||
type Resp = C::Resp;
|
||||
type Transport = C::Transport;
|
||||
|
||||
fn config(&self) -> &server::Config {
|
||||
self.inner.config()
|
||||
}
|
||||
|
||||
fn in_flight_requests(&self) -> usize {
|
||||
self.inner.in_flight_requests()
|
||||
}
|
||||
|
||||
fn transport(&self) -> &Self::Transport {
|
||||
self.inner.transport()
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, K> TrackedChannel<C, K> {
|
||||
/// Returns the inner channel.
|
||||
pub fn get_ref(&self) -> &C {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Returns the pinned inner channel.
|
||||
fn inner_pin_mut<'a>(self: &'a mut Pin<&mut Self>) -> Pin<&'a mut C> {
|
||||
self.as_mut().project().inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, K, F> MaxChannelsPerKey<S, K, F>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
S: Stream,
|
||||
F: Fn(&S::Item) -> K,
|
||||
{
|
||||
/// Sheds new channels to stay under configured limits.
|
||||
pub(crate) fn new(listener: S, channels_per_key: u32, keymaker: F) -> Self {
|
||||
let (dropped_keys_tx, dropped_keys) = mpsc::unbounded_channel();
|
||||
MaxChannelsPerKey {
|
||||
listener: listener.fuse(),
|
||||
channels_per_key,
|
||||
dropped_keys,
|
||||
dropped_keys_tx,
|
||||
key_counts: FnvHashMap::default(),
|
||||
keymaker,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, K, F> MaxChannelsPerKey<S, K, F>
|
||||
where
|
||||
S: Stream,
|
||||
K: fmt::Display + Eq + Hash + Clone + Unpin,
|
||||
F: Fn(&S::Item) -> K,
|
||||
{
|
||||
fn listener_pin_mut<'a>(self: &'a mut Pin<&mut Self>) -> Pin<&'a mut Fuse<S>> {
|
||||
self.as_mut().project().listener
|
||||
}
|
||||
|
||||
fn handle_new_channel(
|
||||
mut self: Pin<&mut Self>,
|
||||
stream: S::Item,
|
||||
) -> Result<TrackedChannel<S::Item, K>, K> {
|
||||
let key = (self.as_mut().keymaker)(&stream);
|
||||
let tracker = self.as_mut().increment_channels_for_key(key.clone())?;
|
||||
|
||||
trace!(
|
||||
channel_filter_key = %key,
|
||||
open_channels = Arc::strong_count(&tracker),
|
||||
max_open_channels = self.channels_per_key,
|
||||
"Opening channel");
|
||||
|
||||
Ok(TrackedChannel {
|
||||
tracker,
|
||||
inner: stream,
|
||||
})
|
||||
}
|
||||
|
||||
fn increment_channels_for_key(self: Pin<&mut Self>, key: K) -> Result<Arc<Tracker<K>>, K> {
|
||||
let self_ = self.project();
|
||||
let dropped_keys = self_.dropped_keys_tx;
|
||||
match self_.key_counts.entry(key.clone()) {
|
||||
Entry::Vacant(vacant) => {
|
||||
let tracker = Arc::new(Tracker {
|
||||
key: Some(key),
|
||||
dropped_keys: dropped_keys.clone(),
|
||||
});
|
||||
|
||||
vacant.insert(Arc::downgrade(&tracker));
|
||||
Ok(tracker)
|
||||
}
|
||||
Entry::Occupied(mut o) => {
|
||||
let count = o.get().strong_count();
|
||||
if count >= TryFrom::try_from(*self_.channels_per_key).unwrap() {
|
||||
info!(
|
||||
channel_filter_key = %key,
|
||||
open_channels = count,
|
||||
max_open_channels = *self_.channels_per_key,
|
||||
"At open channel limit");
|
||||
Err(key)
|
||||
} else {
|
||||
Ok(o.get().upgrade().unwrap_or_else(|| {
|
||||
let tracker = Arc::new(Tracker {
|
||||
key: Some(key),
|
||||
dropped_keys: dropped_keys.clone(),
|
||||
});
|
||||
|
||||
*o.get_mut() = Arc::downgrade(&tracker);
|
||||
tracker
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_listener(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<TrackedChannel<S::Item, K>, K>>> {
|
||||
match ready!(self.listener_pin_mut().poll_next_unpin(cx)) {
|
||||
Some(codec) => Poll::Ready(Some(self.handle_new_channel(codec))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_closed_channels(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
let self_ = self.project();
|
||||
match ready!(self_.dropped_keys.poll_recv(cx)) {
|
||||
Some(key) => {
|
||||
debug!(
|
||||
channel_filter_key = %key,
|
||||
"All channels dropped");
|
||||
self_.key_counts.remove(&key);
|
||||
self_.key_counts.compact(0.1);
|
||||
Poll::Ready(())
|
||||
}
|
||||
None => unreachable!("Holding a copy of closed_channels and didn't close it."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, K, F> Stream for MaxChannelsPerKey<S, K, F>
|
||||
where
|
||||
S: Stream,
|
||||
K: fmt::Display + Eq + Hash + Clone + Unpin,
|
||||
F: Fn(&S::Item) -> K,
|
||||
{
|
||||
type Item = TrackedChannel<S::Item, K>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<TrackedChannel<S::Item, K>>> {
|
||||
loop {
|
||||
match (
|
||||
self.as_mut().poll_listener(cx),
|
||||
self.as_mut().poll_closed_channels(cx),
|
||||
) {
|
||||
(Poll::Ready(Some(Ok(channel))), _) => {
|
||||
return Poll::Ready(Some(channel));
|
||||
}
|
||||
(Poll::Ready(Some(Err(_))), _) => {
|
||||
continue;
|
||||
}
|
||||
(_, Poll::Ready(())) => continue,
|
||||
(Poll::Pending, Poll::Pending) => return Poll::Pending,
|
||||
(Poll::Ready(None), Poll::Pending) => {
|
||||
trace!("Shutting down listener.");
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
fn ctx() -> Context<'static> {
|
||||
use futures::task::*;
|
||||
|
||||
Context::from_waker(&noop_waker_ref())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracker_drop() {
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
Tracker {
|
||||
key: Some(1),
|
||||
dropped_keys: tx,
|
||||
};
|
||||
assert_matches!(rx.poll_recv(&mut ctx()), Poll::Ready(Some(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracked_channel_stream() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
let (chan_tx, chan) = futures::channel::mpsc::unbounded();
|
||||
let (dropped_keys, _) = mpsc::unbounded_channel();
|
||||
let channel = TrackedChannel {
|
||||
inner: chan,
|
||||
tracker: Arc::new(Tracker {
|
||||
key: Some(1),
|
||||
dropped_keys,
|
||||
}),
|
||||
};
|
||||
|
||||
chan_tx.unbounded_send("test").unwrap();
|
||||
pin_mut!(channel);
|
||||
assert_matches!(channel.poll_next(&mut ctx()), Poll::Ready(Some("test")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracked_channel_sink() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
let (chan, mut chan_rx) = futures::channel::mpsc::unbounded();
|
||||
let (dropped_keys, _) = mpsc::unbounded_channel();
|
||||
let channel = TrackedChannel {
|
||||
inner: chan,
|
||||
tracker: Arc::new(Tracker {
|
||||
key: Some(1),
|
||||
dropped_keys,
|
||||
}),
|
||||
};
|
||||
|
||||
pin_mut!(channel);
|
||||
assert_matches!(channel.as_mut().poll_ready(&mut ctx()), Poll::Ready(Ok(())));
|
||||
assert_matches!(channel.as_mut().start_send("test"), Ok(()));
|
||||
assert_matches!(channel.as_mut().poll_flush(&mut ctx()), Poll::Ready(Ok(())));
|
||||
assert_matches!(chan_rx.try_next(), Ok(Some("test")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_filter_increment_channels_for_key() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
struct TestChannel {
|
||||
key: &'static str,
|
||||
}
|
||||
let (_, listener) = futures::channel::mpsc::unbounded();
|
||||
let filter = MaxChannelsPerKey::new(listener, 2, |chan: &TestChannel| chan.key);
|
||||
pin_mut!(filter);
|
||||
let tracker1 = filter.as_mut().increment_channels_for_key("key").unwrap();
|
||||
assert_eq!(Arc::strong_count(&tracker1), 1);
|
||||
let tracker2 = filter.as_mut().increment_channels_for_key("key").unwrap();
|
||||
assert_eq!(Arc::strong_count(&tracker1), 2);
|
||||
assert_matches!(filter.increment_channels_for_key("key"), Err("key"));
|
||||
drop(tracker2);
|
||||
assert_eq!(Arc::strong_count(&tracker1), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_filter_handle_new_channel() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestChannel {
|
||||
key: &'static str,
|
||||
}
|
||||
let (_, listener) = futures::channel::mpsc::unbounded();
|
||||
let filter = MaxChannelsPerKey::new(listener, 2, |chan: &TestChannel| chan.key);
|
||||
pin_mut!(filter);
|
||||
let channel1 = filter
|
||||
.as_mut()
|
||||
.handle_new_channel(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
|
||||
|
||||
let channel2 = filter
|
||||
.as_mut()
|
||||
.handle_new_channel(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
|
||||
|
||||
assert_matches!(
|
||||
filter.handle_new_channel(TestChannel { key: "key" }),
|
||||
Err("key")
|
||||
);
|
||||
drop(channel2);
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_filter_poll_listener() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestChannel {
|
||||
key: &'static str,
|
||||
}
|
||||
let (new_channels, listener) = futures::channel::mpsc::unbounded();
|
||||
let filter = MaxChannelsPerKey::new(listener, 2, |chan: &TestChannel| chan.key);
|
||||
pin_mut!(filter);
|
||||
|
||||
new_channels
|
||||
.unbounded_send(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
let channel1 =
|
||||
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 1);
|
||||
|
||||
new_channels
|
||||
.unbounded_send(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
let _channel2 =
|
||||
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
|
||||
|
||||
new_channels
|
||||
.unbounded_send(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
let key =
|
||||
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Err(k))) => k);
|
||||
assert_eq!(key, "key");
|
||||
assert_eq!(Arc::strong_count(&channel1.tracker), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_filter_poll_closed_channels() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestChannel {
|
||||
key: &'static str,
|
||||
}
|
||||
let (new_channels, listener) = futures::channel::mpsc::unbounded();
|
||||
let filter = MaxChannelsPerKey::new(listener, 2, |chan: &TestChannel| chan.key);
|
||||
pin_mut!(filter);
|
||||
|
||||
new_channels
|
||||
.unbounded_send(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
let channel =
|
||||
assert_matches!(filter.as_mut().poll_listener(&mut ctx()), Poll::Ready(Some(Ok(c))) => c);
|
||||
assert_eq!(filter.key_counts.len(), 1);
|
||||
|
||||
drop(channel);
|
||||
assert_matches!(
|
||||
filter.as_mut().poll_closed_channels(&mut ctx()),
|
||||
Poll::Ready(())
|
||||
);
|
||||
assert!(filter.key_counts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_filter_stream() {
|
||||
use assert_matches::assert_matches;
|
||||
use pin_utils::pin_mut;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestChannel {
|
||||
key: &'static str,
|
||||
}
|
||||
let (new_channels, listener) = futures::channel::mpsc::unbounded();
|
||||
let filter = MaxChannelsPerKey::new(listener, 2, |chan: &TestChannel| chan.key);
|
||||
pin_mut!(filter);
|
||||
|
||||
new_channels
|
||||
.unbounded_send(TestChannel { key: "key" })
|
||||
.unwrap();
|
||||
let channel = assert_matches!(filter.as_mut().poll_next(&mut ctx()), Poll::Ready(Some(c)) => c);
|
||||
assert_eq!(filter.key_counts.len(), 1);
|
||||
|
||||
drop(channel);
|
||||
assert_matches!(filter.as_mut().poll_next(&mut ctx()), Poll::Pending);
|
||||
assert!(filter.key_counts.is_empty());
|
||||
}
|
||||
349
tarpc/src/server/limits/requests_per_channel.rs
Normal file
349
tarpc/src/server/limits/requests_per_channel.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use crate::{
|
||||
server::{Channel, Config},
|
||||
Response, ServerError,
|
||||
};
|
||||
use futures::{prelude::*, ready, task::*};
|
||||
use pin_project::pin_project;
|
||||
use std::{io, pin::Pin};
|
||||
|
||||
/// A [`Channel`] that limits the number of concurrent requests by throttling.
|
||||
///
|
||||
/// Note that this is a very basic throttling heuristic. It is easy to set a number that is too low
|
||||
/// for the resources available to the server. For production use cases, a more advanced throttler
|
||||
/// is likely needed.
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct MaxRequests<C> {
|
||||
max_in_flight_requests: usize,
|
||||
#[pin]
|
||||
inner: C,
|
||||
}
|
||||
|
||||
impl<C> MaxRequests<C> {
|
||||
/// Returns the inner channel.
|
||||
pub fn get_ref(&self) -> &C {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> MaxRequests<C>
|
||||
where
|
||||
C: Channel,
|
||||
{
|
||||
/// Returns a new `MaxRequests` that wraps the given channel and limits concurrent requests to
|
||||
/// `max_in_flight_requests`.
|
||||
pub fn new(inner: C, max_in_flight_requests: usize) -> Self {
|
||||
MaxRequests {
|
||||
max_in_flight_requests,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Stream for MaxRequests<C>
|
||||
where
|
||||
C: Channel,
|
||||
{
|
||||
type Item = <C as Stream>::Item;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
while self.as_mut().in_flight_requests() >= *self.as_mut().project().max_in_flight_requests
|
||||
{
|
||||
ready!(self.as_mut().project().inner.poll_ready(cx)?);
|
||||
|
||||
match ready!(self.as_mut().project().inner.poll_next(cx)?) {
|
||||
Some(r) => {
|
||||
let _entered = r.span.enter();
|
||||
tracing::info!(
|
||||
in_flight_requests = self.as_mut().in_flight_requests(),
|
||||
"ThrottleRequest",
|
||||
);
|
||||
|
||||
self.as_mut().start_send(Response {
|
||||
request_id: r.request.id,
|
||||
message: Err(ServerError {
|
||||
kind: io::ErrorKind::WouldBlock,
|
||||
detail: "server throttled the request.".into(),
|
||||
}),
|
||||
})?;
|
||||
}
|
||||
None => return Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
self.project().inner.poll_next(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Sink<Response<<C as Channel>::Resp>> for MaxRequests<C>
|
||||
where
|
||||
C: Channel,
|
||||
{
|
||||
type Error = C::Error;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn start_send(
|
||||
self: Pin<&mut Self>,
|
||||
item: Response<<C as Channel>::Resp>,
|
||||
) -> Result<(), Self::Error> {
|
||||
self.project().inner.start_send(item)
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().inner.poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().inner.poll_close(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsRef<C> for MaxRequests<C> {
|
||||
fn as_ref(&self) -> &C {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Channel for MaxRequests<C>
|
||||
where
|
||||
C: Channel,
|
||||
{
|
||||
type Req = <C as Channel>::Req;
|
||||
type Resp = <C as Channel>::Resp;
|
||||
type Transport = <C as Channel>::Transport;
|
||||
|
||||
fn in_flight_requests(&self) -> usize {
|
||||
self.inner.in_flight_requests()
|
||||
}
|
||||
|
||||
fn config(&self) -> &Config {
|
||||
self.inner.config()
|
||||
}
|
||||
|
||||
fn transport(&self) -> &Self::Transport {
|
||||
self.inner.transport()
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Incoming`](crate::server::incoming::Incoming) stream of channels that enforce limits on
|
||||
/// the number of in-flight requests.
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct MaxRequestsPerChannel<S> {
|
||||
#[pin]
|
||||
inner: S,
|
||||
max_in_flight_requests: usize,
|
||||
}
|
||||
|
||||
impl<S> MaxRequestsPerChannel<S>
|
||||
where
|
||||
S: Stream,
|
||||
<S as Stream>::Item: Channel,
|
||||
{
|
||||
pub(crate) fn new(inner: S, max_in_flight_requests: usize) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
max_in_flight_requests,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Stream for MaxRequestsPerChannel<S>
|
||||
where
|
||||
S: Stream,
|
||||
<S as Stream>::Item: Channel,
|
||||
{
|
||||
type Item = MaxRequests<<S as Stream>::Item>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
match ready!(self.as_mut().project().inner.poll_next(cx)) {
|
||||
Some(channel) => Poll::Ready(Some(MaxRequests::new(
|
||||
channel,
|
||||
*self.project().max_in_flight_requests,
|
||||
))),
|
||||
None => Poll::Ready(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::server::{
|
||||
testing::{self, FakeChannel, PollExt},
|
||||
TrackedRequest,
|
||||
};
|
||||
use pin_utils::pin_mut;
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
use tracing::Span;
|
||||
|
||||
#[tokio::test]
|
||||
async fn throttler_in_flight_requests() {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 0,
|
||||
inner: FakeChannel::default::<isize, isize>(),
|
||||
};
|
||||
|
||||
pin_mut!(throttler);
|
||||
for i in 0..5 {
|
||||
throttler
|
||||
.inner
|
||||
.in_flight_requests
|
||||
.start_request(
|
||||
i,
|
||||
SystemTime::now() + Duration::from_secs(1),
|
||||
Span::current(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(throttler.as_mut().in_flight_requests(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn throttler_poll_next_done() {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 0,
|
||||
inner: FakeChannel::default::<isize, isize>(),
|
||||
};
|
||||
|
||||
pin_mut!(throttler);
|
||||
assert!(throttler.as_mut().poll_next(&mut testing::cx()).is_done());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn throttler_poll_next_some() -> io::Result<()> {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 1,
|
||||
inner: FakeChannel::default::<isize, isize>(),
|
||||
};
|
||||
|
||||
pin_mut!(throttler);
|
||||
throttler.inner.push_req(0, 1);
|
||||
assert!(throttler.as_mut().poll_ready(&mut testing::cx()).is_ready());
|
||||
assert_eq!(
|
||||
throttler
|
||||
.as_mut()
|
||||
.poll_next(&mut testing::cx())?
|
||||
.map(|r| r.map(|r| (r.request.id, r.request.message))),
|
||||
Poll::Ready(Some((0, 1)))
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn throttler_poll_next_throttled() {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 0,
|
||||
inner: FakeChannel::default::<isize, isize>(),
|
||||
};
|
||||
|
||||
pin_mut!(throttler);
|
||||
throttler.inner.push_req(1, 1);
|
||||
assert!(throttler.as_mut().poll_next(&mut testing::cx()).is_done());
|
||||
assert_eq!(throttler.inner.sink.len(), 1);
|
||||
let resp = throttler.inner.sink.get(0).unwrap();
|
||||
assert_eq!(resp.request_id, 1);
|
||||
assert!(resp.message.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn throttler_poll_next_throttled_sink_not_ready() {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 0,
|
||||
inner: PendingSink::default::<isize, isize>(),
|
||||
};
|
||||
pin_mut!(throttler);
|
||||
assert!(throttler.poll_next(&mut testing::cx()).is_pending());
|
||||
|
||||
struct PendingSink<In, Out> {
|
||||
ghost: PhantomData<fn(Out) -> In>,
|
||||
}
|
||||
impl PendingSink<(), ()> {
|
||||
pub fn default<Req, Resp>(
|
||||
) -> PendingSink<io::Result<TrackedRequest<Req>>, Response<Resp>> {
|
||||
PendingSink { ghost: PhantomData }
|
||||
}
|
||||
}
|
||||
impl<In, Out> Stream for PendingSink<In, Out> {
|
||||
type Item = In;
|
||||
fn poll_next(self: Pin<&mut Self>, _: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
impl<In, Out> Sink<Out> for PendingSink<In, Out> {
|
||||
type Error = io::Error;
|
||||
fn poll_ready(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
fn start_send(self: Pin<&mut Self>, _: Out) -> Result<(), Self::Error> {
|
||||
Err(io::Error::from(io::ErrorKind::WouldBlock))
|
||||
}
|
||||
fn poll_flush(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
fn poll_close(self: Pin<&mut Self>, _: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
impl<Req, Resp> Channel for PendingSink<io::Result<TrackedRequest<Req>>, Response<Resp>> {
|
||||
type Req = Req;
|
||||
type Resp = Resp;
|
||||
type Transport = ();
|
||||
fn config(&self) -> &Config {
|
||||
unimplemented!()
|
||||
}
|
||||
fn in_flight_requests(&self) -> usize {
|
||||
0
|
||||
}
|
||||
fn transport(&self) -> &() {
|
||||
&()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn throttler_start_send() {
|
||||
let throttler = MaxRequests {
|
||||
max_in_flight_requests: 0,
|
||||
inner: FakeChannel::default::<isize, isize>(),
|
||||
};
|
||||
|
||||
pin_mut!(throttler);
|
||||
throttler
|
||||
.inner
|
||||
.in_flight_requests
|
||||
.start_request(
|
||||
0,
|
||||
SystemTime::now() + Duration::from_secs(1),
|
||||
Span::current(),
|
||||
)
|
||||
.unwrap();
|
||||
throttler
|
||||
.as_mut()
|
||||
.start_send(Response {
|
||||
request_id: 0,
|
||||
message: Ok(1),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(throttler.inner.in_flight_requests.len(), 0);
|
||||
assert_eq!(
|
||||
throttler.inner.sink.get(0),
|
||||
Some(&Response {
|
||||
request_id: 0,
|
||||
message: Ok(1),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
127
tarpc/src/server/testing.rs
Normal file
127
tarpc/src/server/testing.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use crate::{
|
||||
context,
|
||||
server::{Channel, Config, TrackedRequest},
|
||||
Request, Response,
|
||||
};
|
||||
use futures::{task::*, Sink, Stream};
|
||||
use pin_project::pin_project;
|
||||
use std::{collections::VecDeque, io, pin::Pin, time::SystemTime};
|
||||
use tracing::Span;
|
||||
|
||||
#[pin_project]
|
||||
pub(crate) struct FakeChannel<In, Out> {
|
||||
#[pin]
|
||||
pub stream: VecDeque<In>,
|
||||
#[pin]
|
||||
pub sink: VecDeque<Out>,
|
||||
pub config: Config,
|
||||
pub in_flight_requests: super::in_flight_requests::InFlightRequests,
|
||||
}
|
||||
|
||||
impl<In, Out> Stream for FakeChannel<In, Out>
|
||||
where
|
||||
In: Unpin,
|
||||
{
|
||||
type Item = In;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
Poll::Ready(self.project().stream.pop_front())
|
||||
}
|
||||
}
|
||||
|
||||
impl<In, Resp> Sink<Response<Resp>> for FakeChannel<In, Response<Resp>> {
|
||||
type Error = io::Error;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().sink.poll_ready(cx).map_err(|e| match e {})
|
||||
}
|
||||
|
||||
fn start_send(mut self: Pin<&mut Self>, response: Response<Resp>) -> Result<(), Self::Error> {
|
||||
self.as_mut()
|
||||
.project()
|
||||
.in_flight_requests
|
||||
.remove_request(response.request_id);
|
||||
self.project()
|
||||
.sink
|
||||
.start_send(response)
|
||||
.map_err(|e| match e {})
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().sink.poll_flush(cx).map_err(|e| match e {})
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), Self::Error>> {
|
||||
self.project().sink.poll_close(cx).map_err(|e| match e {})
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Resp> Channel for FakeChannel<io::Result<TrackedRequest<Req>>, Response<Resp>>
|
||||
where
|
||||
Req: Unpin,
|
||||
{
|
||||
type Req = Req;
|
||||
type Resp = Resp;
|
||||
type Transport = ();
|
||||
|
||||
fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
fn in_flight_requests(&self) -> usize {
|
||||
self.in_flight_requests.len()
|
||||
}
|
||||
|
||||
fn transport(&self) -> &() {
|
||||
&()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Req, Resp> FakeChannel<io::Result<TrackedRequest<Req>>, Response<Resp>> {
|
||||
pub fn push_req(&mut self, id: u64, message: Req) {
|
||||
let (_, abort_registration) = futures::future::AbortHandle::new_pair();
|
||||
self.stream.push_back(Ok(TrackedRequest {
|
||||
request: Request {
|
||||
context: context::Context {
|
||||
deadline: SystemTime::UNIX_EPOCH,
|
||||
trace_context: Default::default(),
|
||||
},
|
||||
id,
|
||||
message,
|
||||
},
|
||||
abort_registration,
|
||||
span: Span::none(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
impl FakeChannel<(), ()> {
|
||||
pub fn default<Req, Resp>() -> FakeChannel<io::Result<TrackedRequest<Req>>, Response<Resp>> {
|
||||
FakeChannel {
|
||||
stream: Default::default(),
|
||||
sink: Default::default(),
|
||||
config: Default::default(),
|
||||
in_flight_requests: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PollExt {
|
||||
fn is_done(&self) -> bool;
|
||||
}
|
||||
|
||||
impl<T> PollExt for Poll<Option<T>> {
|
||||
fn is_done(&self) -> bool {
|
||||
matches!(self, Poll::Ready(None))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cx() -> Context<'static> {
|
||||
Context::from_waker(&noop_waker_ref())
|
||||
}
|
||||
111
tarpc/src/server/tokio.rs
Normal file
111
tarpc/src/server/tokio.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use super::{Channel, Requests, Serve};
|
||||
use futures::{prelude::*, ready, task::*};
|
||||
use pin_project::pin_project;
|
||||
use std::pin::Pin;
|
||||
|
||||
/// A future that drives the server by [spawning](tokio::spawn) a [`TokioChannelExecutor`](TokioChannelExecutor)
|
||||
/// for each new channel. Returned by
|
||||
/// [`Incoming::execute`](crate::server::incoming::Incoming::execute).
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct TokioServerExecutor<T, S> {
|
||||
#[pin]
|
||||
inner: T,
|
||||
serve: S,
|
||||
}
|
||||
|
||||
impl<T, S> TokioServerExecutor<T, S> {
|
||||
pub(crate) fn new(inner: T, serve: S) -> Self {
|
||||
Self { inner, serve }
|
||||
}
|
||||
}
|
||||
|
||||
/// A future that drives the server by [spawning](tokio::spawn) each [response
|
||||
/// handler](super::InFlightRequest::execute) on tokio's default executor. Returned by
|
||||
/// [`Channel::execute`](crate::server::Channel::execute).
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct TokioChannelExecutor<T, S> {
|
||||
#[pin]
|
||||
inner: T,
|
||||
serve: S,
|
||||
}
|
||||
|
||||
impl<T, S> TokioServerExecutor<T, S> {
|
||||
fn inner_pin_mut<'a>(self: &'a mut Pin<&mut Self>) -> Pin<&'a mut T> {
|
||||
self.as_mut().project().inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> TokioChannelExecutor<T, S> {
|
||||
fn inner_pin_mut<'a>(self: &'a mut Pin<&mut Self>) -> Pin<&'a mut T> {
|
||||
self.as_mut().project().inner
|
||||
}
|
||||
}
|
||||
|
||||
// Send + 'static execution helper methods.
|
||||
|
||||
impl<C> Requests<C>
|
||||
where
|
||||
C: Channel,
|
||||
C::Req: Send + 'static,
|
||||
C::Resp: Send + 'static,
|
||||
{
|
||||
/// Executes all requests using the given service function. Requests are handled concurrently
|
||||
/// by [spawning](::tokio::spawn) each handler on tokio's default executor.
|
||||
pub fn execute<S>(self, serve: S) -> TokioChannelExecutor<Self, S>
|
||||
where
|
||||
S: Serve<C::Req, Resp = C::Resp> + Send + 'static,
|
||||
{
|
||||
TokioChannelExecutor { inner: self, serve }
|
||||
}
|
||||
}
|
||||
|
||||
impl<St, C, Se> Future for TokioServerExecutor<St, Se>
|
||||
where
|
||||
St: Sized + Stream<Item = C>,
|
||||
C: Channel + Send + 'static,
|
||||
C::Req: Send + 'static,
|
||||
C::Resp: Send + 'static,
|
||||
Se: Serve<C::Req, Resp = C::Resp> + Send + 'static + Clone,
|
||||
Se::Fut: Send,
|
||||
{
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
||||
while let Some(channel) = ready!(self.inner_pin_mut().poll_next(cx)) {
|
||||
tokio::spawn(channel.execute(self.serve.clone()));
|
||||
}
|
||||
tracing::info!("Server shutting down.");
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<C, S> Future for TokioChannelExecutor<Requests<C>, S>
|
||||
where
|
||||
C: Channel + 'static,
|
||||
C::Req: Send + 'static,
|
||||
C::Resp: Send + 'static,
|
||||
S: Serve<C::Req, Resp = C::Resp> + Send + 'static + Clone,
|
||||
S::Fut: Send,
|
||||
{
|
||||
type Output = ();
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
while let Some(response_handler) = ready!(self.inner_pin_mut().poll_next(cx)) {
|
||||
match response_handler {
|
||||
Ok(resp) => {
|
||||
let server = self.serve.clone();
|
||||
tokio::spawn(async move {
|
||||
resp.execute(server).await;
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Requests stream errored out: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
261
tarpc/src/trace.rs
Normal file
261
tarpc/src/trace.rs
Normal file
@@ -0,0 +1,261 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
#![deny(missing_docs, missing_debug_implementations)]
|
||||
|
||||
//! Provides building blocks for tracing distributed programs.
|
||||
//!
|
||||
//! A trace is logically a tree of causally-related events called spans. Traces are tracked via a
|
||||
//! [context](Context) that identifies the current trace, span, and parent of the current span. In
|
||||
//! distributed systems, a context can be sent from client to server to connect events occurring on
|
||||
//! either side.
|
||||
//!
|
||||
//! This crate's design is based on [opencensus
|
||||
//! tracing](https://opencensus.io/core-concepts/tracing/).
|
||||
|
||||
use opentelemetry::trace::TraceContextExt;
|
||||
use rand::Rng;
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Formatter},
|
||||
num::{NonZeroU128, NonZeroU64},
|
||||
};
|
||||
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
||||
|
||||
/// A context for tracing the execution of processes, distributed or otherwise.
|
||||
///
|
||||
/// Consists of a span identifying an event, an optional parent span identifying a causal event
|
||||
/// that triggered the current span, and a trace with which all related spans are associated.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Context {
|
||||
/// An identifier of the trace associated with the current context. A trace ID is typically
|
||||
/// created at a root span and passed along through all causal events.
|
||||
pub trace_id: TraceId,
|
||||
/// An identifier of the current span. In typical RPC usage, a span is created by a client
|
||||
/// before making an RPC, and the span ID is sent to the server. The server is free to create
|
||||
/// its own spans, for which it sets the client's span as the parent span.
|
||||
pub span_id: SpanId,
|
||||
/// Indicates whether a sampler has already decided whether or not to sample the trace
|
||||
/// associated with the Context. If `sampling_decision` is None, then a decision has not yet
|
||||
/// been made. Downstream samplers do not need to abide by "no sample" decisions--for example,
|
||||
/// an upstream client may choose to never sample, which may not make sense for the client's
|
||||
/// dependencies. On the other hand, if an upstream process has chosen to sample this trace,
|
||||
/// then the downstream samplers are expected to respect that decision and also sample the
|
||||
/// trace. Otherwise, the full trace would not be able to be reconstructed.
|
||||
pub sampling_decision: SamplingDecision,
|
||||
}
|
||||
|
||||
/// A 128-bit UUID identifying a trace. All spans caused by the same originating span share the
|
||||
/// same trace ID.
|
||||
#[derive(Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct TraceId(#[cfg_attr(feature = "serde1", serde(with = "u128_serde"))] u128);
|
||||
|
||||
/// A 64-bit identifier of a span within a trace. The identifier is unique within the span's trace.
|
||||
#[derive(Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SpanId(u64);
|
||||
|
||||
/// Indicates whether a sampler has decided whether or not to sample the trace associated with the
|
||||
/// Context. Downstream samplers do not need to abide by "no sample" decisions--for example, an
|
||||
/// upstream client may choose to never sample, which may not make sense for the client's
|
||||
/// dependencies. On the other hand, if an upstream process has chosen to sample this trace, then
|
||||
/// the downstream samplers are expected to respect that decision and also sample the trace.
|
||||
/// Otherwise, the full trace would not be able to be reconstructed reliably.
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||
#[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[repr(u8)]
|
||||
pub enum SamplingDecision {
|
||||
/// The associated span was sampled by its creating process. Child spans must also be sampled.
|
||||
Sampled,
|
||||
/// The associated span was not sampled by its creating process.
|
||||
Unsampled,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Constructs a new context with the trace ID and sampling decision inherited from the parent.
|
||||
pub(crate) fn new_child(&self) -> Self {
|
||||
Self {
|
||||
trace_id: self.trace_id,
|
||||
span_id: SpanId::random(&mut rand::thread_rng()),
|
||||
sampling_decision: self.sampling_decision,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TraceId {
|
||||
/// Returns a random trace ID that can be assumed to be globally unique if `rng` generates
|
||||
/// actually-random numbers.
|
||||
pub fn random<R: Rng>(rng: &mut R) -> Self {
|
||||
TraceId(rng.gen::<NonZeroU128>().get())
|
||||
}
|
||||
|
||||
/// Returns true iff the trace ID is 0.
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl SpanId {
|
||||
/// Returns a random span ID that can be assumed to be unique within a single trace.
|
||||
pub fn random<R: Rng>(rng: &mut R) -> Self {
|
||||
SpanId(rng.gen::<NonZeroU64>().get())
|
||||
}
|
||||
|
||||
/// Returns true iff the span ID is 0.
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.0 == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TraceId> for u128 {
|
||||
fn from(trace_id: TraceId) -> Self {
|
||||
trace_id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u128> for TraceId {
|
||||
fn from(trace_id: u128) -> Self {
|
||||
Self(trace_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpanId> for u64 {
|
||||
fn from(span_id: SpanId) -> Self {
|
||||
span_id.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for SpanId {
|
||||
fn from(span_id: u64) -> Self {
|
||||
Self(span_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<opentelemetry::trace::TraceId> for TraceId {
|
||||
fn from(trace_id: opentelemetry::trace::TraceId) -> Self {
|
||||
Self::from(u128::from_be_bytes(trace_id.to_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TraceId> for opentelemetry::trace::TraceId {
|
||||
fn from(trace_id: TraceId) -> Self {
|
||||
Self::from_bytes(u128::from(trace_id).to_be_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<opentelemetry::trace::SpanId> for SpanId {
|
||||
fn from(span_id: opentelemetry::trace::SpanId) -> Self {
|
||||
Self::from(u64::from_be_bytes(span_id.to_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SpanId> for opentelemetry::trace::SpanId {
|
||||
fn from(span_id: SpanId) -> Self {
|
||||
Self::from_bytes(u64::from(span_id).to_be_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&tracing::Span> for Context {
|
||||
type Error = NoActiveSpan;
|
||||
|
||||
fn try_from(span: &tracing::Span) -> Result<Self, NoActiveSpan> {
|
||||
let context = span.context();
|
||||
if context.has_active_span() {
|
||||
Ok(Self::from(context.span()))
|
||||
} else {
|
||||
Err(NoActiveSpan)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<opentelemetry::trace::SpanRef<'_>> for Context {
|
||||
fn from(span: opentelemetry::trace::SpanRef<'_>) -> Self {
|
||||
let otel_ctx = span.span_context();
|
||||
Self {
|
||||
trace_id: TraceId::from(otel_ctx.trace_id()),
|
||||
span_id: SpanId::from(otel_ctx.span_id()),
|
||||
sampling_decision: SamplingDecision::from(otel_ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SamplingDecision> for opentelemetry::trace::TraceFlags {
|
||||
fn from(decision: SamplingDecision) -> Self {
|
||||
match decision {
|
||||
SamplingDecision::Sampled => opentelemetry::trace::TraceFlags::SAMPLED,
|
||||
SamplingDecision::Unsampled => opentelemetry::trace::TraceFlags::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&opentelemetry::trace::SpanContext> for SamplingDecision {
|
||||
fn from(context: &opentelemetry::trace::SpanContext) -> Self {
|
||||
if context.is_sampled() {
|
||||
SamplingDecision::Sampled
|
||||
} else {
|
||||
SamplingDecision::Unsampled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SamplingDecision {
|
||||
fn default() -> Self {
|
||||
Self::Unsampled
|
||||
}
|
||||
}
|
||||
|
||||
/// Returned when a [`Context`] cannot be constructed from a [`Span`](tracing::Span).
|
||||
#[derive(Debug)]
|
||||
pub struct NoActiveSpan;
|
||||
|
||||
impl fmt::Display for TraceId {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:02x}", self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TraceId {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:02x}", self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SpanId {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:02x}", self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for SpanId {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:02x}", self.0)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde1")]
|
||||
mod u128_serde {
|
||||
pub fn serialize<S>(u: &u128, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serde::Serialize::serialize(&u.to_le_bytes(), serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<u128, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Ok(u128::from_le_bytes(serde::Deserialize::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
40
tarpc/src/transport.rs
Normal file
40
tarpc/src/transport.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! Provides a [`Transport`](sealed::Transport) trait as well as implementations.
|
||||
//!
|
||||
//! The rpc crate is transport- and protocol-agnostic. Any transport that impls [`Transport`](sealed::Transport)
|
||||
//! can be plugged in, using whatever protocol it wants.
|
||||
|
||||
pub mod channel;
|
||||
|
||||
pub(crate) mod sealed {
|
||||
use futures::prelude::*;
|
||||
use std::error::Error;
|
||||
|
||||
/// A bidirectional stream ([`Sink`] + [`Stream`]) of messages.
|
||||
pub trait Transport<SinkItem, Item>
|
||||
where
|
||||
Self: Stream<Item = Result<Item, <Self as Sink<SinkItem>>::Error>>,
|
||||
Self: Sink<SinkItem, Error = <Self as Transport<SinkItem, Item>>::TransportError>,
|
||||
<Self as Sink<SinkItem>>::Error: Error,
|
||||
{
|
||||
/// Associated type where clauses are not elaborated; this associated type allows users
|
||||
/// bounding types by Transport to avoid having to explicitly add `T::Error: Error` to their
|
||||
/// bounds.
|
||||
type TransportError: Error + Send + Sync + 'static;
|
||||
}
|
||||
|
||||
impl<T, SinkItem, Item, E> Transport<SinkItem, Item> for T
|
||||
where
|
||||
T: ?Sized,
|
||||
T: Stream<Item = Result<Item, E>>,
|
||||
T: Sink<SinkItem, Error = E>,
|
||||
T::Error: Error + Send + Sync + 'static,
|
||||
{
|
||||
type TransportError = E;
|
||||
}
|
||||
}
|
||||
202
tarpc/src/transport/channel.rs
Normal file
202
tarpc/src/transport/channel.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
//! Transports backed by in-memory channels.
|
||||
|
||||
use futures::{task::*, Sink, Stream};
|
||||
use pin_project::pin_project;
|
||||
use std::{error::Error, pin::Pin};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Errors that occur in the sending or receiving of messages over a channel.
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ChannelError {
|
||||
/// An error occurred sending over the channel.
|
||||
#[error("an error occurred sending over the channel")]
|
||||
Send(#[source] Box<dyn Error + Send + Sync + 'static>),
|
||||
}
|
||||
|
||||
/// Returns two unbounded channel peers. Each [`Stream`] yields items sent through the other's
|
||||
/// [`Sink`].
|
||||
pub fn unbounded<SinkItem, Item>() -> (
|
||||
UnboundedChannel<SinkItem, Item>,
|
||||
UnboundedChannel<Item, SinkItem>,
|
||||
) {
|
||||
let (tx1, rx2) = mpsc::unbounded_channel();
|
||||
let (tx2, rx1) = mpsc::unbounded_channel();
|
||||
(
|
||||
UnboundedChannel { tx: tx1, rx: rx1 },
|
||||
UnboundedChannel { tx: tx2, rx: rx2 },
|
||||
)
|
||||
}
|
||||
|
||||
/// A bi-directional channel backed by an [`UnboundedSender`](mpsc::UnboundedSender)
|
||||
/// and [`UnboundedReceiver`](mpsc::UnboundedReceiver).
|
||||
#[derive(Debug)]
|
||||
pub struct UnboundedChannel<Item, SinkItem> {
|
||||
rx: mpsc::UnboundedReceiver<Item>,
|
||||
tx: mpsc::UnboundedSender<SinkItem>,
|
||||
}
|
||||
|
||||
impl<Item, SinkItem> Stream for UnboundedChannel<Item, SinkItem> {
|
||||
type Item = Result<Item, ChannelError>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Item, ChannelError>>> {
|
||||
self.rx.poll_recv(cx).map(|option| option.map(Ok))
|
||||
}
|
||||
}
|
||||
|
||||
const CLOSED_MESSAGE: &str = "the channel is closed and cannot accept new items for sending";
|
||||
|
||||
impl<Item, SinkItem> Sink<SinkItem> for UnboundedChannel<Item, SinkItem> {
|
||||
type Error = ChannelError;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(if self.tx.is_closed() {
|
||||
Err(ChannelError::Send(CLOSED_MESSAGE.into()))
|
||||
} else {
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> {
|
||||
self.tx
|
||||
.send(item)
|
||||
.map_err(|_| ChannelError::Send(CLOSED_MESSAGE.into()))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
// UnboundedSender requires no flushing.
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
// UnboundedSender can't initiate closure.
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns two channel peers with buffer equal to `capacity`. Each [`Stream`] yields items sent
|
||||
/// through the other's [`Sink`].
|
||||
pub fn bounded<SinkItem, Item>(
|
||||
capacity: usize,
|
||||
) -> (Channel<SinkItem, Item>, Channel<Item, SinkItem>) {
|
||||
let (tx1, rx2) = futures::channel::mpsc::channel(capacity);
|
||||
let (tx2, rx1) = futures::channel::mpsc::channel(capacity);
|
||||
(Channel { tx: tx1, rx: rx1 }, Channel { tx: tx2, rx: rx2 })
|
||||
}
|
||||
|
||||
/// A bi-directional channel backed by a [`Sender`](futures::channel::mpsc::Sender)
|
||||
/// and [`Receiver`](futures::channel::mpsc::Receiver).
|
||||
#[pin_project]
|
||||
#[derive(Debug)]
|
||||
pub struct Channel<Item, SinkItem> {
|
||||
#[pin]
|
||||
rx: futures::channel::mpsc::Receiver<Item>,
|
||||
#[pin]
|
||||
tx: futures::channel::mpsc::Sender<SinkItem>,
|
||||
}
|
||||
|
||||
impl<Item, SinkItem> Stream for Channel<Item, SinkItem> {
|
||||
type Item = Result<Item, ChannelError>;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Item, ChannelError>>> {
|
||||
self.project().rx.poll_next(cx).map(|option| option.map(Ok))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Item, SinkItem> Sink<SinkItem> for Channel<Item, SinkItem> {
|
||||
type Error = ChannelError;
|
||||
|
||||
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.project()
|
||||
.tx
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| ChannelError::Send(Box::new(e)))
|
||||
}
|
||||
|
||||
fn start_send(self: Pin<&mut Self>, item: SinkItem) -> Result<(), Self::Error> {
|
||||
self.project()
|
||||
.tx
|
||||
.start_send(item)
|
||||
.map_err(|e| ChannelError::Send(Box::new(e)))
|
||||
}
|
||||
|
||||
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.project()
|
||||
.tx
|
||||
.poll_flush(cx)
|
||||
.map_err(|e| ChannelError::Send(Box::new(e)))
|
||||
}
|
||||
|
||||
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.project()
|
||||
.tx
|
||||
.poll_close(cx)
|
||||
.map_err(|e| ChannelError::Send(Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "tokio1")]
|
||||
mod tests {
|
||||
use crate::{
|
||||
client, context,
|
||||
server::{incoming::Incoming, BaseChannel},
|
||||
transport::{
|
||||
self,
|
||||
channel::{Channel, UnboundedChannel},
|
||||
},
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{prelude::*, stream};
|
||||
use std::io;
|
||||
use tracing::trace;
|
||||
|
||||
#[test]
|
||||
fn ensure_is_transport() {
|
||||
fn is_transport<SinkItem, Item, T: crate::Transport<SinkItem, Item>>() {}
|
||||
is_transport::<(), (), UnboundedChannel<(), ()>>();
|
||||
is_transport::<(), (), Channel<(), ()>>();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn integration() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (client_channel, server_channel) = transport::channel::unbounded();
|
||||
tokio::spawn(
|
||||
stream::once(future::ready(server_channel))
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(|_ctx, request: String| {
|
||||
future::ready(request.parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("{request:?} is not an int"),
|
||||
)
|
||||
}))
|
||||
}),
|
||||
);
|
||||
|
||||
let client = client::new(client::Config::default(), client_channel).spawn();
|
||||
|
||||
let response1 = client.call(context::current(), "", "123".into()).await?;
|
||||
let response2 = client.call(context::current(), "", "abc".into()).await?;
|
||||
|
||||
trace!("response1: {:?}, response2: {:?}", response1, response2);
|
||||
|
||||
assert_matches!(response1, Ok(123));
|
||||
assert_matches!(response2, Err(ref e) if e.kind() == io::ErrorKind::InvalidInput);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
use std::io::{self, Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// A factory for creating a listener on a given address.
|
||||
/// For TCP, an address might be an IPv4 address; for Unix sockets, it
|
||||
/// is just a file name.
|
||||
pub trait Transport {
|
||||
/// The type of listener that binds to the given address.
|
||||
type Listener: Listener;
|
||||
/// Return a listener on the given address, and a dialer to that address.
|
||||
fn bind(&self) -> io::Result<Self::Listener>;
|
||||
}
|
||||
|
||||
/// Accepts incoming connections from dialers.
|
||||
pub trait Listener: Send + 'static {
|
||||
/// The type of address being listened on.
|
||||
type Dialer: Dialer;
|
||||
/// The type of stream this listener accepts.
|
||||
type Stream: Stream;
|
||||
/// Accept an incoming stream.
|
||||
fn accept(&self) -> io::Result<Self::Stream>;
|
||||
/// Returns the local address being listened on.
|
||||
fn dialer(&self) -> io::Result<Self::Dialer>;
|
||||
/// Iterate over incoming connections.
|
||||
fn incoming(&self) -> Incoming<Self> {
|
||||
Incoming { listener: self }
|
||||
}
|
||||
}
|
||||
|
||||
/// A cloneable Reader/Writer.
|
||||
pub trait Stream: Read + Write + Send + Sized + 'static {
|
||||
/// Creates a new independently owned handle to the Stream.
|
||||
///
|
||||
/// The returned Stream should reference the same stream that this
|
||||
/// object references. Both handles should read and write the same
|
||||
/// stream of data, and options set on one stream should be propagated
|
||||
/// to the other stream.
|
||||
fn try_clone(&self) -> io::Result<Self>;
|
||||
/// Sets a read timeout.
|
||||
///
|
||||
/// If the value specified is `None`, then read calls will block indefinitely.
|
||||
/// It is an error to pass the zero `Duration` to this method.
|
||||
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
|
||||
/// Sets a write timeout.
|
||||
///
|
||||
/// If the value specified is `None`, then write calls will block indefinitely.
|
||||
/// It is an error to pass the zero `Duration` to this method.
|
||||
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()>;
|
||||
/// Shuts down both ends of the stream.
|
||||
///
|
||||
/// Implementations should cause all pending and future I/O on the specified
|
||||
/// portions to return immediately with an appropriate value.
|
||||
fn shutdown(&self) -> io::Result<()>;
|
||||
}
|
||||
|
||||
/// A `Stream` factory.
|
||||
pub trait Dialer {
|
||||
/// The type of `Stream` this can create.
|
||||
type Stream: Stream;
|
||||
/// Open a stream.
|
||||
fn dial(&self) -> io::Result<Self::Stream>;
|
||||
}
|
||||
|
||||
impl<P, D: ?Sized> Dialer for P
|
||||
where P: ::std::ops::Deref<Target = D>,
|
||||
D: Dialer + 'static
|
||||
{
|
||||
type Stream = D::Stream;
|
||||
|
||||
fn dial(&self) -> io::Result<Self::Stream> {
|
||||
(**self).dial()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over incoming connections.
|
||||
pub struct Incoming<'a, L: Listener + ?Sized + 'a> {
|
||||
listener: &'a L,
|
||||
}
|
||||
|
||||
impl<'a, L: Listener> Iterator for Incoming<'a, L> {
|
||||
type Item = io::Result<L::Stream>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
Some(self.listener.accept())
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides a TCP transport.
|
||||
pub mod tcp;
|
||||
/// Provides a unix socket transport.
|
||||
pub mod unix;
|
||||
@@ -1,77 +0,0 @@
|
||||
use std::io;
|
||||
use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
|
||||
/// A transport for TCP.
|
||||
#[derive(Debug)]
|
||||
pub struct TcpTransport<A: ToSocketAddrs>(pub A);
|
||||
|
||||
impl<A: ToSocketAddrs> super::Transport for TcpTransport<A> {
|
||||
type Listener = TcpListener;
|
||||
|
||||
fn bind(&self) -> io::Result<TcpListener> {
|
||||
TcpListener::bind(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: ToSocketAddrs> super::Transport for A {
|
||||
type Listener = TcpListener;
|
||||
|
||||
fn bind(&self) -> io::Result<TcpListener> {
|
||||
TcpListener::bind(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Listener for TcpListener {
|
||||
type Dialer = TcpDialer<SocketAddr>;
|
||||
|
||||
type Stream = TcpStream;
|
||||
|
||||
fn accept(&self) -> io::Result<TcpStream> {
|
||||
self.accept().map(|(stream, _)| stream)
|
||||
}
|
||||
|
||||
fn dialer(&self) -> io::Result<TcpDialer<SocketAddr>> {
|
||||
self.local_addr().map(|addr| TcpDialer(addr))
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Stream for TcpStream {
|
||||
fn try_clone(&self) -> io::Result<Self> {
|
||||
self.try_clone()
|
||||
}
|
||||
|
||||
fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
|
||||
self.set_read_timeout(dur)
|
||||
}
|
||||
|
||||
fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> {
|
||||
self.set_write_timeout(dur)
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> io::Result<()> {
|
||||
self.shutdown(::std::net::Shutdown::Both)
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to a socket address.
|
||||
#[derive(Debug)]
|
||||
pub struct TcpDialer<A = SocketAddr>(pub A) where A: ToSocketAddrs;
|
||||
|
||||
impl<A> super::Dialer for TcpDialer<A>
|
||||
where A: ToSocketAddrs
|
||||
{
|
||||
type Stream = TcpStream;
|
||||
|
||||
fn dial(&self) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Dialer for str {
|
||||
type Stream = TcpStream;
|
||||
|
||||
fn dial(&self) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(self)
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use unix_socket::{UnixListener, UnixStream};
|
||||
|
||||
/// A transport for unix sockets.
|
||||
#[derive(Debug)]
|
||||
pub struct UnixTransport<P>(pub P) where P: AsRef<Path>;
|
||||
|
||||
impl<P> super::Transport for UnixTransport<P>
|
||||
where P: AsRef<Path>
|
||||
{
|
||||
type Listener = UnixListener;
|
||||
|
||||
fn bind(&self) -> io::Result<UnixListener> {
|
||||
UnixListener::bind(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to a unix socket address.
|
||||
#[derive(Debug)]
|
||||
pub struct UnixDialer<P>(pub P) where P: AsRef<Path>;
|
||||
|
||||
impl<P> super::Dialer for UnixDialer<P>
|
||||
where P: AsRef<Path>
|
||||
{
|
||||
type Stream = UnixStream;
|
||||
|
||||
fn dial(&self) -> io::Result<UnixStream> {
|
||||
UnixStream::connect(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Listener for UnixListener {
|
||||
type Stream = UnixStream;
|
||||
|
||||
type Dialer = UnixDialer<PathBuf>;
|
||||
|
||||
fn accept(&self) -> io::Result<UnixStream> {
|
||||
self.accept().map(|(stream, _)| stream)
|
||||
}
|
||||
|
||||
fn dialer(&self) -> io::Result<UnixDialer<PathBuf>> {
|
||||
self.local_addr().and_then(|addr| {
|
||||
match addr.as_pathname() {
|
||||
Some(path) => Ok(UnixDialer(path.to_owned())),
|
||||
None => {
|
||||
Err(io::Error::new(io::ErrorKind::AddrNotAvailable,
|
||||
"Couldn't get a path to bound unix socket"))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Stream for UnixStream {
|
||||
fn try_clone(&self) -> io::Result<Self> {
|
||||
self.try_clone()
|
||||
}
|
||||
|
||||
fn set_read_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
|
||||
self.set_read_timeout(timeout)
|
||||
}
|
||||
|
||||
fn set_write_timeout(&self, timeout: Option<Duration>) -> io::Result<()> {
|
||||
self.set_write_timeout(timeout)
|
||||
}
|
||||
|
||||
fn shutdown(&self) -> io::Result<()> {
|
||||
self.shutdown(::std::net::Shutdown::Both)
|
||||
}
|
||||
}
|
||||
71
tarpc/src/util.rs
Normal file
71
tarpc/src/util.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
hash::{BuildHasher, Hash},
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
#[cfg(feature = "serde1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "serde1")))]
|
||||
pub mod serde;
|
||||
|
||||
/// Extension trait for [SystemTimes](SystemTime) in the future, i.e. deadlines.
|
||||
pub trait TimeUntil {
|
||||
/// How much time from now until this time is reached.
|
||||
fn time_until(&self) -> Duration;
|
||||
}
|
||||
|
||||
impl TimeUntil for SystemTime {
|
||||
fn time_until(&self) -> Duration {
|
||||
self.duration_since(SystemTime::now()).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Collection compaction; configurable `shrink_to_fit`.
|
||||
pub trait Compact {
|
||||
/// Compacts space if the ratio of length : capacity is less than `usage_ratio_threshold`.
|
||||
fn compact(&mut self, usage_ratio_threshold: f64);
|
||||
}
|
||||
|
||||
impl<K, V, H> Compact for HashMap<K, V, H>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
H: BuildHasher,
|
||||
{
|
||||
fn compact(&mut self, usage_ratio_threshold: f64) {
|
||||
let usage_ratio_threshold = usage_ratio_threshold.clamp(f64::MIN_POSITIVE, 1.);
|
||||
let cap = f64::max(1000., self.len() as f64 / usage_ratio_threshold);
|
||||
self.shrink_to(cap as usize);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compact() {
|
||||
let mut map = HashMap::with_capacity(2048);
|
||||
assert_eq!(map.capacity(), 3584);
|
||||
|
||||
// Make usage ratio 25%
|
||||
for i in 0..896 {
|
||||
map.insert(format!("k{i}"), "v");
|
||||
}
|
||||
|
||||
map.compact(-1.0);
|
||||
assert_eq!(map.capacity(), 3584);
|
||||
|
||||
map.compact(0.25);
|
||||
assert_eq!(map.capacity(), 3584);
|
||||
|
||||
map.compact(0.50);
|
||||
assert_eq!(map.capacity(), 1792);
|
||||
|
||||
map.compact(1.0);
|
||||
assert_eq!(map.capacity(), 1792);
|
||||
|
||||
map.compact(2.0);
|
||||
assert_eq!(map.capacity(), 1792);
|
||||
}
|
||||
73
tarpc/src/util/serde.rs
Normal file
73
tarpc/src/util/serde.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::io;
|
||||
|
||||
/// Serializes [`io::ErrorKind`] as a `u32`.
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)] // Exact fn signature required by serde derive
|
||||
pub fn serialize_io_error_kind_as_u32<S>(
|
||||
kind: &io::ErrorKind,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
use std::io::ErrorKind::*;
|
||||
match *kind {
|
||||
NotFound => 0,
|
||||
PermissionDenied => 1,
|
||||
ConnectionRefused => 2,
|
||||
ConnectionReset => 3,
|
||||
ConnectionAborted => 4,
|
||||
NotConnected => 5,
|
||||
AddrInUse => 6,
|
||||
AddrNotAvailable => 7,
|
||||
BrokenPipe => 8,
|
||||
AlreadyExists => 9,
|
||||
WouldBlock => 10,
|
||||
InvalidInput => 11,
|
||||
InvalidData => 12,
|
||||
TimedOut => 13,
|
||||
WriteZero => 14,
|
||||
Interrupted => 15,
|
||||
Other => 16,
|
||||
UnexpectedEof => 17,
|
||||
_ => 16,
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
/// Deserializes [`io::ErrorKind`] from a `u32`.
|
||||
pub fn deserialize_io_error_kind_from_u32<'de, D>(
|
||||
deserializer: D,
|
||||
) -> Result<io::ErrorKind, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use std::io::ErrorKind::*;
|
||||
Ok(match u32::deserialize(deserializer)? {
|
||||
0 => NotFound,
|
||||
1 => PermissionDenied,
|
||||
2 => ConnectionRefused,
|
||||
3 => ConnectionReset,
|
||||
4 => ConnectionAborted,
|
||||
5 => NotConnected,
|
||||
6 => AddrInUse,
|
||||
7 => AddrNotAvailable,
|
||||
8 => BrokenPipe,
|
||||
9 => AlreadyExists,
|
||||
10 => WouldBlock,
|
||||
11 => InvalidInput,
|
||||
12 => InvalidData,
|
||||
13 => TimedOut,
|
||||
14 => WriteZero,
|
||||
15 => Interrupted,
|
||||
16 => Other,
|
||||
17 => UnexpectedEof,
|
||||
_ => Other,
|
||||
})
|
||||
}
|
||||
5
tarpc/tests/compile_fail.rs
Normal file
5
tarpc/tests/compile_fail.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[test]
|
||||
fn ui() {
|
||||
let t = trybuild::TestCases::new();
|
||||
t.compile_fail("tests/compile_fail/*.rs");
|
||||
}
|
||||
15
tarpc/tests/compile_fail/tarpc_server_missing_async.rs
Normal file
15
tarpc/tests/compile_fail/tarpc_server_missing_async.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
#[tarpc::service(derive_serde = false)]
|
||||
trait World {
|
||||
async fn hello(name: String) -> String;
|
||||
}
|
||||
|
||||
struct HelloServer;
|
||||
|
||||
#[tarpc::server]
|
||||
impl World for HelloServer {
|
||||
fn hello(name: String) -> String {
|
||||
format!("Hello, {name}!", name)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
11
tarpc/tests/compile_fail/tarpc_server_missing_async.stderr
Normal file
11
tarpc/tests/compile_fail/tarpc_server_missing_async.stderr
Normal file
@@ -0,0 +1,11 @@
|
||||
error: not all trait items implemented, missing: `HelloFut`
|
||||
--> $DIR/tarpc_server_missing_async.rs:9:1
|
||||
|
|
||||
9 | impl World for HelloServer {
|
||||
| ^^^^
|
||||
|
||||
error: hint: `#[tarpc::server]` only rewrites async fns, and `fn hello` is not async
|
||||
--> $DIR/tarpc_server_missing_async.rs:10:5
|
||||
|
|
||||
10 | fn hello(name: String) -> String {
|
||||
| ^^
|
||||
6
tarpc/tests/compile_fail/tarpc_service_arg_pat.rs
Normal file
6
tarpc/tests/compile_fail/tarpc_service_arg_pat.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[tarpc::service]
|
||||
trait World {
|
||||
async fn pat((a, b): (u8, u32));
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
5
tarpc/tests/compile_fail/tarpc_service_arg_pat.stderr
Normal file
5
tarpc/tests/compile_fail/tarpc_service_arg_pat.stderr
Normal file
@@ -0,0 +1,5 @@
|
||||
error: patterns aren't allowed in RPC args
|
||||
--> $DIR/tarpc_service_arg_pat.rs:3:18
|
||||
|
|
||||
3 | async fn pat((a, b): (u8, u32));
|
||||
| ^^^^^^
|
||||
6
tarpc/tests/compile_fail/tarpc_service_fn_new.rs
Normal file
6
tarpc/tests/compile_fail/tarpc_service_fn_new.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[tarpc::service]
|
||||
trait World {
|
||||
async fn new();
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
5
tarpc/tests/compile_fail/tarpc_service_fn_new.stderr
Normal file
5
tarpc/tests/compile_fail/tarpc_service_fn_new.stderr
Normal file
@@ -0,0 +1,5 @@
|
||||
error: method name conflicts with generated fn `WorldClient::new`
|
||||
--> $DIR/tarpc_service_fn_new.rs:3:14
|
||||
|
|
||||
3 | async fn new();
|
||||
| ^^^
|
||||
6
tarpc/tests/compile_fail/tarpc_service_fn_serve.rs
Normal file
6
tarpc/tests/compile_fail/tarpc_service_fn_serve.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#[tarpc::service]
|
||||
trait World {
|
||||
async fn serve();
|
||||
}
|
||||
|
||||
fn main() {}
|
||||
5
tarpc/tests/compile_fail/tarpc_service_fn_serve.stderr
Normal file
5
tarpc/tests/compile_fail/tarpc_service_fn_serve.stderr
Normal file
@@ -0,0 +1,5 @@
|
||||
error: method name conflicts with generated fn `World::serve`
|
||||
--> $DIR/tarpc_service_fn_serve.rs:3:14
|
||||
|
|
||||
3 | async fn serve();
|
||||
| ^^^^^
|
||||
55
tarpc/tests/dataservice.rs
Normal file
55
tarpc/tests/dataservice.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use futures::prelude::*;
|
||||
use tarpc::serde_transport;
|
||||
use tarpc::{
|
||||
client, context,
|
||||
server::{incoming::Incoming, BaseChannel},
|
||||
};
|
||||
use tokio_serde::formats::Json;
|
||||
|
||||
#[tarpc::derive_serde]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum TestData {
|
||||
Black,
|
||||
White,
|
||||
}
|
||||
|
||||
#[tarpc::service]
|
||||
pub trait ColorProtocol {
|
||||
async fn get_opposite_color(color: TestData) -> TestData;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ColorServer;
|
||||
|
||||
#[tarpc::server]
|
||||
impl ColorProtocol for ColorServer {
|
||||
async fn get_opposite_color(self, _: context::Context, color: TestData) -> TestData {
|
||||
match color {
|
||||
TestData::White => TestData::Black,
|
||||
TestData::Black => TestData::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_call() -> anyhow::Result<()> {
|
||||
let transport = tarpc::serde_transport::tcp::listen("localhost:56797", Json::default).await?;
|
||||
let addr = transport.local_addr();
|
||||
tokio::spawn(
|
||||
transport
|
||||
.take(1)
|
||||
.filter_map(|r| async { r.ok() })
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(ColorServer.serve()),
|
||||
);
|
||||
|
||||
let transport = serde_transport::tcp::connect(addr, Json::default).await?;
|
||||
let client = ColorProtocolClient::new(client::Config::default(), transport).spawn();
|
||||
|
||||
let color = client
|
||||
.get_opposite_color(context::current(), TestData::White)
|
||||
.await?;
|
||||
assert_eq!(color, TestData::Black);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
244
tarpc/tests/service_functional.rs
Normal file
244
tarpc/tests/service_functional.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
use assert_matches::assert_matches;
|
||||
use futures::{
|
||||
future::{join_all, ready, Ready},
|
||||
prelude::*,
|
||||
};
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tarpc::{
|
||||
client::{self},
|
||||
context,
|
||||
server::{self, incoming::Incoming, BaseChannel, Channel},
|
||||
transport::channel,
|
||||
};
|
||||
use tokio::join;
|
||||
|
||||
#[tarpc_plugins::service]
|
||||
trait Service {
|
||||
async fn add(x: i32, y: i32) -> i32;
|
||||
async fn hey(name: String) -> String;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Server;
|
||||
|
||||
impl Service for Server {
|
||||
type AddFut = Ready<i32>;
|
||||
|
||||
fn add(self, _: context::Context, x: i32, y: i32) -> Self::AddFut {
|
||||
ready(x + y)
|
||||
}
|
||||
|
||||
type HeyFut = Ready<String>;
|
||||
|
||||
fn hey(self, _: context::Context, name: String) -> Self::HeyFut {
|
||||
ready(format!("Hey, {name}."))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sequential() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
|
||||
tokio::spawn(
|
||||
BaseChannel::new(server::Config::default(), rx)
|
||||
.requests()
|
||||
.execute(Server.serve()),
|
||||
);
|
||||
|
||||
let client = ServiceClient::new(client::Config::default(), tx).spawn();
|
||||
|
||||
assert_matches!(client.add(context::current(), 1, 2).await, Ok(3));
|
||||
assert_matches!(
|
||||
client.hey(context::current(), "Tim".into()).await,
|
||||
Ok(ref s) if s == "Hey, Tim.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn dropped_channel_aborts_in_flight_requests() -> anyhow::Result<()> {
|
||||
#[tarpc_plugins::service]
|
||||
trait Loop {
|
||||
async fn r#loop();
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LoopServer;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AllHandlersComplete;
|
||||
|
||||
#[tarpc::server]
|
||||
impl Loop for LoopServer {
|
||||
async fn r#loop(self, _: context::Context) {
|
||||
loop {
|
||||
futures::pending!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
|
||||
// Set up a client that initiates a long-lived request.
|
||||
// The request will complete in error when the server drops the connection.
|
||||
tokio::spawn(async move {
|
||||
let client = LoopClient::new(client::Config::default(), tx).spawn();
|
||||
|
||||
let mut ctx = context::current();
|
||||
ctx.deadline = SystemTime::now() + Duration::from_secs(60 * 60);
|
||||
let _ = client.r#loop(ctx).await;
|
||||
});
|
||||
|
||||
let mut requests = BaseChannel::with_defaults(rx).requests();
|
||||
// Reading a request should trigger the request being registered with BaseChannel.
|
||||
let first_request = requests.next().await.unwrap()?;
|
||||
// Dropping the channel should trigger cleanup of outstanding requests.
|
||||
drop(requests);
|
||||
// In-flight requests should be aborted by channel cleanup.
|
||||
// The first and only request sent by the client is `loop`, which is an infinite loop
|
||||
// on the server side, so if cleanup was not triggered, this line should hang indefinitely.
|
||||
first_request.execute(LoopServer.serve()).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "serde-transport", feature = "tcp"))]
|
||||
#[tokio::test]
|
||||
async fn serde() -> anyhow::Result<()> {
|
||||
use tarpc::serde_transport;
|
||||
use tokio_serde::formats::Json;
|
||||
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let transport = tarpc::serde_transport::tcp::listen("localhost:56789", Json::default).await?;
|
||||
let addr = transport.local_addr();
|
||||
tokio::spawn(
|
||||
transport
|
||||
.take(1)
|
||||
.filter_map(|r| async { r.ok() })
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(Server.serve()),
|
||||
);
|
||||
|
||||
let transport = serde_transport::tcp::connect(addr, Json::default).await?;
|
||||
let client = ServiceClient::new(client::Config::default(), transport).spawn();
|
||||
|
||||
assert_matches!(client.add(context::current(), 1, 2).await, Ok(3));
|
||||
assert_matches!(
|
||||
client.hey(context::current(), "Tim".to_string()).await,
|
||||
Ok(ref s) if s == "Hey, Tim."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio::spawn(
|
||||
stream::once(ready(rx))
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(Server.serve()),
|
||||
);
|
||||
|
||||
let client = ServiceClient::new(client::Config::default(), tx).spawn();
|
||||
|
||||
let req1 = client.add(context::current(), 1, 2);
|
||||
let req2 = client.add(context::current(), 3, 4);
|
||||
let req3 = client.hey(context::current(), "Tim".to_string());
|
||||
|
||||
assert_matches!(req1.await, Ok(3));
|
||||
assert_matches!(req2.await, Ok(7));
|
||||
assert_matches!(req3.await, Ok(ref s) if s == "Hey, Tim.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_join() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio::spawn(
|
||||
stream::once(ready(rx))
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(Server.serve()),
|
||||
);
|
||||
|
||||
let client = ServiceClient::new(client::Config::default(), tx).spawn();
|
||||
|
||||
let req1 = client.add(context::current(), 1, 2);
|
||||
let req2 = client.add(context::current(), 3, 4);
|
||||
let req3 = client.hey(context::current(), "Tim".to_string());
|
||||
|
||||
let (resp1, resp2, resp3) = join!(req1, req2, req3);
|
||||
assert_matches!(resp1, Ok(3));
|
||||
assert_matches!(resp2, Ok(7));
|
||||
assert_matches!(resp3, Ok(ref s) if s == "Hey, Tim.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_join_all() -> anyhow::Result<()> {
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio::spawn(
|
||||
stream::once(ready(rx))
|
||||
.map(BaseChannel::with_defaults)
|
||||
.execute(Server.serve()),
|
||||
);
|
||||
|
||||
let client = ServiceClient::new(client::Config::default(), tx).spawn();
|
||||
|
||||
let req1 = client.add(context::current(), 1, 2);
|
||||
let req2 = client.add(context::current(), 3, 4);
|
||||
|
||||
let responses = join_all(vec![req1, req2]).await;
|
||||
assert_matches!(responses[0], Ok(3));
|
||||
assert_matches!(responses[1], Ok(7));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn counter() -> anyhow::Result<()> {
|
||||
#[tarpc::service]
|
||||
trait Counter {
|
||||
async fn count() -> u32;
|
||||
}
|
||||
|
||||
struct CountService(u32);
|
||||
|
||||
impl Counter for &mut CountService {
|
||||
type CountFut = futures::future::Ready<u32>;
|
||||
|
||||
fn count(self, _: context::Context) -> Self::CountFut {
|
||||
self.0 += 1;
|
||||
futures::future::ready(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = channel::unbounded();
|
||||
tokio::spawn(async {
|
||||
let mut requests = BaseChannel::with_defaults(rx).requests();
|
||||
let mut counter = CountService(0);
|
||||
|
||||
while let Some(Ok(request)) = requests.next().await {
|
||||
request.execute(counter.serve()).await;
|
||||
}
|
||||
});
|
||||
|
||||
let client = CounterClient::new(client::Config::default(), tx).spawn();
|
||||
assert_matches!(client.count(context::current()).await, Ok(1));
|
||||
assert_matches!(client.count(context::current()).await, Ok(2));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "tarpc_examples"
|
||||
version = "0.1.0"
|
||||
authors = ["Adam Wright <adam.austin.wright@gmail.com>", "Tim Kuehn <timothy.j.kuehn@gmail.com>"]
|
||||
|
||||
[dev-dependencies]
|
||||
tarpc = { path = "../tarpc" }
|
||||
lazy_static = "0.2"
|
||||
env_logger = "0.3"
|
||||
@@ -1,74 +0,0 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the MIT License, <LICENSE or http://opensource.org/licenses/MIT>.
|
||||
// This file may not be copied, modified, or distributed except according to those terms.
|
||||
|
||||
#![cfg_attr(test, feature(test))]
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate tarpc;
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)] // generated Client isn't used in this benchmark
|
||||
mod benchmark {
|
||||
extern crate env_logger;
|
||||
extern crate test;
|
||||
|
||||
use tarpc::ServeHandle;
|
||||
use self::test::Bencher;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
service! {
|
||||
rpc hello(s: String) -> String;
|
||||
}
|
||||
|
||||
struct HelloServer;
|
||||
impl Service for HelloServer {
|
||||
fn hello(&self, s: String) -> String {
|
||||
format!("Hello, {}!", s)
|
||||
}
|
||||
}
|
||||
|
||||
// Prevents resource exhaustion when benching
|
||||
lazy_static! {
|
||||
static ref HANDLE: Arc<Mutex<ServeHandle>> = {
|
||||
let handle = HelloServer.spawn("localhost:0").unwrap();
|
||||
Arc::new(Mutex::new(handle))
|
||||
};
|
||||
static ref CLIENT: Arc<Mutex<AsyncClient>> = {
|
||||
let lock = HANDLE.lock().unwrap();
|
||||
let dialer = lock.dialer();
|
||||
let client = AsyncClient::new(dialer).unwrap();
|
||||
Arc::new(Mutex::new(client))
|
||||
};
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn hello(bencher: &mut Bencher) {
|
||||
let _ = env_logger::init();
|
||||
let client = CLIENT.lock().unwrap();
|
||||
let concurrency = 100;
|
||||
let mut futures = Vec::with_capacity(concurrency);
|
||||
let mut count = 0;
|
||||
bencher.iter(|| {
|
||||
futures.push(client.hello("Bob".into()));
|
||||
count += 1;
|
||||
if count % concurrency == 0 {
|
||||
// We can't block on each rpc call, otherwise we'd be
|
||||
// benchmarking latency instead of throughput. It's also
|
||||
// not ideal to call more than one rpc per iteration, because
|
||||
// it makes the output of the bencher harder to parse (you have
|
||||
// to mentally divide the number by `concurrency` to get
|
||||
// the ns / iter for one rpc
|
||||
for f in futures.drain(..) {
|
||||
f.get().unwrap();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user