volo-thrift 0.12.2

Thrift RPC framework implementation of volo.
Documentation
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Multi-service router for volo-thrift.
//!
//! This module provides a [`Router`] that allows a single server to handle
//! multiple Thrift services, routing requests based on the IDL service name
//! (`isn`) field in TTHeader.
//!
//! # Overview
//!
//! The multi-service feature enables serving multiple Thrift services from a single
//! server instance. Requests are routed based on the `isn` (IDL Service Name) field
//! in the TTHeader protocol.
//!
//! # Routing Rules
//!
//! 1. If the request has an `isn` that matches a registered service, route to that service
//! 2. If the request has no `isn` or an unknown `isn`, route to the default service
//! 3. If there's no default service and no match, return an error
//!
//! # Example
//!
//! ```ignore
//! use volo_thrift::server::{Router, Server};
//!
//! // Create service implementations
//! let hello_service = HelloServiceServer { inner: HelloImpl };
//! let echo_service = EchoServiceServer { inner: EchoImpl };
//!
//! // Build the router
//! let router = Router::new()
//!     .with_default_service(hello_service)  // Default for requests without ISN
//!     .add_service(echo_service);           // Routes when ISN = "EchoService"
//!
//! // Run the server
//! Server::with_router(router)
//!     .run(addr)
//!     .await?;
//! ```
//!
//! # Client-side ISN
//!
//! Clients can specify the target service by setting the `isn` field in metainfo:
//!
//! ```ignore
//! use volo_thrift::codec::default::ttheader::HEADER_IDL_SERVICE_NAME;
//!
//! metainfo::METAINFO.with(|mi| {
//!     mi.borrow_mut().set_persistent(
//!         HEADER_IDL_SERVICE_NAME.into(),
//!         "EchoService".into(),
//!     );
//! });
//! ```

use ahash::AHashMap;
use motore::{BoxCloneService, service::Service};
use pilota::thrift::{ApplicationException, ApplicationExceptionKind};
use volo::FastStr;

use crate::{
    Bytes, ServerError,
    context::{ServerContext, ThriftContext},
};

/// A trait to provide a static reference to the service's name.
///
/// This is used for routing services within the router.
/// The name should match the service name defined in the Thrift IDL.
///
/// # Example
///
/// ```ignore
/// impl NamedService for MyServiceServer<S> {
///     const NAME: &'static str = "MyService";
/// }
/// ```
pub trait NamedService {
    /// The service name as defined in the Thrift IDL.
    const NAME: &'static str;
}

type BoxedService = BoxCloneService<ServerContext, Bytes, Bytes, ServerError>;

/// A router for multiple Thrift services.
///
/// The router dispatches requests to the appropriate service based on the
/// IDL service name (`isn`) field in TTHeader. If no `isn` is present or
/// the service name is not found, the request is routed to the default service.
///
/// # Example
///
/// ```ignore
/// use volo_thrift::server::{Router, Server};
///
/// let service_a = volo_gen::a::ServiceAServer::new(ImplA);
/// let service_b = volo_gen::b::ServiceBServer::new(ImplB);
///
/// let router = Router::new()
///     .with_default_service(service_a)  // Default service (handles requests without ISN)
///     .add_service(service_b);          // Additional service
///
/// Server::with_router(router)
///     .run(addr)
///     .await?;
/// ```
pub struct Router {
    services: AHashMap<FastStr, BoxedService>,
    default_service: Option<BoxedService>,
}

impl Default for Router {
    #[inline]
    fn default() -> Self {
        Self::new()
    }
}

impl Clone for Router {
    #[inline]
    fn clone(&self) -> Self {
        Self {
            services: self.services.clone(),
            default_service: self.default_service.clone(),
        }
    }
}

impl Router {
    /// Creates a new empty router.
    pub fn new() -> Self {
        Self {
            services: AHashMap::new(),
            default_service: None,
        }
    }

    /// Sets the default service for the router.
    ///
    /// The default service handles requests that either:
    /// - Don't have an `isn` field in TTHeader
    /// - Have an `isn` that doesn't match any registered service
    ///
    /// The service is also registered by its name for explicit routing.
    pub fn with_default_service<S>(mut self, service: S) -> Self
    where
        S: Service<ServerContext, Bytes, Response = Bytes, Error = ServerError>
            + NamedService
            + Clone
            + Send
            + Sync
            + 'static,
    {
        let name = FastStr::from_static_str(S::NAME);
        let boxed = BoxCloneService::new(service);
        self.default_service = Some(boxed.clone());
        self.services.insert(name, boxed);
        self
    }

    /// Adds a service to the router.
    ///
    /// The service will be routed to when the `isn` field in TTHeader
    /// matches the service's name (from [`NamedService::NAME`]).
    pub fn add_service<S>(mut self, service: S) -> Self
    where
        S: Service<ServerContext, Bytes, Response = Bytes, Error = ServerError>
            + NamedService
            + Clone
            + Send
            + Sync
            + 'static,
    {
        let name = FastStr::from_static_str(S::NAME);
        self.services.insert(name, BoxCloneService::new(service));
        self
    }

    /// Returns the number of registered services.
    pub fn service_count(&self) -> usize {
        self.services.len()
    }

    /// Returns whether the router has a default service.
    pub fn has_default_service(&self) -> bool {
        self.default_service.is_some()
    }
}

impl Service<ServerContext, Bytes> for Router {
    type Response = Bytes;
    type Error = ServerError;

    #[inline]
    async fn call(
        &self,
        cx: &mut ServerContext,
        payload: Bytes,
    ) -> Result<Self::Response, Self::Error> {
        // Get the IDL service name from context (set by TTHeader decoder)
        let service_name = cx.idl_service_name();

        let service = match service_name {
            Some(name) => self.services.get(name).or(self.default_service.as_ref()),
            None => self.default_service.as_ref(),
        };

        match service {
            Some(svc) => svc.call(cx, payload).await,
            None => Err(ServerError::Application(ApplicationException::new(
                ApplicationExceptionKind::UNKNOWN_METHOD,
                format!(
                    "service not found: {:?}",
                    service_name.map(|s: &FastStr| s.as_str())
                ),
            ))),
        }
    }
}

impl std::fmt::Debug for Router {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Router")
            .field("services", &self.services.keys().collect::<Vec<_>>())
            .field("has_default_service", &self.default_service.is_some())
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A mock service that returns a fixed response with its name
    #[derive(Clone)]
    struct MockService {
        name: &'static str,
    }

    impl NamedService for MockService {
        const NAME: &'static str = "MockService";
    }

    impl Service<ServerContext, Bytes> for MockService {
        type Response = Bytes;
        type Error = ServerError;

        async fn call(
            &self,
            _cx: &mut ServerContext,
            _payload: Bytes,
        ) -> Result<Self::Response, Self::Error> {
            Ok(Bytes::from(self.name))
        }
    }

    /// Another mock service with a different name
    #[derive(Clone)]
    struct AnotherMockService;

    impl NamedService for AnotherMockService {
        const NAME: &'static str = "AnotherService";
    }

    impl Service<ServerContext, Bytes> for AnotherMockService {
        type Response = Bytes;
        type Error = ServerError;

        async fn call(
            &self,
            _cx: &mut ServerContext,
            _payload: Bytes,
        ) -> Result<Self::Response, Self::Error> {
            Ok(Bytes::from("another"))
        }
    }

    #[test]
    fn test_router_new() {
        let router = Router::new();
        assert_eq!(router.service_count(), 0);
        assert!(!router.has_default_service());
    }

    #[test]
    fn test_router_with_default_service() {
        let router = Router::new().with_default_service(MockService { name: "default" });
        assert_eq!(router.service_count(), 1);
        assert!(router.has_default_service());
    }

    #[test]
    fn test_router_add_service() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);
        assert_eq!(router.service_count(), 2);
        assert!(router.has_default_service());
    }

    #[tokio::test]
    async fn test_router_routes_by_isn() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let mut cx = ServerContext::default();
        // Set ISN to "AnotherService"
        cx.set_idl_service_name(FastStr::from_static_str("AnotherService"));

        let result = router.call(&mut cx, Bytes::new()).await.unwrap();
        assert_eq!(result, Bytes::from("another"));
    }

    #[tokio::test]
    async fn test_router_routes_to_default_without_isn() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let mut cx = ServerContext::default();
        // No ISN set

        let result = router.call(&mut cx, Bytes::new()).await.unwrap();
        assert_eq!(result, Bytes::from("default"));
    }

    #[tokio::test]
    async fn test_router_routes_to_default_with_unknown_isn() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let mut cx = ServerContext::default();
        // Set ISN to unknown service
        cx.set_idl_service_name(FastStr::from_static_str("UnknownService"));

        let result = router.call(&mut cx, Bytes::new()).await.unwrap();
        assert_eq!(result, Bytes::from("default"));
    }

    #[tokio::test]
    async fn test_router_error_no_service_found() {
        let router = Router::new().add_service(AnotherMockService);
        // No default service

        let mut cx = ServerContext::default();
        // Set ISN to unknown service
        cx.set_idl_service_name(FastStr::from_static_str("UnknownService"));

        let result = router.call(&mut cx, Bytes::new()).await;
        assert!(result.is_err());
        match result {
            Err(ServerError::Application(e)) => {
                assert_eq!(e.kind(), ApplicationExceptionKind::UNKNOWN_METHOD);
                assert!(e.message().contains("UnknownService"));
            }
            _ => panic!("Expected ApplicationException"),
        }
    }

    #[tokio::test]
    async fn test_router_error_no_default_no_isn() {
        let router = Router::new().add_service(AnotherMockService);
        // No default service

        let mut cx = ServerContext::default();
        // No ISN set

        let result = router.call(&mut cx, Bytes::new()).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_router_routes_to_named_service_by_isn() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let mut cx = ServerContext::default();
        // Set ISN to "MockService" (the default service's name)
        cx.set_idl_service_name(FastStr::from_static_str("MockService"));

        let result = router.call(&mut cx, Bytes::new()).await.unwrap();
        assert_eq!(result, Bytes::from("default"));
    }

    #[test]
    fn test_router_clone() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let cloned = router.clone();
        assert_eq!(cloned.service_count(), 2);
        assert!(cloned.has_default_service());
    }

    #[test]
    fn test_router_debug() {
        let router = Router::new()
            .with_default_service(MockService { name: "default" })
            .add_service(AnotherMockService);

        let debug_str = format!("{:?}", router);
        assert!(debug_str.contains("Router"));
        assert!(debug_str.contains("has_default_service: true"));
    }
}