-
Notifications
You must be signed in to change notification settings - Fork 172
/
resource_limiting.rs
176 lines (158 loc) · 7.06 KB
/
resource_limiting.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// Copyright 2019-2021 Parity Technologies (UK) Ltd.
//
// Permission is hereby granted, free of charge, to any
// person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the
// Software without restriction, including without
// limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software
// is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice
// shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//! # Resource Limiting
//!
//! This module handles limiting the capacity of the server to respond to requests.
//!
//! `jsonrpsee` is agnostic about the types of resources available on the server, and the units used are arbitrary.
//! The units are used to model the availability of a resource, be it something mundane like CPU or Memory,
//! or more exotic things like remote API access to a 3rd party service, or use of some external hardware
//! that's under the control of the server.
//!
//! To get the most out of this feature, we suggest benchmarking individual methods to see how many resources they
//! consume, in particular anything critical that is expected to result in a lot of stress on the server,
//! and then defining your units such that the limits (`capacity`) can be adjusted for different hardware configurations.
//!
//! Up to 8 resources can be defined using the [`WsServerBuilder::register_resource`](../../../jsonrpsee_ws_server/struct.WsServerBuilder.html#method.register_resource)
//! or [`HttpServerBuilder::register_resource`](../../../jsonrpsee_ws_server/struct.WsServerBuilder.html#method.register_resource) method
//! for the WebSocket and HTTP server respectively.
//!
//! Each method will claim the specified number of units (or the default) for the duration of its execution.
//! Any method execution that would cause the total sum of claimed resource units to exceed
//! the `capacity` of that resource will be denied execution, immediately returning JSON-RPC error object with code `-32604`.
//!
//! Setting the execution cost to `0` equates to the method effectively not being limited by a given resource. Likewise setting the
//! `capacity` to `0` disables any limiting for a given resource.
//!
//! To specify a different than default number of units a method should use, use the `resources` argument in the
//! `#[method]` attribute:
//!
//! ```
//! # use jsonrpsee::{types::RpcResult, proc_macros::rpc};
//! #
//! #[rpc(server)]
//! pub trait Rpc {
//! #[method(name = "my_expensive_method", resources("cpu" = 5, "mem" = 2))]
//! async fn my_expensive_method(&self) -> RpcResult<&'static str> {
//! // Do work
//! Ok("hello")
//! }
//! }
//! ```
//!
//! Alternatively, you can use the `resource` method when creating a module manually without the help of the macro:
//!
//! ```
//! # use jsonrpsee::{RpcModule, types::RpcResult};
//! #
//! # fn main() -> RpcResult<()> {
//! #
//! let mut module = RpcModule::new(());
//!
//! module
//! .register_async_method("my_expensive_method", |_, _| async move {
//! // Do work
//! Ok("hello")
//! })?
//! .resource("cpu", 5)?
//! .resource("mem", 2)?;
//! # Ok(())
//! # }
//! ```
//!
//! Each resource needs to have a unique name, such as `"cpu"` or `"memory"`, which can then be used across all
//! [`RpcModule`s](crate::server::rpc_module::RpcModule). In case a module definition uses a resource label not
//! defined on the server, starting the server with such a module will result in a runtime error containing the
//! information about the offending method.
use std::sync::Arc;
use arrayvec::ArrayVec;
use jsonrpsee_types::error::Error;
use parking_lot::Mutex;
// The number of kinds of resources that can be used for limiting.
const RESOURCE_COUNT: usize = 8;
/// Fixed size table, mapping a resource to a (unitless) value indicating the amount of the resource that is available to RPC calls.
pub type ResourceTable = [u16; RESOURCE_COUNT];
/// Variable size table, mapping a resource to a (unitless) value indicating the amount of the resource that is available to RPC calls.
pub type ResourceVec<T> = ArrayVec<T, RESOURCE_COUNT>;
/// User defined resources available to be used by calls on the JSON-RPC server.
/// Each of the 8 possible resource kinds, for instance "cpu", "io", "nanobots",
/// store a maximum `capacity` and a default. A value of `0` means no limits for the given resource.
#[derive(Debug, Default, Clone)]
pub struct Resources {
/// Resources currently in use by executing calls. 0 for unused resource kinds.
totals: Arc<Mutex<ResourceTable>>,
/// Max capacity for all resource kinds
pub capacities: ResourceTable,
/// Default value for all resource kinds; unless a method has a resource limit defined, this is the cost of a call (0 means no default limit)
pub defaults: ResourceTable,
/// Labels for every registered resource
pub labels: ResourceVec<&'static str>,
}
impl Resources {
/// Register a new resource kind. Errors if `label` is already registered, or if the total number of
/// registered resources would exceed 8.
pub fn register(&mut self, label: &'static str, capacity: u16, default: u16) -> Result<(), Error> {
if self.labels.iter().any(|&l| l == label) {
return Err(Error::ResourceNameAlreadyTaken(label));
}
let idx = self.labels.len();
self.labels.try_push(label).map_err(|_| Error::MaxResourcesReached)?;
self.capacities[idx] = capacity;
self.defaults[idx] = default;
Ok(())
}
/// Attempt to claim `units` units for each resource, incrementing current totals.
/// If successful, returns a [`ResourceGuard`] which decrements the totals by the same
/// amounts once dropped.
pub fn claim(&self, units: ResourceTable) -> Result<ResourceGuard, Error> {
let mut totals = self.totals.lock();
let mut sum = *totals;
for (idx, sum) in sum.iter_mut().enumerate() {
match sum.checked_add(units[idx]) {
Some(s) if s <= self.capacities[idx] => *sum = s,
_ => {
let label = self.labels.get(idx).copied().unwrap_or("<UNKNOWN>");
return Err(Error::ResourceAtCapacity(label));
}
}
}
*totals = sum;
Ok(ResourceGuard { totals: self.totals.clone(), units })
}
}
/// RAII style "lock" for claimed resources, will automatically release them once dropped.
#[derive(Debug)]
pub struct ResourceGuard {
totals: Arc<Mutex<ResourceTable>>,
units: ResourceTable,
}
impl Drop for ResourceGuard {
fn drop(&mut self) {
for (sum, claimed) in self.totals.lock().iter_mut().zip(self.units) {
*sum -= claimed;
}
}
}