Skip to main content

embedded_redis/network/
client.rs

1use crate::commands::auth::AuthCommand;
2use crate::commands::builder::CommandBuilder;
3use crate::commands::hello::{HelloCommand, HelloResponse};
4use crate::commands::Command;
5use crate::network::buffer::Network;
6use crate::network::future::Future;
7use crate::network::handler::{ConnectionError, Credentials};
8use crate::network::protocol::{Protocol, Resp3};
9use crate::network::timeout::{Timeout, TimeoutError};
10use crate::subscription::client::{Error, Subscription};
11use crate::subscription::messages::ToPushMessage;
12use alloc::string::String;
13use bytes::Bytes;
14use core::fmt::{Debug, Formatter};
15use embedded_io_async::{Read, Write};
16use embedded_time::duration::Microseconds;
17use embedded_time::Clock;
18use futures_util::future::{select, Either};
19use futures_util::pin_mut;
20
21/// Error handling for command execution
22#[derive(Debug, Eq, PartialEq, Clone)]
23pub enum CommandErrors {
24    /// No response within expected time frame
25    /// *Is recommended to create a new client/connection in this case*.
26    Timeout,
27    /// Failed encoding TX command
28    EncodingCommandFailed,
29    /// Received an invalid response violating the RESP protocol. Ideally this should never happen.
30    /// The following causes are conceivable
31    /// * Bug in this library (e.g. incomplete implementation of RESP protocol)
32    /// * Redis server bug
33    /// * Network failure. As we are using TCP, only a network stack bug or other exotic causes (e.g. bit flip) is reasonable.
34    /// * Is recommended to create a new client/connection in this case*.
35    ProtocolViolation,
36    /// A pending command operation is no longer valid. This happens on fatal problems like
37    /// timeouts or faulty responses, for which response-to-command mapping can no longer be
38    /// guaranteed.
39    /// *Is recommended to create a new client/connection in this case*.
40    InvalidFuture,
41    /// Low level network error
42    TcpError,
43    /// Upstream timer/clock failure
44    TimerError,
45    /// Received an unexpected response incompatible with the command specification
46    CommandResponseViolation,
47    /// Redis error response. Inner value is the error message received.
48    ErrorResponse(String),
49    /// Memory limit reached. s. [MemoryParameter](crate::network::MemoryParameters)
50    /// *Is recommended to create a new client/connection in this case*.
51    MemoryFull,
52}
53
54/// Client to execute Redis commands
55///
56/// The functionality of the client is best explained by a [command example](crate::commands::get).
57pub struct Client<'a, T: Read + Write, C: Clock, P: Protocol>
58where
59    HelloCommand: Command<<P as Protocol>::FrameType>,
60{
61    pub(crate) network: Network<T, P>,
62    pub(crate) clock: Option<&'a C>,
63
64    /// Max. time waiting for response
65    pub(crate) timeout_duration: Microseconds,
66
67    /// Response to HELLO command, only used for RESP3
68    pub(crate) hello_response: Option<<HelloCommand as Command<<P as Protocol>::FrameType>>::Response>,
69}
70
71impl<'a, T: Read + Write, C: Clock, P: Protocol> Client<'a, T, C, P>
72where
73    AuthCommand: Command<<P as Protocol>::FrameType>,
74    HelloCommand: Command<<P as Protocol>::FrameType>,
75{
76    /// Sends the given command and asynchronously waits for its evaluated response.
77    pub async fn send<Cmd>(&self, command: Cmd) -> Result<Cmd::Response, CommandErrors>
78    where
79        Cmd: Command<P::FrameType>,
80    {
81        self.send_pending(command).await?.wait().await
82    }
83
84    /// Sends a command and returns the internal pending response state.
85    pub(crate) async fn send_pending<'b, Cmd>(
86        &'b self,
87        command: Cmd,
88    ) -> Result<Future<'b, T, C, P, Cmd>, CommandErrors>
89    where
90        Cmd: Command<P::FrameType>,
91    {
92        let id = self.network.send(command.encode()).await?;
93
94        Ok(Future::new(
95            id,
96            command,
97            self.network.get_protocol(),
98            &self.network,
99            Timeout::new(self.clock, self.timeout_duration)?,
100        ))
101    }
102
103    /// Subscribes the given channels and returns a subscription client.
104    ///
105    /// If subscribing fails, drop the client so its connection is closed; the server-side state
106    /// may be undefined.
107    pub async fn subscribe<const L: usize>(
108        self,
109        channels: [Bytes; L],
110    ) -> Result<Subscription<'a, T, C, P, L>, Error>
111    where
112        <P as Protocol>::FrameType: ToPushMessage,
113        <P as Protocol>::FrameType: From<CommandBuilder>,
114    {
115        Subscription::new(self, channels).subscribe().await
116    }
117
118    /// Authenticates with the given credentials during client initialization.
119    pub(crate) async fn auth(&self, credentials: Option<Credentials>) -> Result<(), ConnectionError> {
120        if let Some(credentials) = credentials.as_ref() {
121            self.send(AuthCommand::from(credentials)).await.map_err(auth_error)?;
122        }
123
124        Ok(())
125    }
126
127    /// Prepares the new RESP3 client by authenticating and switching protocol (HELLO command) if needed
128    pub(crate) async fn init(
129        &self,
130        credentials: Option<Credentials>,
131    ) -> Result<Option<<HelloCommand as Command<<P as Protocol>::FrameType>>::Response>, ConnectionError>
132    {
133        self.auth(credentials).await?;
134        if self.network.get_protocol().requires_hello() {
135            return Ok(Some(self.send(HelloCommand {}).await.map_err(hello_error)?));
136        }
137
138        Ok(None)
139    }
140
141    /// Waiting on any dropped futures to leave a clean state
142    pub async fn close(&self) {
143        if !self.network.remaining_dropped_futures() {
144            return;
145        }
146
147        let timer = match Timeout::new(self.clock, self.timeout_duration) {
148            Ok(timer) => timer,
149            Err(_) => {
150                return;
151            }
152        };
153
154        while self.network.remaining_dropped_futures() {
155            self.network.handle_dropped_futures();
156
157            if !self.network.remaining_dropped_futures() {
158                return;
159            }
160
161            if timer.is_enabled() {
162                let receive = self.network.receive_chunk();
163                let expired = timer.wait();
164                pin_mut!(receive, expired);
165
166                match select(receive, expired).await {
167                    Either::Left((Ok(bytes), _)) if bytes > 0 => {}
168                    Either::Left((Err(_), _)) | Either::Right(_) => return,
169                    Either::Left((Ok(_), _)) => return,
170                }
171            } else {
172                match self.network.receive_chunk().await {
173                    Ok(bytes) if bytes > 0 => {}
174                    _ => return,
175                }
176            }
177        }
178    }
179}
180
181impl<T: Read + Write, C: Clock> Client<'_, T, C, Resp3> {
182    /// Returns the response to HELLO command executed during connection initialization
183    /// [Client HELLO response]
184    pub fn get_hello_response(&self) -> &HelloResponse {
185        self.hello_response.as_ref().unwrap()
186    }
187}
188
189impl From<TimeoutError> for CommandErrors {
190    fn from(_: TimeoutError) -> Self {
191        CommandErrors::TimerError
192    }
193}
194
195fn auth_error(error: CommandErrors) -> ConnectionError {
196    ConnectionError::AuthenticationError(error)
197}
198
199#[allow(dead_code)]
200fn hello_error(error: CommandErrors) -> ConnectionError {
201    ConnectionError::ProtocolSwitchError(error)
202}
203
204impl<T: Read + Write, C: Clock, P: Protocol> Debug for Client<'_, T, C, P>
205where
206    HelloCommand: Command<<P as Protocol>::FrameType>,
207{
208    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
209        f.debug_struct("Client")
210            .field("network", &self.network)
211            .field("timeout_duration", &self.timeout_duration)
212            .finish()
213    }
214}