ttrpc-0.5.2/.cargo_vcs_info.json0000644000000001120000000000000121650ustar { "git": { "sha1": "4a6173d5d987ac68fbcd4e150ddafde3b4d4e23f" } } ttrpc-0.5.2/.github/ISSUE_TEMPLATE/bug_report.md000064400000000000000000000005440000000000000171610ustar 00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- # Description of problem (replace this text with the list of steps you followed) # Expected result (replace this text with an explanation of what you thought would happen) # Actual result (replace this text with details of what actually happened) ttrpc-0.5.2/.github/ISSUE_TEMPLATE/enhancement-request.md000064400000000000000000000010220000000000000207540ustar 00000000000000--- name: Enhancement request about: Suggest an improvement to an existing feature title: '' labels: '' assignees: '' --- **Which feature do you think can be improved?** Specify the feature you think could be made better. **How can it be improved?** Describe how specifically you think it could be improved. **Additional Information** Anything else to add? **Before raising this feature request** Have you looked at the [limitations document](https://github.com/kata-containers/documentation/blob/master/Limitations.md)? ttrpc-0.5.2/.github/ISSUE_TEMPLATE/feature_request.md000064400000000000000000000013750000000000000202170ustar 00000000000000--- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. **Before raising this feature request** Have you looked at the [limitations document](https://github.com/kata-containers/documentation/blob/master/Limitations.md)? ttrpc-0.5.2/.github/workflows/bvt.yml000064400000000000000000000012710000000000000156550ustar 00000000000000name: BVT on: [pull_request] jobs: make: name: Make runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] steps: - name: Checkout uses: actions/checkout@v1 - name: Make run: | make deps make clippy_check: name: Clippy Check runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@v1 - run: rustup component add clippy - uses: tim-actions/clippy-check@master with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features name: checks-${{ matrix.os }} ttrpc-0.5.2/.github/workflows/sanity.yml000064400000000000000000000016350000000000000163750ustar 00000000000000name: Sanity Check on: pull_request: types: - opened - synchronize - reopened - edited - labeled - unlabeled jobs: commits_check_job: runs-on: ubuntu-latest name: Commits Check steps: - name: Get PR Commits id: 'get-pr-commits' uses: tim-actions/get-pr-commits@master with: token: ${{ secrets.GITHUB_TOKEN }} - name: Commit Body Check uses: tim-actions/commit-body-check@master with: commits: ${{ steps.get-pr-commits.outputs.commits }} - name: DCO Check uses: tim-actions/dco@master with: commits: ${{ steps.get-pr-commits.outputs.commits }} pr_wip_check: runs-on: ubuntu-latest name: WIP Check steps: - name: WIP Check uses: tim-actions/wip-check@master with: labels: '["do-not-merge", "wip", "rfc"]' keywords: '["WIP", "wip", "RFC", "rfc"]' ttrpc-0.5.2/.gitignore000064400000000000000000000001710000000000000127300ustar 00000000000000target Cargo.lock *.rs.bk *.rs.fmt .vscode .idea *.o example/protocols/**/*.rs !example/protocols/**/mod.rs src/ttrpc.rs ttrpc-0.5.2/Cargo.toml0000644000000034600000000000000101740ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies # # If you believe there's an error in this file please file an # issue against the rust-lang/cargo repository. If you're # editing this file be aware that the upstream Cargo.toml # will likely look very different (and much more reasonable) [package] edition = "2018" name = "ttrpc" version = "0.5.2" authors = ["The AntFin Kata Team "] description = "A Rust version of ttrpc." homepage = "https://github.com/containerd/ttrpc-rust" readme = "README.md" keywords = ["ttrpc", "protobuf", "rpc"] license = "Apache-2.0" repository = "https://github.com/containerd/ttrpc-rust" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies.async-trait] version = "0.1.31" optional = true [dependencies.byteorder] version = "1.3.2" [dependencies.bytes] version = "0.4.11" optional = true [dependencies.futures] version = "0.3" optional = true [dependencies.libc] version = "0.2.59" features = ["extra_traits"] [dependencies.log] version = "0.4" [dependencies.nix] version = "0.20.2" [dependencies.protobuf] version = "2.0" optional = true [dependencies.thiserror] version = "1.0" [dependencies.tokio] version = "1" features = ["rt", "sync", "io-util", "macros", "time"] optional = true [build-dependencies.protobuf-codegen-pure] version = "2.14.0" [features] async = ["async-trait", "tokio", "futures", "tokio-vsock"] default = ["protobuf-codec", "sync"] protobuf-codec = ["protobuf"] sync = [] [target."cfg(target_os = \"linux\")".dependencies.tokio-vsock] version = "0.3.1" optional = true ttrpc-0.5.2/Cargo.toml.orig000064400000000000000000000022420000000000000136300ustar 00000000000000[package] name = "ttrpc" version = "0.5.2" authors = ["The AntFin Kata Team "] edition = "2018" license = "Apache-2.0" keywords = ["ttrpc", "protobuf", "rpc"] readme = "README.md" repository = "https://github.com/containerd/ttrpc-rust" homepage = "https://github.com/containerd/ttrpc-rust" description = "A Rust version of ttrpc." [dependencies] protobuf = { version = "2.0", optional = true } bytes = { version = "0.4.11", optional = true } libc = { version = "0.2.59", features = [ "extra_traits" ] } nix = "0.20.2" log = "0.4" byteorder = "1.3.2" thiserror = "1.0" async-trait = { version = "0.1.31", optional = true } tokio = { version = "1", features = ["rt", "sync", "io-util", "macros", "time"], optional = true } futures = { version = "0.3", optional = true } [target.'cfg(target_os = "linux")'.dependencies] tokio-vsock = { version = "0.3.1", optional = true } [build-dependencies] protobuf-codegen-pure = "2.14.0" [features] default = ["protobuf-codec", "sync"] protobuf-codec = ["protobuf"] async = ["async-trait", "tokio", "futures", "tokio-vsock"] sync = [] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] ttrpc-0.5.2/LICENSE000064400000000000000000000261360000000000000117560ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ttrpc-0.5.2/MAINTAINERS000064400000000000000000000002460000000000000124400ustar 00000000000000# ttrpc-rust project maintainers # # MAINTAINERS # GitHub ID, Name, Email address teawater, Hui Zhu, teawater@antfin.com lifupan, Fupan Li, fupan.lfp@alibaba-inc.com ttrpc-0.5.2/Makefile000064400000000000000000000007000000000000000123760ustar 00000000000000all: debug check test # # Build # .PHONY: debug debug: cargo build --verbose --all-targets .PHONY: release release: cargo build --release .PHONY: build build: debug # # Tests and linters # .PHONY: test test: cargo test --verbose .PHONY: check check: cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings .PHONY: deps deps: rustup update stable rustup default stable rustup component add rustfmt clippy ttrpc-0.5.2/README.md000064400000000000000000000070350000000000000122250ustar 00000000000000# ttrpc-rust _ttrpc-rust is a **non-core** subproject of containerd_ `ttrpc-rust` is the Rust version of [ttrpc](https://github.com/containerd/ttrpc). [ttrpc](https://github.com/containerd/ttrpc) is GRPC for low-memory environments. The ttrpc compiler of `ttrpc-rust` `ttrpc_rust_plugin` is modified from gRPC compiler of [gRPC-rs](https://github.com/pingcap/grpc-rs) [grpcio-compiler](https://github.com/pingcap/grpc-rs/tree/master/compiler). ## Usage ### 1. Generate with `protoc` command To generate the sources from proto files: 1. Install protoc from github.com/protocolbuffers/protobuf 2. Install protobuf-codegen ``` cargo install --force protobuf-codegen ``` 3. Install ttrpc_rust_plugin from ttrpc-rust/compiler ``` cd ttrpc-rust/compiler cargo install --force --path . ``` 4. Generate the sources: ``` $ protoc --rust_out=. --ttrpc_out=. --plugin=protoc-gen-ttrpc=`which ttrpc_rust_plugin` example.proto ``` ### 2. Generate programmatically API to generate .rs files to be used e. g. from build.rs. Example code: ``` fn main() { protoc_rust_ttrpc::Codegen::new() .out_dir("protocols") .inputs(&[ "protocols/protos/agent.proto", ]) .include("protocols/protos") .rust_protobuf() // also generate protobuf messages, not just services .run() .expect("Codegen failed."); } ``` # async/.await ttrpc-rust supports async/.await. By using async/.await you can reduce the overhead and resource consumption caused by threads. ## Usage ### 1. Generate codes in async version Currently we only support generating async codes by using ttrpc-codegen ``` ttrpc_codegen::Codegen::new() .out_dir("protocols/asynchronous") .inputs(&protos) .include("protocols/protos") .rust_protobuf() .customize(Customize { async_all: true, // It's the key option. ..Default::default() }) .run() .expect("Gen async codes failed."); ``` Provide customize option - `async_all`: generate async codes for both server and client - `async_server`: generate async codes for server - `async_client`: generate async codes for client > See more in `example/build.rs` ### 2. Write your implemention in async/.await's way Please follow the guidlines in `example/async-server.rs` and `example/async-client.rs` # Run Examples 1. Go to the directory ``` $ cd ttrpc-rust/example ``` 2. Start the server ``` $ cargo run --example server ``` or ``` $ cargo run --example async-server ``` 3. Start a client ``` $ cargo run --example client ``` or ``` $ cargo run --example async-client ``` # Notes: the version of protobuf protobuf-codegen, ttrpc_rust_plugin and your code should use the same version protobuf. You will get following fail if use the different version protobuf. ``` 27 | const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_0; | ^^^^^^^^^^^^^ help: a constant with a similar name exists: `VERSION_2_10_1` ``` The reason is that [files generated by protobuf-codegen are compatible only with the same version of runtime](https://github.com/stepancheg/rust-protobuf/commit/2ab4d50c27c4dd7803b64ce1a43e2c134532c7a6) To fix this issue: 1. Rebuild protobuf-codegen with new protobuf: ``` cd grpc-rs cargo clean cargo update cargo install --force protobuf-codegen ``` 2. Rebuild ttrpc_rust_plugin with new protobuf: ``` cd ttrpc-rust/compiler cargo clean cargo update cargo install --force --path . ``` 3. Build your project. ttrpc-0.5.2/build.rs000064400000000000000000000006520000000000000124110ustar 00000000000000use std::env; use std::fs; use std::path::PathBuf; fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let path: PathBuf = [out_dir.clone(), "mod.rs".to_string()].iter().collect(); fs::write(path, "pub mod ttrpc;").unwrap(); protobuf_codegen_pure::Codegen::new() .out_dir(out_dir) .inputs(&["src/ttrpc.proto"]) .include("src") .run() .expect("Codegen failed."); } ttrpc-0.5.2/src/asynchronous/client.rs000064400000000000000000000152200000000000000161070ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // use nix::unistd::close; use protobuf::{CodedInputStream, CodedOutputStream, Message}; use std::collections::HashMap; use std::os::unix::io::RawFd; use std::sync::{Arc, Mutex}; use crate::common::{client_connect, MESSAGE_TYPE_RESPONSE}; use crate::error::{Error, Result}; use crate::ttrpc::{Code, Request, Response}; use crate::asynchronous::stream::{receive, to_req_buf}; use crate::r#async::utils; use tokio::{ self, io::{split, AsyncWriteExt}, sync::mpsc::{channel, Receiver, Sender}, sync::Notify, }; type RequestSender = Sender<(Vec, Sender>>)>; type RequestReceiver = Receiver<(Vec, Sender>>)>; type ResponseSender = Sender>>; type ResponseReceiver = Receiver>>; /// A ttrpc Client (async). #[derive(Clone)] pub struct Client { req_tx: RequestSender, } impl Client { pub fn connect(path: &str) -> Result { let fd = unsafe { client_connect(path)? }; Ok(Self::new(fd)) } /// Initialize a new [`Client`]. pub fn new(fd: RawFd) -> Client { let stream = utils::new_unix_stream_from_raw_fd(fd); let (mut reader, mut writer) = split(stream); let (req_tx, mut rx): (RequestSender, RequestReceiver) = channel(100); let req_map = Arc::new(Mutex::new(HashMap::new())); let req_map2 = req_map.clone(); let notify = Arc::new(Notify::new()); let notify2 = notify.clone(); // Request sender tokio::spawn(async move { let mut stream_id: u32 = 1; while let Some((body, resp_tx)) = rx.recv().await { let current_stream_id = stream_id; stream_id += 2; { let mut map = req_map2.lock().unwrap(); map.insert(current_stream_id, resp_tx.clone()); } let buf = to_req_buf(current_stream_id, body); if let Err(e) = writer.write_all(&buf).await { error!("write_message got error: {:?}", e); { let mut map = req_map2.lock().unwrap(); map.remove(¤t_stream_id); } let e = Error::Socket(format!("{:?}", e)); resp_tx .send(Err(e)) .await .unwrap_or_else(|_e| error!("The request has returned")); break; // The stream is dead, exit the loop. } } // rx.recv will abort when client.req_tx and client is dropped. // notify the response-receiver to quit at this time. notify.notify_one(); }); // Response receiver tokio::spawn(async move { loop { let req_map = req_map.clone(); tokio::select! { _ = notify2.notified() => { break; } res = receive(&mut reader) => { match res { Ok((header, body)) => { tokio::spawn(async move { let resp_tx2; { let mut map = req_map.lock().unwrap(); let resp_tx = match map.get(&header.stream_id) { Some(tx) => tx, None => { debug!( "Receiver got unknown packet {:?} {:?}", header, body ); return; } }; resp_tx2 = resp_tx.clone(); map.remove(&header.stream_id); // Forget the result, just remove. } if header.type_ != MESSAGE_TYPE_RESPONSE { resp_tx2 .send(Err(Error::Others(format!( "Recver got malformed packet {:?} {:?}", header, body )))) .await .unwrap_or_else(|_e| error!("The request has returned")); return; } resp_tx2.send(Ok(body)).await.unwrap_or_else(|_e| error!("The request has returned")); }); } Err(e) => { trace!("error {:?}", e); break; } } } }; } }); Client { req_tx } } pub async fn request(&mut self, req: Request) -> Result { let mut buf = Vec::with_capacity(req.compute_size() as usize); { let mut s = CodedOutputStream::vec(&mut buf); req.write_to(&mut s).map_err(err_to_others_err!(e, ""))?; s.flush().map_err(err_to_others_err!(e, ""))?; } let (tx, mut rx): (ResponseSender, ResponseReceiver) = channel(100); self.req_tx .send((buf, tx)) .await .map_err(|e| Error::Others(format!("Send packet to sender error {:?}", e)))?; let result = rx .recv() .await .ok_or_else(|| Error::Others("Recive packet from recver error".to_string()))?; let buf = result?; let mut s = CodedInputStream::from_bytes(&buf); let mut res = Response::new(); res.merge_from(&mut s) .map_err(err_to_others_err!(e, "Unpack response error "))?; let status = res.get_status(); if status.get_code() != Code::OK { return Err(Error::RpcStatus((*status).clone())); } Ok(res) } } struct ClientClose { fd: RawFd, close_fd: RawFd, } impl Drop for ClientClose { fn drop(&mut self) { close(self.close_fd).unwrap(); close(self.fd).unwrap(); trace!("All client is droped"); } } ttrpc-0.5.2/src/asynchronous/mod.rs000064400000000000000000000006740000000000000154170ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // //! Server and client in async mode (alias r#async). pub mod client; pub mod server; pub mod stream; #[macro_use] pub mod utils; mod unix_incoming; #[doc(inline)] pub use crate::r#async::client::Client; #[doc(inline)] pub use crate::r#async::server::Server; #[doc(inline)] pub use crate::r#async::utils::{convert_response_to_buf, MethodHandler, TtrpcContext}; ttrpc-0.5.2/src/asynchronous/server.rs000064400000000000000000000276570000000000000161600ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // use crate::r#async::utils; use nix::unistd; use std::collections::HashMap; use std::os::unix::io::RawFd; use std::result::Result as StdResult; use std::sync::Arc; use std::time::Duration; use crate::asynchronous::stream::{receive, respond, respond_with_status}; use crate::asynchronous::unix_incoming::UnixIncoming; use crate::common::{self, Domain, MESSAGE_TYPE_REQUEST}; use crate::context; use crate::error::{get_status, Error, Result}; use crate::r#async::{MethodHandler, TtrpcContext}; use crate::ttrpc::{Code, Status}; use crate::MessageHeader; use futures::stream::Stream; use futures::StreamExt as _; use std::marker::Unpin; use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::net::UnixListener as SysUnixListener; use tokio::{ self, io::{split, AsyncRead, AsyncWrite, AsyncWriteExt}, net::UnixListener, select, spawn, sync::mpsc::{channel, Receiver, Sender}, sync::watch, time::timeout, }; #[cfg(target_os = "linux")] use tokio_vsock::VsockListener; /// A ttrpc Server (async). pub struct Server { listeners: Vec, methods: Arc>>, domain: Option, disconnect_tx: Option>, all_conn_done_rx: Option>, stop_listen_tx: Option>>, } impl Default for Server { fn default() -> Self { Server { listeners: Vec::with_capacity(1), methods: Arc::new(HashMap::new()), domain: None, disconnect_tx: None, all_conn_done_rx: None, stop_listen_tx: None, } } } impl Server { pub fn new() -> Server { Server::default() } pub fn bind(mut self, host: &str) -> Result { if !self.listeners.is_empty() { return Err(Error::Others( "ttrpc-rust just support 1 host now".to_string(), )); } let (fd, domain) = common::do_bind(host)?; self.domain = Some(domain); common::do_listen(fd)?; self.listeners.push(fd); Ok(self) } pub fn set_domain_unix(mut self) -> Self { self.domain = Some(Domain::Unix); self } #[cfg(target_os = "linux")] pub fn set_domain_vsock(mut self) -> Self { self.domain = Some(Domain::Vsock); self } pub fn add_listener(mut self, fd: RawFd) -> Result { self.listeners.push(fd); Ok(self) } pub fn register_service( mut self, methods: HashMap>, ) -> Server { let mut_methods = Arc::get_mut(&mut self.methods).unwrap(); mut_methods.extend(methods); self } fn get_listenfd(&self) -> Result { if self.listeners.is_empty() { return Err(Error::Others("ttrpc-rust not bind".to_string())); } let listenfd = self.listeners[self.listeners.len() - 1]; Ok(listenfd) } pub async fn start(&mut self) -> Result<()> { let listenfd = self.get_listenfd()?; match self.domain.as_ref() { Some(Domain::Unix) => { let sys_unix_listener; unsafe { sys_unix_listener = SysUnixListener::from_raw_fd(listenfd); } sys_unix_listener .set_nonblocking(true) .map_err(err_to_others_err!(e, "set_nonblocking error "))?; let unix_listener = UnixListener::from_std(sys_unix_listener) .map_err(err_to_others_err!(e, "from_std error "))?; let incoming = UnixIncoming::new(unix_listener); self.do_start(listenfd, incoming).await } #[cfg(target_os = "linux")] Some(Domain::Vsock) => { let incoming = unsafe { VsockListener::from_raw_fd(listenfd).incoming() }; self.do_start(listenfd, incoming).await } _ => Err(Error::Others( "Domain is not set or not supported".to_string(), )), } } pub async fn do_start(&mut self, listenfd: RawFd, mut incoming: I) -> Result<()> where I: Stream> + Unpin + Send + 'static + AsRawFd, S: AsyncRead + AsyncWrite + AsRawFd + Send + 'static, { let methods = self.methods.clone(); let (disconnect_tx, close_conn_rx) = watch::channel(0); self.disconnect_tx = Some(disconnect_tx); let (conn_done_tx, all_conn_done_rx) = channel::(1); self.all_conn_done_rx = Some(all_conn_done_rx); let (stop_listen_tx, mut stop_listen_rx) = channel(1); self.stop_listen_tx = Some(stop_listen_tx); spawn(async move { loop { select! { conn = incoming.next() => { if let Some(conn) = conn { // Accept a new connection match conn { Ok(stream) => { // spawn a connection handler, would not block spawn_connection_handler( listenfd, stream, methods.clone(), close_conn_rx.clone(), conn_done_tx.clone() ).await; } Err(e) => { error!("{:?}", e) } } } else { break; } } fd_tx = stop_listen_rx.recv() => { if let Some(fd_tx) = fd_tx { // dup fd to keep the listener open // or the listener will be closed when the incoming was dropped. let dup_fd = unistd::dup(incoming.as_raw_fd()).unwrap(); common::set_fd_close_exec(dup_fd).unwrap(); drop(incoming); fd_tx.send(dup_fd).await.unwrap(); break; } } } } drop(conn_done_tx); }); Ok(()) } pub async fn shutdown(&mut self) -> Result<()> { self.stop_listen().await; self.disconnect().await; Ok(()) } pub async fn disconnect(&mut self) { if let Some(tx) = self.disconnect_tx.take() { tx.send(1).ok(); } if let Some(mut rx) = self.all_conn_done_rx.take() { rx.recv().await; } } pub async fn stop_listen(&mut self) { if let Some(tx) = self.stop_listen_tx.take() { let (fd_tx, mut fd_rx) = channel(1); tx.send(fd_tx).await.unwrap(); let fd = fd_rx.recv().await.unwrap(); self.listeners.clear(); self.listeners.push(fd); } } } async fn spawn_connection_handler( listenfd: RawFd, stream: S, methods: Arc>>, mut close_conn_rx: watch::Receiver, conn_done_tx: Sender, ) where S: AsyncRead + AsyncWrite + AsRawFd + Send + 'static, { let (req_done_tx, mut all_req_done_rx) = channel::(1); spawn(async move { let (mut reader, mut writer) = split(stream); let (tx, mut rx): (Sender>, Receiver>) = channel(100); let (client_disconnected_tx, client_disconnected_rx) = watch::channel(false); spawn(async move { while let Some(buf) = rx.recv().await { if let Err(e) = writer.write_all(&buf).await { error!("write_message got error: {:?}", e); } } }); loop { let tx = tx.clone(); let methods = methods.clone(); let req_done_tx2 = req_done_tx.clone(); let mut client_disconnected_rx2 = client_disconnected_rx.clone(); select! { resp = receive(&mut reader) => { match resp { Ok(message) => { spawn(async move { select! { _ = handle_request(tx, listenfd, methods, message) => {} _ = client_disconnected_rx2.changed() => {} } drop(req_done_tx2); }); } Err(e) => { let _ = client_disconnected_tx.send(true); trace!("error {:?}", e); break; } } } v = close_conn_rx.changed() => { // 0 is the init value of this watch, not a valid signal // is_err means the tx was dropped. if v.is_err() || *close_conn_rx.borrow() != 0 { info!("Stop accepting new connections."); break; } } } } drop(req_done_tx); all_req_done_rx.recv().await; drop(conn_done_tx); }); } async fn do_handle_request( fd: RawFd, methods: Arc>>, header: MessageHeader, body: &[u8], ) -> StdResult<(u32, Vec), Status> { let req = utils::body_to_request(body)?; let path = utils::get_path(&req.service, &req.method); let method = methods .get(&path) .ok_or_else(|| get_status(Code::INVALID_ARGUMENT, format!("{} does not exist", &path)))?; let ctx = TtrpcContext { fd, mh: header, metadata: context::from_pb(&req.metadata), timeout_nano: req.timeout_nano, }; let get_unknown_status_and_log_err = |e| { error!("method handle {} got error {:?}", path, &e); get_status(Code::UNKNOWN, e) }; if req.timeout_nano == 0 { method .handler(ctx, req) .await .map_err(get_unknown_status_and_log_err) } else { timeout( Duration::from_nanos(req.timeout_nano as u64), method.handler(ctx, req), ) .await .map_err(|_| { // Timed out error!("method handle {} got error timed out", path); get_status(Code::DEADLINE_EXCEEDED, "timeout") }) .and_then(|r| { // Handler finished r.map_err(get_unknown_status_and_log_err) }) } } async fn handle_request( tx: Sender>, fd: RawFd, methods: Arc>>, message: (MessageHeader, Vec), ) { let (header, body) = message; let stream_id = header.stream_id; if header.type_ != MESSAGE_TYPE_REQUEST { return; } match do_handle_request(fd, methods, header, &body).await { Ok((stream_id, resp_body)) => { if let Err(x) = respond(tx.clone(), stream_id, resp_body).await { error!("respond got error {:?}", x); } } Err(status) => { if let Err(x) = respond_with_status(tx.clone(), stream_id, status).await { error!("respond got error {:?}", x); } } } } impl FromRawFd for Server { unsafe fn from_raw_fd(fd: RawFd) -> Self { Self::default().add_listener(fd).unwrap() } } impl AsRawFd for Server { fn as_raw_fd(&self) -> RawFd { self.listeners[0] } } ttrpc-0.5.2/src/asynchronous/stream.rs000064400000000000000000000104210000000000000161220ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // use byteorder::{BigEndian, ByteOrder}; use crate::common::{MESSAGE_HEADER_LENGTH, MESSAGE_LENGTH_MAX, MESSAGE_TYPE_RESPONSE}; use crate::error::{get_rpc_status, sock_error_msg, Error, Result}; use crate::r#async::utils; use crate::ttrpc::{Code, Response, Status}; use crate::MessageHeader; use protobuf::Message; use tokio::io::AsyncReadExt; async fn receive_count(reader: &mut T, count: usize) -> Result> where T: AsyncReadExt + std::marker::Unpin, { let mut content = vec![0u8; count]; if let Err(e) = reader.read_exact(&mut content).await { return Err(Error::Socket(e.to_string())); } Ok(content) } async fn receive_header(reader: &mut T) -> Result where T: AsyncReadExt + std::marker::Unpin, { let buf = receive_count(reader, MESSAGE_HEADER_LENGTH).await?; let size = buf.len(); if size != MESSAGE_HEADER_LENGTH { return Err(sock_error_msg( size, format!("Message header length {} is too small", size), )); } let mut mh = MessageHeader::default(); let mut covbuf: &[u8] = &buf[..4]; mh.length = byteorder::ReadBytesExt::read_u32::(&mut covbuf) .map_err(err_to_rpc_err!(Code::INVALID_ARGUMENT, e, ""))?; let mut covbuf: &[u8] = &buf[4..8]; mh.stream_id = byteorder::ReadBytesExt::read_u32::(&mut covbuf) .map_err(err_to_rpc_err!(Code::INVALID_ARGUMENT, e, ""))?; mh.type_ = buf[8]; mh.flags = buf[9]; Ok(mh) } pub async fn receive(reader: &mut T) -> Result<(MessageHeader, Vec)> where T: AsyncReadExt + std::marker::Unpin, { let mh = receive_header(reader).await?; trace!("Got Message header {:?}", mh); if mh.length > MESSAGE_LENGTH_MAX as u32 { return Err(get_rpc_status( Code::INVALID_ARGUMENT, format!( "message length {} exceed maximum message size of {}", mh.length, MESSAGE_LENGTH_MAX ), )); } let buf = receive_count(reader, mh.length as usize).await?; let size = buf.len(); if size != mh.length as usize { return Err(sock_error_msg( size, format!("Message length {} is not {}", size, mh.length), )); } trace!("Got Message body {:?}", buf); Ok((mh, buf)) } fn header_to_buf(mh: MessageHeader) -> Vec { let mut buf = vec![0u8; MESSAGE_HEADER_LENGTH]; let mut covbuf: &mut [u8] = &mut buf[..4]; BigEndian::write_u32(&mut covbuf, mh.length); let mut covbuf: &mut [u8] = &mut buf[4..8]; BigEndian::write_u32(&mut covbuf, mh.stream_id); buf[8] = mh.type_; buf[9] = mh.flags; buf } pub fn to_req_buf(stream_id: u32, mut body: Vec) -> Vec { let header = utils::get_request_header_from_body(stream_id, &body); let mut buf = header_to_buf(header); buf.append(&mut body); buf } pub fn to_res_buf(stream_id: u32, mut body: Vec) -> Vec { let header = utils::get_response_header_from_body(stream_id, &body); let mut buf = header_to_buf(header); buf.append(&mut body); buf } fn get_response_body(res: &Response) -> Result> { let mut buf = Vec::with_capacity(res.compute_size() as usize); let mut s = protobuf::CodedOutputStream::vec(&mut buf); res.write_to(&mut s).map_err(err_to_others_err!(e, ""))?; s.flush().map_err(err_to_others_err!(e, ""))?; Ok(buf) } pub async fn respond( tx: tokio::sync::mpsc::Sender>, stream_id: u32, body: Vec, ) -> Result<()> { let buf = to_res_buf(stream_id, body); tx.send(buf) .await .map_err(err_to_others_err!(e, "Send packet to sender error ")) } pub async fn respond_with_status( tx: tokio::sync::mpsc::Sender>, stream_id: u32, status: Status, ) -> Result<()> { let mut res = Response::new(); res.set_status(status); let mut body = get_response_body(&res)?; let mh = MessageHeader { length: body.len() as u32, stream_id, type_: MESSAGE_TYPE_RESPONSE, flags: 0, }; let mut buf = header_to_buf(mh); buf.append(&mut body); tx.send(buf) .await .map_err(err_to_others_err!(e, "Send packet to sender error ")) } ttrpc-0.5.2/src/asynchronous/unix_incoming.rs000064400000000000000000000020060000000000000174750ustar 00000000000000// Copyright (c) 2021 Ant Group // // SPDX-License-Identifier: Apache-2.0 // //! Because Tokio has removed UnixIncoming since version 0.3, //! we define the UnixIncoming and implement the Stream for UnixIncoming. use std::io; use std::os::unix::io::{AsRawFd, RawFd}; use std::pin::Pin; use std::task::{Context, Poll}; use futures::{ready, Stream}; use tokio::net::{UnixListener, UnixStream}; /// Stream of listeners #[derive(Debug)] #[must_use = "streams do nothing unless polled"] pub struct UnixIncoming { inner: UnixListener, } impl UnixIncoming { pub fn new(listener: UnixListener) -> Self { Self { inner: listener } } } impl Stream for UnixIncoming { type Item = io::Result; fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let (socket, _) = ready!(self.inner.poll_accept(cx))?; Poll::Ready(Some(Ok(socket))) } } impl AsRawFd for UnixIncoming { fn as_raw_fd(&self) -> RawFd { self.inner.as_raw_fd() } } ttrpc-0.5.2/src/asynchronous/utils.rs000064400000000000000000000120030000000000000157650ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // use crate::common::{MessageHeader, MESSAGE_TYPE_REQUEST, MESSAGE_TYPE_RESPONSE}; use crate::error::{get_status, Error, Result}; use crate::ttrpc::{Code, Request, Response, Status}; use async_trait::async_trait; use protobuf::{CodedInputStream, Message}; use std::collections::HashMap; use std::os::unix::io::{FromRawFd, RawFd}; use std::result::Result as StdResult; use tokio::net::UnixStream; /// Handle request in async mode. #[macro_export] macro_rules! async_request_handler { ($class: ident, $ctx: ident, $req: ident, $server: ident, $req_type: ident, $req_fn: ident) => { let mut req = super::$server::$req_type::new(); { let mut s = CodedInputStream::from_bytes(&$req.payload); req.merge_from(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; } let mut res = ::ttrpc::Response::new(); match $class.service.$req_fn(&$ctx, req).await { Ok(rep) => { res.set_status(::ttrpc::get_status(::ttrpc::Code::OK, "".to_string())); res.payload.reserve(rep.compute_size() as usize); let mut s = protobuf::CodedOutputStream::vec(&mut res.payload); rep.write_to(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; s.flush().map_err(::ttrpc::Err_to_Others!(e, ""))?; } Err(x) => match x { ::ttrpc::Error::RpcStatus(s) => { res.set_status(s); } _ => { res.set_status(::ttrpc::get_status( ::ttrpc::Code::UNKNOWN, format!("{:?}", x), )); } }, } let buf = ::ttrpc::r#async::convert_response_to_buf(res)?; return Ok(($ctx.mh.stream_id, buf)); }; } /// Send request through async client. #[macro_export] macro_rules! async_client_request { ($self: ident, $ctx: ident, $req: ident, $server: expr, $method: expr, $cres: ident) => { let mut creq = ::ttrpc::Request::new(); creq.set_service($server.to_string()); creq.set_method($method.to_string()); creq.set_timeout_nano($ctx.timeout_nano); let md = ::ttrpc::context::to_pb($ctx.metadata); creq.set_metadata(md); creq.payload.reserve($req.compute_size() as usize); { let mut s = CodedOutputStream::vec(&mut creq.payload); $req.write_to(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; s.flush().map_err(::ttrpc::Err_to_Others!(e, ""))?; } let res = $self.client.request(creq).await?; let mut s = CodedInputStream::from_bytes(&res.payload); $cres .merge_from(&mut s) .map_err(::ttrpc::Err_to_Others!(e, "Unpack get error "))?; return Ok($cres); }; } /// Trait that implements handler which is a proxy to the desired method (async). #[async_trait] pub trait MethodHandler { async fn handler(&self, ctx: TtrpcContext, req: Request) -> Result<(u32, Vec)>; } /// The context of ttrpc (async). #[derive(Debug)] pub struct TtrpcContext { pub fd: std::os::unix::io::RawFd, pub mh: MessageHeader, pub metadata: HashMap>, pub timeout_nano: i64, } pub fn convert_response_to_buf(res: Response) -> Result> { let mut buf = Vec::with_capacity(res.compute_size() as usize); let mut s = protobuf::CodedOutputStream::vec(&mut buf); res.write_to(&mut s).map_err(err_to_others_err!(e, ""))?; s.flush().map_err(err_to_others_err!(e, ""))?; Ok(buf) } pub fn get_response_header_from_body(stream_id: u32, body: &[u8]) -> MessageHeader { MessageHeader { length: body.len() as u32, stream_id, type_: MESSAGE_TYPE_RESPONSE, flags: 0, } } pub fn get_request_header_from_body(stream_id: u32, body: &[u8]) -> MessageHeader { MessageHeader { length: body.len() as u32, stream_id, type_: MESSAGE_TYPE_REQUEST, flags: 0, } } pub fn new_unix_stream_from_raw_fd(fd: RawFd) -> UnixStream { let std_stream: std::os::unix::net::UnixStream; unsafe { std_stream = std::os::unix::net::UnixStream::from_raw_fd(fd); } // Notice: There is a big change between tokio 1.0 and 0.2 // we must set nonblocking by ourselves in tokio 1.0 std_stream.set_nonblocking(true).unwrap(); UnixStream::from_std(std_stream).unwrap() } pub fn body_to_request(body: &[u8]) -> StdResult { let mut req = Request::new(); let merge_result; { let mut s = CodedInputStream::from_bytes(body); merge_result = req.merge_from(&mut s); } if merge_result.is_err() { return Err(get_status(Code::INVALID_ARGUMENT, "".to_string())); } trace!("Got Message request {:?}", req); Ok(req) } pub fn get_path(service: &str, method: &str) -> String { format!("/{}/{}", service, method) } ttrpc-0.5.2/src/common.rs000064400000000000000000000117370000000000000133770ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // //! Common functions and macros. #![allow(unused_macros)] use crate::error::{Error, Result}; use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag}; use nix::sys::socket::*; use std::os::unix::io::RawFd; #[derive(Debug)] pub enum Domain { Unix, #[cfg(target_os = "linux")] Vsock, } /// Message header of ttrpc. #[derive(Default, Debug)] pub struct MessageHeader { pub length: u32, pub stream_id: u32, pub type_: u8, pub flags: u8, } pub fn do_listen(listener: RawFd) -> Result<()> { if let Err(e) = fcntl(listener, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)) { return Err(Error::Others(format!( "failed to set listener fd: {} as non block: {}", listener, e ))); } listen(listener, 10).map_err(|e| Error::Socket(e.to_string())) } pub fn parse_host(host: &str) -> Result<(Domain, Vec<&str>)> { let hostv: Vec<&str> = host.trim().split("://").collect(); if hostv.len() != 2 { return Err(Error::Others(format!("Host {} is not right", host))); } let domain = match &hostv[0].to_lowercase()[..] { "unix" => Domain::Unix, #[cfg(target_os = "linux")] "vsock" => Domain::Vsock, x => return Err(Error::Others(format!("Scheme {:?} is not supported", x))), }; Ok((domain, hostv)) } pub fn set_fd_close_exec(fd: RawFd) -> Result { if let Err(e) = fcntl(fd, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)) { return Err(Error::Others(format!( "failed to set fd: {} as close-on-exec: {}", fd, e ))); } Ok(fd) } // SOCK_CLOEXEC flag is Linux specific #[cfg(target_os = "linux")] pub(crate) const SOCK_CLOEXEC: SockFlag = SockFlag::SOCK_CLOEXEC; #[cfg(not(target_os = "linux"))] pub(crate) const SOCK_CLOEXEC: SockFlag = SockFlag::empty(); #[cfg(target_os = "linux")] fn make_addr(host: &str) -> Result { UnixAddr::new_abstract(host.as_bytes()).map_err(err_to_others_err!(e, "")) } #[cfg(not(target_os = "linux"))] fn make_addr(host: &str) -> Result { UnixAddr::new(host).map_err(err_to_others_err!(e, "")) } fn make_socket(addr: (&str, u32)) -> Result<(RawFd, Domain, SockAddr)> { let (host, _) = addr; let (domain, hostv) = parse_host(host)?; let sockaddr: SockAddr; let fd: RawFd; match domain { Domain::Unix => { fd = socket(AddressFamily::Unix, SockType::Stream, SOCK_CLOEXEC, None) .map_err(|e| Error::Socket(e.to_string()))?; // MacOS doesn't support atomic creation of a socket descriptor with SOCK_CLOEXEC flag, // so there is a chance of leak if fork + exec happens in between of these calls. #[cfg(target_os = "macos")] set_fd_close_exec(fd)?; sockaddr = SockAddr::Unix(make_addr(hostv[1])?); } #[cfg(target_os = "linux")] Domain::Vsock => { let host_port_v: Vec<&str> = hostv[1].split(':').collect(); if host_port_v.len() != 2 { return Err(Error::Others(format!( "Host {} is not right for vsock", host ))); } let port: u32 = host_port_v[1] .parse() .expect("the vsock port is not an number"); fd = socket( AddressFamily::Vsock, SockType::Stream, SockFlag::SOCK_CLOEXEC, None, ) .map_err(|e| Error::Socket(e.to_string()))?; let cid = addr.1; sockaddr = SockAddr::new_vsock(cid, port); } }; Ok((fd, domain, sockaddr)) } // Vsock is not supported on non Linux. #[cfg(target_os = "linux")] use libc::VMADDR_CID_ANY; #[cfg(not(target_os = "linux"))] const VMADDR_CID_ANY: u32 = 0; #[cfg(target_os = "linux")] use libc::VMADDR_CID_HOST; #[cfg(not(target_os = "linux"))] const VMADDR_CID_HOST: u32 = 0; pub fn do_bind(host: &str) -> Result<(RawFd, Domain)> { let (fd, domain, sockaddr) = make_socket((host, VMADDR_CID_ANY))?; setsockopt(fd, sockopt::ReusePort, &true)?; bind(fd, &sockaddr).map_err(err_to_others_err!(e, ""))?; Ok((fd, domain)) } /// Creates a unix socket for client. pub(crate) unsafe fn client_connect(host: &str) -> Result { let (fd, _, sockaddr) = make_socket((host, VMADDR_CID_HOST))?; connect(fd, &sockaddr)?; Ok(fd) } macro_rules! cfg_sync { ($($item:item)*) => { $( #[cfg(feature = "sync")] #[cfg_attr(docsrs, doc(cfg(feature = "sync")))] $item )* } } macro_rules! cfg_async { ($($item:item)*) => { $( #[cfg(feature = "async")] #[cfg_attr(docsrs, doc(cfg(feature = "async")))] $item )* } } pub const MESSAGE_HEADER_LENGTH: usize = 10; pub const MESSAGE_LENGTH_MAX: usize = 4 << 20; pub const MESSAGE_TYPE_REQUEST: u8 = 0x1; pub const MESSAGE_TYPE_RESPONSE: u8 = 0x2; ttrpc-0.5.2/src/context.rs000064400000000000000000000107430000000000000135670ustar 00000000000000// Copyright (c) 2021 Ant group // // SPDX-License-Identifier: Apache-2.0 // use crate::ttrpc::KeyValue; use std::collections::HashMap; #[derive(Clone, Default, Debug)] pub struct Context { pub metadata: HashMap>, pub timeout_nano: i64, } pub fn with_timeout(i: i64) -> Context { Context { timeout_nano: i, ..Default::default() } } pub fn with_metadata(md: HashMap>) -> Context { Context { metadata: md, ..Default::default() } } impl Context { // appends additional values to the given key. pub fn add(&mut self, key: String, value: String) { if let Some(ref mut vl) = self.metadata.get_mut(&key) { vl.push(value); } else { self.metadata.insert(key.to_lowercase(), vec![value]); } } // Set sets the provided values for a given key. // The values will overwrite any existing values. // If no values provided, a key will be deleted. pub fn set(&mut self, key: String, value: Vec) { if value.is_empty() { self.metadata.remove(&key); } else { self.metadata.insert(key.to_lowercase(), value); } } } pub fn from_pb(kvs: &protobuf::RepeatedField) -> HashMap> { let mut meta: HashMap> = HashMap::new(); for kv in kvs { if let Some(ref mut vl) = meta.get_mut(&kv.key) { vl.push(kv.value.clone()); } else { meta.insert(kv.key.clone(), vec![kv.value.clone()]); } } meta } pub fn to_pb(kvs: HashMap>) -> protobuf::RepeatedField { let mut meta: protobuf::RepeatedField = protobuf::RepeatedField::default(); for (k, vl) in kvs { for v in vl { let key = KeyValue { key: k.clone(), value: v.clone(), ..Default::default() }; meta.push(key); } } meta } #[cfg(test)] mod tests { use crate::context; use crate::ttrpc::KeyValue; #[test] fn test_metadata() { // RepeatedField -> HashMap, test from_pb() let mut src: protobuf::RepeatedField = protobuf::RepeatedField::default(); for i in &[ ("key1", "value1-1"), ("key1", "value1-2"), ("key2", "value2"), ] { let key = KeyValue { key: i.0.to_string(), value: i.1.to_string(), ..Default::default() }; src.push(key); } let dst = context::from_pb(&src); assert_eq!(dst.len(), 2); assert_eq!( dst.get("key1"), Some(&vec!["value1-1".to_string(), "value1-2".to_string()]) ); assert_eq!(dst.get("key2"), Some(&vec!["value2".to_string()])); assert_eq!(dst.get("key3"), None); // HashMap -> RepeatedField , test to_pb() let src = context::to_pb(dst); let mut kvs = src.into_vec(); kvs.sort_by(|a, b| a.key.partial_cmp(&b.key).unwrap()); assert_eq!(kvs.len(), 3); assert_eq!(kvs[0].key, "key1"); assert_eq!(kvs[0].value, "value1-1"); assert_eq!(kvs[1].key, "key1"); assert_eq!(kvs[1].value, "value1-2"); assert_eq!(kvs[2].key, "key2"); assert_eq!(kvs[2].value, "value2"); } #[test] fn test_context() { let ctx: context::Context = Default::default(); assert_eq!(0, ctx.timeout_nano); assert_eq!(ctx.metadata.len(), 0); let mut ctx = context::with_timeout(99); assert_eq!(99, ctx.timeout_nano); assert_eq!(ctx.metadata.len(), 0); ctx.add("key1".to_string(), "value1-1".to_string()); assert_eq!(ctx.metadata.len(), 1); assert_eq!( ctx.metadata.get("key1"), Some(&vec!["value1-1".to_string()]) ); ctx.add("key1".to_string(), "value1-2".to_string()); assert_eq!(ctx.metadata.len(), 1); assert_eq!( ctx.metadata.get("key1"), Some(&vec!["value1-1".to_string(), "value1-2".to_string()]) ); ctx.set("key2".to_string(), vec!["value2".to_string()]); assert_eq!(ctx.metadata.len(), 2); assert_eq!(ctx.metadata.get("key2"), Some(&vec!["value2".to_string()])); ctx.set("key1".to_string(), vec![]); assert_eq!(ctx.metadata.len(), 1); assert_eq!(ctx.metadata.get("key1"), None); } } ttrpc-0.5.2/src/error.rs000064400000000000000000000042010000000000000132240ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Error and Result of ttrpc and relevant functions, macros. use crate::ttrpc::{Code, Status}; use std::result; use thiserror::Error; /// The error type for ttrpc. #[derive(Error, Debug)] pub enum Error { #[error("socket err: {0}")] Socket(String), #[error("rpc status: {0:?}")] RpcStatus(Status), #[error("Nix error: {0}")] Nix(#[from] nix::Error), #[error("ttrpc err: {0}")] Others(String), } /// A specialized Result type for ttrpc. pub type Result = result::Result; /// Get ttrpc::Status from ttrpc::Code and a message. pub fn get_status(c: Code, msg: impl ToString) -> Status { let mut status = Status::new(); status.set_code(c); status.set_message(msg.to_string()); status } pub fn get_rpc_status(c: Code, msg: impl ToString) -> Error { Error::RpcStatus(get_status(c, msg)) } const SOCK_DICONNECTED: &str = "socket disconnected"; pub fn sock_error_msg(size: usize, msg: String) -> Error { if size == 0 { return Error::Socket(SOCK_DICONNECTED.to_string()); } get_rpc_status(Code::INVALID_ARGUMENT, msg) } macro_rules! err_to_rpc_err { ($c: expr, $e: ident, $s: expr) => { |$e| get_rpc_status($c, $s.to_string() + &$e.to_string()) }; } macro_rules! err_to_others_err { ($e: ident, $s: expr) => { |$e| Error::Others($s.to_string() + &$e.to_string()) }; } /// Convert to ttrpc::Error::Others. #[macro_export] macro_rules! Err_to_Others { ($e: ident, $s: expr) => { |$e| ::ttrpc::Error::Others($s.to_string() + &$e.to_string()) }; } ttrpc-0.5.2/src/lib.rs000064400000000000000000000042330000000000000126460ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! ttrpc-rust is a **non-core** subproject of containerd //! //! `ttrpc-rust` is the Rust version of [ttrpc](https://github.com/containerd/ttrpc). [ttrpc](https://github.com/containerd/ttrpc) is GRPC for low-memory environments. //! //! Example: //! //! Check [this](https://github.com/containerd/ttrpc-rust/tree/master/example) //! //! # Feature flags //! //! - `async`: Enables async server and client. //! - `sync`: Enables traditional sync server and client (default enabled). //! - `protobuf-codec`: Includes rust-protobuf (default enabled). #![cfg_attr(docsrs, feature(doc_cfg))] #[macro_use] extern crate log; #[macro_use] pub mod error; #[macro_use] pub mod common; #[allow(soft_unstable, clippy::type_complexity, clippy::too_many_arguments)] mod compiled { include!(concat!(env!("OUT_DIR"), "/mod.rs")); } #[doc(inline)] pub use compiled::ttrpc; pub mod context; #[doc(inline)] pub use crate::common::MessageHeader; #[doc(inline)] pub use crate::error::{get_status, Error, Result}; #[doc(inline)] pub use crate::ttrpc::{Code, Request, Response, Status}; cfg_sync! { pub mod sync; #[doc(inline)] pub use crate::sync::channel::{write_message}; #[doc(inline)] pub use crate::sync::utils::{response_to_channel, MethodHandler, TtrpcContext}; #[doc(inline)] pub use crate::sync::client; #[doc(inline)] pub use crate::sync::client::Client; #[doc(inline)] pub use crate::sync::server; #[doc(inline)] pub use crate::sync::server::Server; } cfg_async! { pub mod asynchronous; #[doc(hidden)] pub use crate::asynchronous as r#async; } ttrpc-0.5.2/src/sync/channel.rs000064400000000000000000000113420000000000000144630ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use byteorder::{BigEndian, ByteOrder, ReadBytesExt}; use nix::sys::socket::*; use std::os::unix::io::RawFd; use crate::common::{MESSAGE_HEADER_LENGTH, MESSAGE_LENGTH_MAX}; use crate::error::{get_rpc_status, sock_error_msg, Error, Result}; use crate::ttrpc::Code; use crate::MessageHeader; fn retryable(e: nix::Error) -> bool { use ::nix::{errno::Errno, Error}; e == Error::from_errno(Errno::EINTR) || e == Error::from_errno(Errno::EAGAIN) } fn read_count(fd: RawFd, count: usize) -> Result> { let mut v: Vec = vec![0; count]; let mut len = 0; if count == 0 { return Ok(v.to_vec()); } loop { match recv(fd, &mut v[len..], MsgFlags::empty()) { Ok(l) => { len += l; // when socket peer closed, it would return 0. if len == count || l == 0 { break; } } Err(e) if retryable(e) => { // Should retry } Err(e) => { return Err(Error::Socket(e.to_string())); } } } Ok(v[0..len].to_vec()) } fn write_count(fd: RawFd, buf: &[u8], count: usize) -> Result { let mut len = 0; if count == 0 { return Ok(0); } loop { match send(fd, &buf[len..], MsgFlags::empty()) { Ok(l) => { len += l; if len == count { break; } } Err(e) if retryable(e) => { // Should retry } Err(e) => { return Err(Error::Socket(e.to_string())); } } } Ok(len) } fn read_message_header(fd: RawFd) -> Result { let buf = read_count(fd, MESSAGE_HEADER_LENGTH)?; let size = buf.len(); if size != MESSAGE_HEADER_LENGTH { return Err(sock_error_msg( size, format!("Message header length {} is too small", size), )); } let mut mh = MessageHeader::default(); let mut covbuf: &[u8] = &buf[..4]; mh.length = covbuf .read_u32::() .map_err(err_to_rpc_err!(Code::INVALID_ARGUMENT, e, ""))?; let mut covbuf: &[u8] = &buf[4..8]; mh.stream_id = covbuf .read_u32::() .map_err(err_to_rpc_err!(Code::INVALID_ARGUMENT, e, ""))?; mh.type_ = buf[8]; mh.flags = buf[9]; Ok(mh) } pub fn read_message(fd: RawFd) -> Result<(MessageHeader, Vec)> { let mh = read_message_header(fd)?; trace!("Got Message header {:?}", mh); if mh.length > MESSAGE_LENGTH_MAX as u32 { return Err(get_rpc_status( Code::INVALID_ARGUMENT, format!( "message length {} exceed maximum message size of {}", mh.length, MESSAGE_LENGTH_MAX ), )); } let buf = read_count(fd, mh.length as usize)?; let size = buf.len(); if size != mh.length as usize { return Err(sock_error_msg( size, format!("Message length {} is not {}", size, mh.length), )); } trace!("Got Message body {:?}", buf); Ok((mh, buf)) } fn write_message_header(fd: RawFd, mh: MessageHeader) -> Result<()> { let mut buf = [0u8; MESSAGE_HEADER_LENGTH]; let mut covbuf: &mut [u8] = &mut buf[..4]; BigEndian::write_u32(&mut covbuf, mh.length); let mut covbuf: &mut [u8] = &mut buf[4..8]; BigEndian::write_u32(&mut covbuf, mh.stream_id); buf[8] = mh.type_; buf[9] = mh.flags; let size = write_count(fd, &buf, MESSAGE_HEADER_LENGTH)?; if size != MESSAGE_HEADER_LENGTH { return Err(sock_error_msg( size, format!("Send Message header length size {} is not right", size), )); } Ok(()) } pub fn write_message(fd: RawFd, mh: MessageHeader, buf: Vec) -> Result<()> { write_message_header(fd, mh)?; let size = write_count(fd, &buf, buf.len())?; if size != buf.len() { return Err(sock_error_msg( size, format!("Send Message length size {} is not right", size), )); } Ok(()) } ttrpc-0.5.2/src/sync/client.rs000064400000000000000000000216400000000000000143330ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Sync client of ttrpc. use nix::sys::socket::*; use nix::unistd::close; use protobuf::{CodedInputStream, CodedOutputStream, Message}; use std::collections::HashMap; use std::os::unix::io::RawFd; use std::sync::mpsc; use std::sync::{Arc, Mutex}; use std::{io, thread}; #[cfg(target_os = "macos")] use crate::common::set_fd_close_exec; use crate::common::{client_connect, MESSAGE_TYPE_REQUEST, MESSAGE_TYPE_RESPONSE, SOCK_CLOEXEC}; use crate::error::{Error, Result}; use crate::sync::channel::{read_message, write_message}; use crate::ttrpc::{Code, Request, Response}; use crate::MessageHeader; use std::time::Duration; type Sender = mpsc::Sender<(Vec, mpsc::SyncSender>>)>; type Receiver = mpsc::Receiver<(Vec, mpsc::SyncSender>>)>; /// A ttrpc Client (sync). #[derive(Clone)] pub struct Client { fd: RawFd, sender_tx: Sender, client_close: Arc, } impl Client { pub fn connect(path: &str) -> Result { let fd = unsafe { client_connect(path)? }; Ok(Self::new(fd)) } /// Initialize a new [`Client`] from raw file descriptor. pub fn new(fd: RawFd) -> Client { let (sender_tx, rx): (Sender, Receiver) = mpsc::channel(); let (recver_fd, close_fd) = socketpair(AddressFamily::Unix, SockType::Stream, None, SOCK_CLOEXEC).unwrap(); // MacOS doesn't support descriptor creation with SOCK_CLOEXEC automically, // so there is a chance of leak if fork + exec happens in between of these calls. #[cfg(target_os = "macos")] { set_fd_close_exec(recver_fd).unwrap(); set_fd_close_exec(close_fd).unwrap(); } let client_close = Arc::new(ClientClose { fd, close_fd }); let recver_map_orig = Arc::new(Mutex::new(HashMap::new())); //Sender let recver_map = recver_map_orig.clone(); thread::spawn(move || { let mut stream_id: u32 = 1; for (buf, recver_tx) in rx.iter() { let current_stream_id = stream_id; stream_id += 2; //Put current_stream_id and recver_tx to recver_map { let mut map = recver_map.lock().unwrap(); map.insert(current_stream_id, recver_tx.clone()); } let mh = MessageHeader { length: buf.len() as u32, stream_id: current_stream_id, type_: MESSAGE_TYPE_REQUEST, flags: 0, }; if let Err(e) = write_message(fd, mh, buf) { //Remove current_stream_id and recver_tx to recver_map { let mut map = recver_map.lock().unwrap(); map.remove(¤t_stream_id); } recver_tx .send(Err(e)) .unwrap_or_else(|_e| error!("The request has returned")); } } trace!("Sender quit"); }); //Recver thread::spawn(move || { let mut pollers = vec![ libc::pollfd { fd: recver_fd, events: libc::POLLIN, revents: 0, }, libc::pollfd { fd, events: libc::POLLIN, revents: 0, }, ]; loop { let returned = unsafe { let pollers: &mut [libc::pollfd] = &mut pollers; libc::poll( pollers as *mut _ as *mut libc::pollfd, pollers.len() as _, -1, ) }; if returned == -1 { let err = io::Error::last_os_error(); if err.raw_os_error() == Some(libc::EINTR) { continue; } error!("fatal error in process reaper:{}", err); break; } else if returned < 1 { continue; } if pollers[0].revents != 0 { break; } if pollers[pollers.len() - 1].revents == 0 { continue; } let mh; let buf; match read_message(fd) { Ok((x, y)) => { mh = x; buf = y; } Err(x) => match x { Error::Socket(y) => { trace!("Socket error {}", y); let mut map = recver_map_orig.lock().unwrap(); for (_, recver_tx) in map.iter_mut() { recver_tx .send(Err(Error::Socket(format!("socket error {}", y)))) .unwrap_or_else(|e| { error!("The request has returned error {:?}", e) }); } map.clear(); break; } _ => { trace!("Others error {:?}", x); continue; } }, }; let mut map = recver_map_orig.lock().unwrap(); let recver_tx = match map.get(&mh.stream_id) { Some(tx) => tx, None => { debug!("Recver got unknown packet {:?} {:?}", mh, buf); continue; } }; if mh.type_ != MESSAGE_TYPE_RESPONSE { recver_tx .send(Err(Error::Others(format!( "Recver got malformed packet {:?} {:?}", mh, buf )))) .unwrap_or_else(|_e| error!("The request has returned")); continue; } recver_tx .send(Ok(buf)) .unwrap_or_else(|_e| error!("The request has returned")); map.remove(&mh.stream_id); } let _ = close(recver_fd).map_err(|e| { warn!( "failed to close recver_fd: {} with error: {:?}", recver_fd, e ) }); trace!("Recver quit"); }); Client { fd, sender_tx, client_close, } } pub fn request(&self, req: Request) -> Result { let mut buf = Vec::with_capacity(req.compute_size() as usize); let mut s = CodedOutputStream::vec(&mut buf); req.write_to(&mut s).map_err(err_to_others_err!(e, ""))?; s.flush().map_err(err_to_others_err!(e, ""))?; let (tx, rx) = mpsc::sync_channel(0); self.sender_tx .send((buf, tx)) .map_err(err_to_others_err!(e, "Send packet to sender error "))?; let result: Result>; if req.timeout_nano == 0 { result = rx .recv() .map_err(err_to_others_err!(e, "Receive packet from recver error: "))?; } else { result = rx .recv_timeout(Duration::from_nanos(req.timeout_nano as u64)) .map_err(err_to_others_err!( e, "Receive packet from recver timeout: " ))?; } let buf = result?; let mut s = CodedInputStream::from_bytes(&buf); let mut res = Response::new(); res.merge_from(&mut s) .map_err(err_to_others_err!(e, "Unpack response error "))?; let status = res.get_status(); if status.get_code() != Code::OK { return Err(Error::RpcStatus((*status).clone())); } Ok(res) } } struct ClientClose { fd: RawFd, close_fd: RawFd, } impl Drop for ClientClose { fn drop(&mut self) { close(self.close_fd).unwrap(); close(self.fd).unwrap(); trace!("All client is droped"); } } ttrpc-0.5.2/src/sync/mod.rs000064400000000000000000000004430000000000000136320ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // //! Server and Client in sync mode. #[macro_use] pub mod channel; pub mod client; // TODO: address this after merging linters #[allow(clippy::too_many_arguments)] pub mod server; #[macro_use] pub mod utils; ttrpc-0.5.2/src/sync/server.rs000064400000000000000000000527670000000000000144010ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Sync server of ttrpc. use nix::sys::socket::{self, *}; use nix::unistd::*; use protobuf::{CodedInputStream, Message}; use std::collections::HashMap; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::mpsc::{channel, sync_channel, Receiver, Sender, SyncSender}; use std::sync::{Arc, Mutex}; use std::thread::JoinHandle; use std::{io, thread}; #[cfg(not(target_os = "linux"))] use crate::common::set_fd_close_exec; use crate::common::{self, MESSAGE_TYPE_REQUEST}; use crate::context; use crate::error::{get_status, Error, Result}; use crate::sync::channel::{read_message, write_message}; use crate::ttrpc::{Code, Request, Response}; use crate::MessageHeader; use crate::{response_to_channel, MethodHandler, TtrpcContext}; // poll_queue will create WAIT_THREAD_COUNT_DEFAULT threads in begin. // If wait thread count < WAIT_THREAD_COUNT_MIN, create number to WAIT_THREAD_COUNT_DEFAULT. // If wait thread count > WAIT_THREAD_COUNT_MAX, wait thread will quit to WAIT_THREAD_COUNT_DEFAULT. const DEFAULT_WAIT_THREAD_COUNT_DEFAULT: usize = 3; const DEFAULT_WAIT_THREAD_COUNT_MIN: usize = 1; const DEFAULT_WAIT_THREAD_COUNT_MAX: usize = 5; type MessageSender = Sender<(MessageHeader, Vec)>; type MessageReceiver = Receiver<(MessageHeader, Vec)>; /// A ttrpc Server (sync). pub struct Server { listeners: Vec, monitor_fd: (RawFd, RawFd), listener_quit_flag: Arc, connections: Arc>>, methods: Arc>>, handler: Option>, reaper: Option<(Sender, JoinHandle<()>)>, thread_count_default: usize, thread_count_min: usize, thread_count_max: usize, } struct Connection { fd: RawFd, quit: Arc, handler: Option>, } impl Connection { fn close(&self) { self.quit.store(true, Ordering::SeqCst); // in case the connection had closed socket::shutdown(self.fd, Shutdown::Read).unwrap_or(()); } } struct ThreadS<'a> { fd: RawFd, fdlock: &'a Arc>, wtc: &'a Arc, quit: &'a Arc, methods: &'a Arc>>, res_tx: &'a MessageSender, control_tx: &'a SyncSender<()>, default: usize, min: usize, max: usize, } fn start_method_handler_thread( fd: RawFd, fdlock: Arc>, wtc: Arc, quit: Arc, methods: Arc>>, res_tx: MessageSender, control_tx: SyncSender<()>, min: usize, max: usize, ) { thread::spawn(move || { while !quit.load(Ordering::SeqCst) { let c = wtc.fetch_add(1, Ordering::SeqCst) + 1; if c > max { wtc.fetch_sub(1, Ordering::SeqCst); break; } let result; { let _guard = fdlock.lock().unwrap(); if quit.load(Ordering::SeqCst) { // notify the connection dealing main thread to stop. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); break; } result = read_message(fd); } if quit.load(Ordering::SeqCst) { // notify the connection dealing main thread to stop. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); break; } let c = wtc.fetch_sub(1, Ordering::SeqCst) - 1; if c < min { trace!("notify client handler to create much more worker threads!"); control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); } let mh; let buf; match result { Ok((x, y)) => { mh = x; buf = y; } Err(x) => match x { Error::Socket(y) => { trace!("Socket error {}", y); quit.store(true, Ordering::SeqCst); // the client connection would be closed and // the connection dealing main thread would // have exited. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); trace!("Socket error send control_tx"); break; } _ => { trace!("Others error {:?}", x); continue; } }, } if mh.type_ != MESSAGE_TYPE_REQUEST { continue; } let mut s = CodedInputStream::from_bytes(&buf); let mut req = Request::new(); if let Err(x) = req.merge_from(&mut s) { let status = get_status(Code::INVALID_ARGUMENT, x.to_string()); let mut res = Response::new(); res.set_status(status); if let Err(x) = response_to_channel(mh.stream_id, res, res_tx.clone()) { debug!("response_to_channel get error {:?}", x); quit.store(true, Ordering::SeqCst); // the client connection would be closed and // the connection dealing main thread would have // exited. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); break; } continue; } trace!("Got Message request {:?}", req); let path = format!("/{}/{}", req.service, req.method); let method; if let Some(x) = methods.get(&path) { method = x; } else { let status = get_status(Code::INVALID_ARGUMENT, format!("{} does not exist", path)); let mut res = Response::new(); res.set_status(status); if let Err(x) = response_to_channel(mh.stream_id, res, res_tx.clone()) { info!("response_to_channel get error {:?}", x); quit.store(true, Ordering::SeqCst); // the client connection would be closed and // the connection dealing main thread would have // exited. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); break; } continue; } let ctx = TtrpcContext { fd, mh, res_tx: res_tx.clone(), metadata: context::from_pb(&req.metadata), timeout_nano: req.timeout_nano, }; if let Err(x) = method.handler(ctx, req) { debug!("method handle {} get error {:?}", path, x); quit.store(true, Ordering::SeqCst); // the client connection would be closed and // the connection dealing main thread would have // exited. control_tx .send(()) .unwrap_or_else(|err| debug!("Failed to send {:?}", err)); break; } } }); } fn start_method_handler_threads(num: usize, ts: &ThreadS) { for _ in 0..num { if ts.quit.load(Ordering::SeqCst) { break; } start_method_handler_thread( ts.fd, ts.fdlock.clone(), ts.wtc.clone(), ts.quit.clone(), ts.methods.clone(), ts.res_tx.clone(), ts.control_tx.clone(), ts.min, ts.max, ); } } fn check_method_handler_threads(ts: &ThreadS) { let c = ts.wtc.load(Ordering::SeqCst); if c < ts.min { start_method_handler_threads(ts.default - c, ts); } } impl Default for Server { fn default() -> Self { Server { listeners: Vec::with_capacity(1), monitor_fd: (-1, -1), listener_quit_flag: Arc::new(AtomicBool::new(false)), connections: Arc::new(Mutex::new(HashMap::new())), methods: Arc::new(HashMap::new()), handler: None, reaper: None, thread_count_default: DEFAULT_WAIT_THREAD_COUNT_DEFAULT, thread_count_min: DEFAULT_WAIT_THREAD_COUNT_MIN, thread_count_max: DEFAULT_WAIT_THREAD_COUNT_MAX, } } } impl Server { pub fn new() -> Server { Server::default() } pub fn bind(mut self, host: &str) -> Result { if !self.listeners.is_empty() { return Err(Error::Others( "ttrpc-rust just support 1 host now".to_string(), )); } let (fd, _) = common::do_bind(host)?; common::do_listen(fd)?; self.listeners.push(fd); Ok(self) } pub fn add_listener(mut self, fd: RawFd) -> Result { self.listeners.push(fd); Ok(self) } pub fn register_service( mut self, methods: HashMap>, ) -> Server { let mut_methods = Arc::get_mut(&mut self.methods).unwrap(); mut_methods.extend(methods); self } pub fn set_thread_count_default(mut self, count: usize) -> Server { self.thread_count_default = count; self } pub fn set_thread_count_min(mut self, count: usize) -> Server { self.thread_count_min = count; self } pub fn set_thread_count_max(mut self, count: usize) -> Server { self.thread_count_max = count; self } pub fn start_listen(&mut self) -> Result<()> { let connections = self.connections.clone(); if self.listeners.is_empty() { return Err(Error::Others("ttrpc-rust not bind".to_string())); } self.listener_quit_flag.store(false, Ordering::SeqCst); #[cfg(target_os = "linux")] let fds = pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; #[cfg(not(target_os = "linux"))] let fds = { let (rfd, wfd) = pipe()?; set_fd_close_exec(rfd)?; set_fd_close_exec(wfd)?; (rfd, wfd) }; self.monitor_fd = fds; let listener = self.listeners[0]; let methods = self.methods.clone(); let default = self.thread_count_default; let min = self.thread_count_min; let max = self.thread_count_max; let listener_quit_flag = self.listener_quit_flag.clone(); let monitor_fd = self.monitor_fd.0; let reaper_tx = match self.reaper.take() { None => { let reaper_connections = connections.clone(); let (reaper_tx, reaper_rx) = channel(); let reaper_handler = thread::Builder::new() .name("reaper".into()) .spawn(move || { for fd in reaper_rx.iter() { reaper_connections .lock() .unwrap() .remove(&fd) .map(|mut cn| { cn.handler.take().map(|handler| { handler.join().unwrap(); close(fd).unwrap(); }) }); } info!("reaper thread exited"); }) .unwrap(); self.reaper = Some((reaper_tx.clone(), reaper_handler)); reaper_tx } Some(r) => { let reaper_tx = r.0.clone(); self.reaper = Some(r); reaper_tx } }; let handler = thread::Builder::new() .name("listener_loop".into()) .spawn(move || { let mut pollers = vec![ libc::pollfd { fd: monitor_fd, events: libc::POLLIN, revents: 0, }, libc::pollfd { fd: listener, events: libc::POLLIN, revents: 0, }, ]; loop { if listener_quit_flag.load(Ordering::SeqCst) { info!("listener shutdown for quit flag"); break; } let returned = unsafe { let pollers: &mut [libc::pollfd] = &mut pollers; libc::poll( pollers as *mut _ as *mut libc::pollfd, pollers.len() as _, -1, ) }; if returned == -1 { let err = io::Error::last_os_error(); if err.raw_os_error() == Some(libc::EINTR) { continue; } error!("fatal error in listener_loop:{:?}", err); break; } else if returned < 1 { continue; } if pollers[0].revents != 0 || pollers[pollers.len() - 1].revents == 0 { continue; } if listener_quit_flag.load(Ordering::SeqCst) { info!("listener shutdown for quit flag"); break; } #[cfg(target_os = "linux")] let fd = match accept4(listener, SockFlag::SOCK_CLOEXEC) { Ok(fd) => fd, Err(e) => { error!("failed to accept error {:?}", e); break; } }; // Non Linux platforms do not support accept4 with SOCK_CLOEXEC flag, so instead // use accept and call fcntl separately to set SOCK_CLOEXEC. // Because of this there is chance of the descriptor leak if fork + exec happens in between. #[cfg(not(target_os = "linux"))] let fd = match accept(listener) { Ok(fd) => { if let Err(err) = set_fd_close_exec(fd) { error!("fcntl failed after accept: {:?}", err); break; }; fd } Err(e) => { error!("failed to accept error {:?}", e); break; } }; let methods = methods.clone(); let quit = Arc::new(AtomicBool::new(false)); let child_quit = quit.clone(); let reaper_tx_child = reaper_tx.clone(); let handler = thread::Builder::new() .name("client_handler".into()) .spawn(move || { debug!("Got new client"); // Start response thread let quit_res = child_quit.clone(); let (res_tx, res_rx): (MessageSender, MessageReceiver) = channel(); let handler = thread::spawn(move || { for r in res_rx.iter() { trace!("response thread get {:?}", r); if let Err(e) = write_message(fd, r.0, r.1) { error!("write_message got {:?}", e); quit_res.store(true, Ordering::SeqCst); break; } } trace!("response thread quit"); }); let (control_tx, control_rx): (SyncSender<()>, Receiver<()>) = sync_channel(0); let ts = ThreadS { fd, fdlock: &Arc::new(Mutex::new(())), wtc: &Arc::new(AtomicUsize::new(0)), methods: &methods, res_tx: &res_tx, control_tx: &control_tx, quit: &child_quit, default, min, max, }; start_method_handler_threads(ts.default, &ts); while !child_quit.load(Ordering::SeqCst) { check_method_handler_threads(&ts); if control_rx.recv().is_err() { break; } } // drop the control_rx, thus all of the method handler threads would // terminated. drop(control_rx); // drop the res_tx, thus the res_rx would get terminated notification. drop(res_tx); handler.join().unwrap_or(()); // client_handler should not close fd before exit // , which prevent fd reuse issue. reaper_tx_child.send(fd).unwrap(); debug!("client thread quit"); }) .unwrap(); let mut cns = connections.lock().unwrap(); cns.insert( fd, Connection { fd, handler: Some(handler), quit: quit.clone(), }, ); } // end loop // notify reaper thread to exit. drop(reaper_tx); info!("ttrpc server listener stopped"); }) .unwrap(); self.handler = Some(handler); info!("server listen started"); Ok(()) } pub fn start(&mut self) -> Result<()> { if self.thread_count_default >= self.thread_count_max { return Err(Error::Others( "thread_count_default should smaller than thread_count_max".to_string(), )); } if self.thread_count_default <= self.thread_count_min { return Err(Error::Others( "thread_count_default should biger than thread_count_min".to_string(), )); } self.start_listen()?; info!("server started"); Ok(()) } pub fn stop_listen(mut self) -> Self { self.listener_quit_flag.store(true, Ordering::SeqCst); close(self.monitor_fd.1).unwrap_or_else(|e| { warn!( "failed to close notify fd: {} with error: {}", self.monitor_fd.1, e ) }); info!("close monitor"); if let Some(handler) = self.handler.take() { handler.join().unwrap(); } info!("listener thread stopped"); self } pub fn disconnect(mut self) { info!("begin to shutdown connection"); let connections = self.connections.lock().unwrap(); for (_fd, c) in connections.iter() { c.close(); } // release connections's lock, since the following handler.join() // would wait on the other thread's exit in which would take the lock. drop(connections); info!("connections closed"); if let Some(r) = self.reaper.take() { drop(r.0); r.1.join().unwrap(); } info!("reaper thread stopped"); } pub fn shutdown(self) { self.stop_listen().disconnect(); } } impl FromRawFd for Server { unsafe fn from_raw_fd(fd: RawFd) -> Self { Self::default().add_listener(fd).unwrap() } } impl AsRawFd for Server { fn as_raw_fd(&self) -> RawFd { self.listeners[0] } } ttrpc-0.5.2/src/sync/utils.rs000064400000000000000000000073110000000000000142140ustar 00000000000000// Copyright (c) 2020 Ant Financial // // SPDX-License-Identifier: Apache-2.0 // use crate::common::{MessageHeader, MESSAGE_TYPE_RESPONSE}; use crate::error::{Error, Result}; use crate::ttrpc::{Request, Response}; use protobuf::Message; use std::collections::HashMap; /// Response message through a channel. /// Eventually the message will sent to Client. pub fn response_to_channel( stream_id: u32, res: Response, tx: std::sync::mpsc::Sender<(MessageHeader, Vec)>, ) -> Result<()> { let mut buf = Vec::with_capacity(res.compute_size() as usize); let mut s = protobuf::CodedOutputStream::vec(&mut buf); res.write_to(&mut s).map_err(err_to_others_err!(e, ""))?; s.flush().map_err(err_to_others_err!(e, ""))?; let mh = MessageHeader { length: buf.len() as u32, stream_id, type_: MESSAGE_TYPE_RESPONSE, flags: 0, }; tx.send((mh, buf)).map_err(err_to_others_err!(e, ""))?; Ok(()) } /// Handle request in sync mode. #[macro_export] macro_rules! request_handler { ($class: ident, $ctx: ident, $req: ident, $server: ident, $req_type: ident, $req_fn: ident) => { let mut s = CodedInputStream::from_bytes(&$req.payload); let mut req = super::$server::$req_type::new(); req.merge_from(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; let mut res = ::ttrpc::Response::new(); match $class.service.$req_fn(&$ctx, req) { Ok(rep) => { res.set_status(::ttrpc::get_status(::ttrpc::Code::OK, "".to_string())); res.payload.reserve(rep.compute_size() as usize); let mut s = protobuf::CodedOutputStream::vec(&mut res.payload); rep.write_to(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; s.flush().map_err(::ttrpc::Err_to_Others!(e, ""))?; } Err(x) => match x { ::ttrpc::Error::RpcStatus(s) => { res.set_status(s); } _ => { res.set_status(::ttrpc::get_status( ::ttrpc::Code::UNKNOWN, format!("{:?}", x), )); } }, } ::ttrpc::response_to_channel($ctx.mh.stream_id, res, $ctx.res_tx)? }; } /// Send request through sync client. #[macro_export] macro_rules! client_request { ($self: ident, $ctx: ident, $req: ident, $server: expr, $method: expr, $cres: ident) => { let mut creq = ::ttrpc::Request::new(); creq.set_service($server.to_string()); creq.set_method($method.to_string()); creq.set_timeout_nano($ctx.timeout_nano); let md = ::ttrpc::context::to_pb($ctx.metadata); creq.set_metadata(md); creq.payload.reserve($req.compute_size() as usize); let mut s = CodedOutputStream::vec(&mut creq.payload); $req.write_to(&mut s) .map_err(::ttrpc::Err_to_Others!(e, ""))?; s.flush().map_err(::ttrpc::Err_to_Others!(e, ""))?; let res = $self.client.request(creq)?; let mut s = CodedInputStream::from_bytes(&res.payload); $cres .merge_from(&mut s) .map_err(::ttrpc::Err_to_Others!(e, "Unpack get error "))?; }; } /// The context of ttrpc (sync). #[derive(Debug)] pub struct TtrpcContext { pub fd: std::os::unix::io::RawFd, pub mh: MessageHeader, pub res_tx: std::sync::mpsc::Sender<(MessageHeader, Vec)>, pub metadata: HashMap>, pub timeout_nano: i64, } /// Trait that implements handler which is a proxy to the desired method (sync). pub trait MethodHandler { fn handler(&self, ctx: TtrpcContext, req: Request) -> Result<()>; } ttrpc-0.5.2/src/ttrpc.proto000064400000000000000000000222250000000000000137540ustar 00000000000000// Copyright (c) 2019 Ant Financial // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. syntax = "proto3"; package grpc; message Request { string service = 1; string method = 2; bytes payload = 3; int64 timeout_nano = 4; repeated KeyValue metadata = 5; } message KeyValue { string key = 1; string value = 2; } // Get from github.com/gogo/protobuf/protobuf/google/protobuf/any.proto message Any { // A URL/resource name that uniquely identifies the type of the serialized // protocol buffer message. The last segment of the URL's path must represent // the fully qualified name of the type (as in // `path/google.protobuf.Duration`). The name should be in a canonical form // (e.g., leading "." is not accepted). // // In practice, teams usually precompile into the binary all types that they // expect it to use in the context of Any. However, for URLs which use the // scheme `http`, `https`, or no scheme, one can optionally set up a type // server that maps type URLs to message definitions as follows: // // * If no scheme is provided, `https` is assumed. // * An HTTP GET on the URL must yield a [google.protobuf.Type][] // value in binary format, or produce an error. // * Applications are allowed to cache lookup results based on the // URL, or have them precompiled into a binary to avoid any // lookup. Therefore, binary compatibility needs to be preserved // on changes to types. (Use versioned type names to manage // breaking changes.) // // Note: this functionality is not currently available in the official // protobuf release, and it is not used for type URLs beginning with // type.googleapis.com. // // Schemes other than `http`, `https` (or the empty scheme) might be // used with implementation specific semantics. // string type_url = 1; // Must be a valid serialized protocol buffer of the above specified type. bytes value = 2; } // Get from github.com/gogo/googleapis/google/rpc/code.proto // The canonical error codes for Google APIs. // // // Sometimes multiple error codes may apply. Services should return // the most specific error code that applies. For example, prefer // `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. // Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. enum Code { // Not an error; returned on success // // HTTP Mapping: 200 OK OK = 0; // The operation was cancelled, typically by the caller. // // HTTP Mapping: 499 Client Closed Request CANCELLED = 1; // Unknown error. For example, this error may be returned when // a `Status` value received from another address space belongs to // an error space that is not known in this address space. Also // errors raised by APIs that do not return enough error information // may be converted to this error. // // HTTP Mapping: 500 Internal Server Error UNKNOWN = 2; // The client specified an invalid argument. Note that this differs // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments // that are problematic regardless of the state of the system // (e.g., a malformed file name). // // HTTP Mapping: 400 Bad Request INVALID_ARGUMENT = 3; // The deadline expired before the operation could complete. For operations // that change the state of the system, this error may be returned // even if the operation has completed successfully. For example, a // successful response from a server could have been delayed long // enough for the deadline to expire. // // HTTP Mapping: 504 Gateway Timeout DEADLINE_EXCEEDED = 4; // Some requested entity (e.g., file or directory) was not found. // // Note to server developers: if a request is denied for an entire class // of users, such as gradual feature rollout or undocumented whitelist, // `NOT_FOUND` may be used. If a request is denied for some users within // a class of users, such as user-based access control, `PERMISSION_DENIED` // must be used. // // HTTP Mapping: 404 Not Found NOT_FOUND = 5; // The entity that a client attempted to create (e.g., file or directory) // already exists. // // HTTP Mapping: 409 Conflict ALREADY_EXISTS = 6; // The caller does not have permission to execute the specified // operation. `PERMISSION_DENIED` must not be used for rejections // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` // instead for those errors). `PERMISSION_DENIED` must not be // used if the caller can not be identified (use `UNAUTHENTICATED` // instead for those errors). This error code does not imply the // request is valid or the requested entity exists or satisfies // other pre-conditions. // // HTTP Mapping: 403 Forbidden PERMISSION_DENIED = 7; // The request does not have valid authentication credentials for the // operation. // // HTTP Mapping: 401 Unauthorized UNAUTHENTICATED = 16; // Some resource has been exhausted, perhaps a per-user quota, or // perhaps the entire file system is out of space. // // HTTP Mapping: 429 Too Many Requests RESOURCE_EXHAUSTED = 8; // The operation was rejected because the system is not in a state // required for the operation's execution. For example, the directory // to be deleted is non-empty, an rmdir operation is applied to // a non-directory, etc. // // Service implementors can use the following guidelines to decide // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: // (a) Use `UNAVAILABLE` if the client can retry just the failing call. // (b) Use `ABORTED` if the client should retry at a higher level // (e.g., when a client-specified test-and-set fails, indicating the // client should restart a read-modify-write sequence). // (c) Use `FAILED_PRECONDITION` if the client should not retry until // the system state has been explicitly fixed. E.g., if an "rmdir" // fails because the directory is non-empty, `FAILED_PRECONDITION` // should be returned since the client should not retry unless // the files are deleted from the directory. // // HTTP Mapping: 400 Bad Request FAILED_PRECONDITION = 9; // The operation was aborted, typically due to a concurrency issue such as // a sequencer check failure or transaction abort. // // See the guidelines above for deciding between `FAILED_PRECONDITION`, // `ABORTED`, and `UNAVAILABLE`. // // HTTP Mapping: 409 Conflict ABORTED = 10; // The operation was attempted past the valid range. E.g., seeking or // reading past end-of-file. // // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may // be fixed if the system state changes. For example, a 32-bit file // system will generate `INVALID_ARGUMENT` if asked to read at an // offset that is not in the range [0,2^32-1], but it will generate // `OUT_OF_RANGE` if asked to read from an offset past the current // file size. // // There is a fair bit of overlap between `FAILED_PRECONDITION` and // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific // error) when it applies so that callers who are iterating through // a space can easily look for an `OUT_OF_RANGE` error to detect when // they are done. // // HTTP Mapping: 400 Bad Request OUT_OF_RANGE = 11; // The operation is not implemented or is not supported/enabled in this // service. // // HTTP Mapping: 501 Not Implemented UNIMPLEMENTED = 12; // Internal errors. This means that some invariants expected by the // underlying system have been broken. This error code is reserved // for serious errors. // // HTTP Mapping: 500 Internal Server Error INTERNAL = 13; // The service is currently unavailable. This is most likely a // transient condition, which can be corrected by retrying with // a backoff. // // See the guidelines above for deciding between `FAILED_PRECONDITION`, // `ABORTED`, and `UNAVAILABLE`. // // HTTP Mapping: 503 Service Unavailable UNAVAILABLE = 14; // Unrecoverable data loss or corruption. // // HTTP Mapping: 500 Internal Server Error DATA_LOSS = 15; } // Get from github.com/gogo/googleapis/google/rpc/status.proto message Status { // The status code, which should be an enum value of // [google.rpc.Code][google.rpc.Code]. Code code = 1; // A developer-facing error message, which should be in English. Any // user-facing error message should be localized and sent in the // [google.rpc.Status.details][google.rpc.Status.details] field, or localized // by the client. string message = 2; // A list of messages that carry the error details. There is a common set of // message types for APIs to use. repeated Any details = 3; } message Response { Status status = 1; bytes payload = 2; }