surfpool_core/rpc/admin.rs
1use jsonrpc_core::{BoxFuture, Error, ErrorCode, Result};
2use jsonrpc_derive::rpc;
3use solana_client::rpc_custom_error::RpcCustomError;
4use surfpool_types::SimnetCommand;
5
6use super::RunloopContext;
7use crate::{PluginInfo, rpc::State, surfnet::PluginCommand};
8
9#[rpc]
10pub trait AdminRpc {
11 type Metadata;
12
13 /// Immediately shuts down the RPC server.
14 ///
15 /// This administrative endpoint is typically used during controlled shutdowns of the validator
16 /// or service exposing the RPC interface. It allows remote administrators to gracefully terminate
17 /// the process, stopping all RPC activity.
18 ///
19 /// ## Returns
20 /// - [`Result<()>`] — A unit result indicating successful shutdown, or an error if the call fails.
21 ///
22 /// ## Example Request (JSON-RPC)
23 /// ```json
24 /// {
25 /// "jsonrpc": "2.0",
26 /// "id": 42,
27 /// "method": "exit",
28 /// "params": []
29 /// }
30 /// ```
31 ///
32 /// # Notes
33 /// - This method is privileged and should only be accessible to trusted clients.
34 /// - Use with extreme caution in production environments.
35 /// - If successful, the RPC server process will terminate immediately after processing this call.
36 ///
37 /// # Security
38 /// Access to this method should be tightly restricted. Implement proper authorization mechanisms
39 /// via the RPC metadata to prevent accidental or malicious use.
40 #[rpc(meta, name = "exit")]
41 fn exit(&self, meta: Self::Metadata) -> Result<()>;
42
43 #[rpc(meta, name = "reloadPlugin")]
44 fn reload_plugin(
45 &self,
46 meta: Self::Metadata,
47 name: String,
48 config_file: String,
49 ) -> BoxFuture<Result<()>>;
50
51 /// Reloads a runtime plugin with new configuration.
52 ///
53 /// This administrative endpoint is used to dynamically reload a plugin without restarting
54 /// the entire RPC server or validator. It is useful for applying updated configurations
55 /// to a plugin that supports hot-reloading.
56 ///
57 /// ## Parameters
58 /// - `name`: The identifier of the plugin to reload.
59 /// - `config_file`: Path to the new configuration file to load for the plugin.
60 ///
61 /// ## Returns
62 /// - [`BoxFuture<Result<()>>`] — A future resolving to a unit result on success, or an error if
63 /// reloading fails.
64 ///
65 /// ## Example Request (JSON-RPC)
66 /// ```json
67 /// {
68 /// "jsonrpc": "2.0",
69 /// "id": 101,
70 /// "method": "reloadPlugin",
71 /// "params": ["myPlugin", "/etc/plugins/my_plugin_config.toml"]
72 /// }
73 /// ```
74 ///
75 /// # Notes
76 /// - The plugin must support reloading in order for this to succeed.
77 /// - A failed reload will leave the plugin in its previous state.
78 /// - This method is intended for administrators and should be properly secured.
79 ///
80 /// # Security
81 /// Ensure only trusted clients can invoke this method. Use metadata-based access control to limit exposure.
82 #[rpc(meta, name = "unloadPlugin")]
83 fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>>;
84
85 /// Dynamically loads a new plugin into the runtime from a configuration file.
86 ///
87 /// This administrative endpoint is used to add a new plugin to the system at runtime,
88 /// based on the configuration provided. It enables extensibility without restarting
89 /// the validator or RPC server.
90 ///
91 /// ## Parameters
92 /// - `config_file`: Path to the plugin's configuration file, which defines its behavior and settings.
93 ///
94 /// ## Returns
95 /// - [`BoxFuture<Result<String>>`] — A future resolving to the name or identifier of the loaded plugin,
96 /// or an error if the plugin could not be loaded.
97 ///
98 /// ## Example Request (JSON-RPC)
99 /// ```json
100 /// {
101 /// "jsonrpc": "2.0",
102 /// "id": 102,
103 /// "method": "loadPlugin",
104 /// "params": ["/etc/plugins/my_plugin_config.toml"]
105 /// }
106 /// ```
107 ///
108 /// # Notes
109 /// - The plugin system must be initialized and support runtime loading.
110 /// - The config file should be well-formed and point to a valid plugin implementation.
111 /// - Duplicate plugin names may lead to conflicts or errors.
112 ///
113 /// # Security
114 /// This method should be restricted to administrators only. Validate inputs and use access control.
115 #[rpc(meta, name = "loadPlugin")]
116 fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>>;
117
118 /// Returns a list of all currently loaded plugin names.
119 ///
120 /// This administrative RPC method is used to inspect which plugins have been successfully
121 /// loaded into the runtime. It can be useful for debugging or operational monitoring.
122 ///
123 /// ## Returns
124 /// - `Vec<PluginInfo>` — A list of plugin information objects, each containing:
125 /// - `plugin_name`: The name of the plugin (e.g., "surfpool-subgraph")
126 /// - `uuid`: The unique identifier of the plugin instance
127 ///
128 /// ## Example Request (JSON-RPC)
129 /// ```json
130 /// {
131 /// "jsonrpc": "2.0",
132 /// "id": 103,
133 /// "method": "listPlugins",
134 /// "params": []
135 /// }
136 /// ```
137 ///
138 /// ## Example Response
139 /// ```json
140 /// {
141 /// "jsonrpc": "2.0",
142 /// "result": [
143 /// {
144 /// "plugin_name": "surfpool-subgraph",
145 /// "uuid": "550e8400-e29b-41d4-a716-446655440000"
146 /// }
147 /// ],
148 /// "id": 103
149 /// }
150 /// ```
151 ///
152 /// # Notes
153 /// - Only plugins that have been successfully loaded will appear in this list.
154 /// - This method is read-only and safe to call frequently.
155 #[rpc(meta, name = "listPlugins")]
156 fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>>;
157
158 /// Returns the system start time.
159 ///
160 /// This RPC method retrieves the timestamp of when the system was started, represented as
161 /// a `SystemTime`. It can be useful for measuring uptime or for tracking the system's runtime
162 /// in logs or monitoring systems.
163 ///
164 /// ## Returns
165 /// - `SystemTime` — The timestamp representing when the system was started.
166 ///
167 /// ## Example Request (JSON-RPC)
168 /// ```json
169 /// {
170 /// "jsonrpc": "2.0",
171 /// "id": 106,
172 /// "method": "startTime",
173 /// "params": []
174 /// }
175 /// ```
176 ///
177 /// ## Example Response
178 /// ```json
179 /// {
180 /// "jsonrpc": "2.0",
181 /// "result": "2025-04-24T12:34:56Z",
182 /// "id": 106
183 /// }
184 /// ```
185 ///
186 /// # Notes
187 /// - The result is a `String` in UTC, reflecting the moment the system was initialized.
188 /// - This method is useful for monitoring system uptime and verifying system health.
189 #[rpc(meta, name = "startTime")]
190 fn start_time(&self, meta: Self::Metadata) -> Result<String>;
191}
192
193pub struct SurfpoolAdminRpc;
194impl AdminRpc for SurfpoolAdminRpc {
195 type Metadata = Option<RunloopContext>;
196
197 fn exit(&self, meta: Self::Metadata) -> Result<()> {
198 let Some(ctx) = meta else {
199 return Err(RpcCustomError::NodeUnhealthy {
200 num_slots_behind: None,
201 }
202 .into());
203 };
204
205 let _ = ctx
206 .simnet_commands_tx
207 .send(SimnetCommand::Terminate(ctx.id));
208
209 Ok(())
210 }
211
212 fn reload_plugin(
213 &self,
214 meta: Self::Metadata,
215 name: String,
216 config_file: String,
217 ) -> BoxFuture<Result<()>> {
218 Box::pin(async move {
219 let Some(ctx) = meta else {
220 return Err(RpcCustomError::NodeUnhealthy {
221 num_slots_behind: None,
222 }
223 .into());
224 };
225 let (tx, rx) = crossbeam_channel::bounded(1);
226 ctx.plugin_commands_tx
227 .send(PluginCommand::Reload {
228 name,
229 config_file,
230 response_tx: tx,
231 })
232 .map_err(|_| Error::internal_error())?;
233 rx.recv()
234 .map_err(|_| Error::internal_error())?
235 .map_err(|e| Error {
236 code: ErrorCode::InternalError,
237 message: e,
238 data: None,
239 })
240 })
241 }
242
243 fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>> {
244 Box::pin(async move {
245 let Some(ctx) = meta else {
246 return Err(RpcCustomError::NodeUnhealthy {
247 num_slots_behind: None,
248 }
249 .into());
250 };
251 let (tx, rx) = crossbeam_channel::bounded(1);
252 ctx.plugin_commands_tx
253 .send(PluginCommand::Unload {
254 name,
255 response_tx: tx,
256 })
257 .map_err(|_| Error::internal_error())?;
258 rx.recv()
259 .map_err(|_| Error::internal_error())?
260 .map_err(|e| Error {
261 code: ErrorCode::InternalError,
262 message: e,
263 data: None,
264 })
265 })
266 }
267
268 fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>> {
269 Box::pin(async move {
270 let Some(ctx) = meta else {
271 return Err(RpcCustomError::NodeUnhealthy {
272 num_slots_behind: None,
273 }
274 .into());
275 };
276 let (tx, rx) = crossbeam_channel::bounded(1);
277 ctx.plugin_commands_tx
278 .send(PluginCommand::Load {
279 config_file,
280 response_tx: tx,
281 })
282 .map_err(|_| Error::internal_error())?;
283 rx.recv()
284 .map_err(|_| Error::internal_error())?
285 .map(|info| info.plugin_name)
286 .map_err(|e| Error {
287 code: ErrorCode::InternalError,
288 message: e,
289 data: None,
290 })
291 })
292 }
293
294 fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>> {
295 Box::pin(async move {
296 let Some(ctx) = meta else {
297 return Err(RpcCustomError::NodeUnhealthy {
298 num_slots_behind: None,
299 }
300 .into());
301 };
302 let (tx, rx) = crossbeam_channel::bounded(1);
303 ctx.plugin_commands_tx
304 .send(PluginCommand::List { response_tx: tx })
305 .map_err(|_| Error::internal_error())?;
306 Ok(rx.recv().map_err(|_| Error::internal_error())?)
307 })
308 }
309
310 fn start_time(&self, meta: Self::Metadata) -> Result<String> {
311 let svm_locker = meta.get_svm_locker()?;
312 let system_time = svm_locker.get_start_time();
313
314 let datetime_utc: chrono::DateTime<chrono::Utc> = system_time.into();
315 Ok(datetime_utc.to_rfc3339())
316 }
317}