Line data Source code
1 : use std::sync::Arc;
2 : use std::time::Duration;
3 :
4 : use futures::FutureExt;
5 : use redis::aio::{ConnectionLike, MultiplexedConnection};
6 : use redis::{ConnectionInfo, IntoConnectionInfo, RedisConnectionInfo, RedisResult};
7 : use tokio::task::JoinHandle;
8 : use tracing::{debug, error, info, warn};
9 :
10 : use super::elasticache::CredentialsProvider;
11 :
12 : enum Credentials {
13 : Static(ConnectionInfo),
14 : Dynamic(Arc<CredentialsProvider>, redis::ConnectionAddr),
15 : }
16 :
17 : impl Clone for Credentials {
18 0 : fn clone(&self) -> Self {
19 0 : match self {
20 0 : Credentials::Static(info) => Credentials::Static(info.clone()),
21 0 : Credentials::Dynamic(provider, addr) => {
22 0 : Credentials::Dynamic(Arc::clone(provider), addr.clone())
23 : }
24 : }
25 0 : }
26 : }
27 :
28 : /// A wrapper around `redis::MultiplexedConnection` that automatically refreshes the token.
29 : /// Provides PubSub connection without credentials refresh.
30 : pub struct ConnectionWithCredentialsProvider {
31 : credentials: Credentials,
32 : con: Option<MultiplexedConnection>,
33 : refresh_token_task: Option<JoinHandle<()>>,
34 : mutex: tokio::sync::Mutex<()>,
35 : }
36 :
37 : impl Clone for ConnectionWithCredentialsProvider {
38 0 : fn clone(&self) -> Self {
39 0 : Self {
40 0 : credentials: self.credentials.clone(),
41 0 : con: None,
42 0 : refresh_token_task: None,
43 0 : mutex: tokio::sync::Mutex::new(()),
44 0 : }
45 0 : }
46 : }
47 :
48 : impl ConnectionWithCredentialsProvider {
49 0 : pub fn new_with_credentials_provider(
50 0 : host: String,
51 0 : port: u16,
52 0 : credentials_provider: Arc<CredentialsProvider>,
53 0 : ) -> Self {
54 0 : Self {
55 0 : credentials: Credentials::Dynamic(
56 0 : credentials_provider,
57 0 : redis::ConnectionAddr::TcpTls {
58 0 : host,
59 0 : port,
60 0 : insecure: false,
61 0 : tls_params: None,
62 0 : },
63 0 : ),
64 0 : con: None,
65 0 : refresh_token_task: None,
66 0 : mutex: tokio::sync::Mutex::new(()),
67 0 : }
68 0 : }
69 :
70 0 : pub fn new_with_static_credentials<T: IntoConnectionInfo>(params: T) -> Self {
71 0 : Self {
72 0 : credentials: Credentials::Static(params.into_connection_info().unwrap()),
73 0 : con: None,
74 0 : refresh_token_task: None,
75 0 : mutex: tokio::sync::Mutex::new(()),
76 0 : }
77 0 : }
78 :
79 0 : async fn ping(con: &mut MultiplexedConnection) -> RedisResult<()> {
80 0 : redis::cmd("PING").query_async(con).await
81 0 : }
82 :
83 0 : pub(crate) async fn connect(&mut self) -> anyhow::Result<()> {
84 0 : let _guard = self.mutex.lock().await;
85 0 : if let Some(con) = self.con.as_mut() {
86 0 : match Self::ping(con).await {
87 : Ok(()) => {
88 0 : return Ok(());
89 : }
90 0 : Err(e) => {
91 0 : warn!("Error during PING: {e:?}");
92 : }
93 : }
94 : } else {
95 0 : info!("Connection is not established");
96 : }
97 0 : info!("Establishing a new connection...");
98 0 : self.con = None;
99 0 : if let Some(f) = self.refresh_token_task.take() {
100 0 : f.abort();
101 0 : }
102 0 : let mut con = self
103 0 : .get_client()
104 0 : .await?
105 0 : .get_multiplexed_tokio_connection()
106 0 : .await?;
107 0 : if let Credentials::Dynamic(credentials_provider, _) = &self.credentials {
108 0 : let credentials_provider = credentials_provider.clone();
109 0 : let con2 = con.clone();
110 0 : let f = tokio::spawn(async move {
111 0 : Self::keep_connection(con2, credentials_provider)
112 0 : .await
113 0 : .inspect_err(|e| debug!("keep_connection failed: {e}"))
114 0 : .ok();
115 0 : });
116 0 : self.refresh_token_task = Some(f);
117 0 : }
118 0 : match Self::ping(&mut con).await {
119 : Ok(()) => {
120 0 : info!("Connection succesfully established");
121 : }
122 0 : Err(e) => {
123 0 : warn!("Connection is broken. Error during PING: {e:?}");
124 : }
125 : }
126 0 : self.con = Some(con);
127 0 : Ok(())
128 0 : }
129 :
130 0 : async fn get_connection_info(&self) -> anyhow::Result<ConnectionInfo> {
131 0 : match &self.credentials {
132 0 : Credentials::Static(info) => Ok(info.clone()),
133 0 : Credentials::Dynamic(provider, addr) => {
134 0 : let (username, password) = provider.provide_credentials().await?;
135 0 : Ok(ConnectionInfo {
136 0 : addr: addr.clone(),
137 0 : redis: RedisConnectionInfo {
138 0 : db: 0,
139 0 : username: Some(username),
140 0 : password: Some(password.clone()),
141 0 : },
142 0 : })
143 : }
144 : }
145 0 : }
146 :
147 0 : async fn get_client(&self) -> anyhow::Result<redis::Client> {
148 0 : let client = redis::Client::open(self.get_connection_info().await?)?;
149 0 : Ok(client)
150 0 : }
151 :
152 : // PubSub does not support credentials refresh.
153 : // Requires manual reconnection every 12h.
154 0 : pub(crate) async fn get_async_pubsub(&self) -> anyhow::Result<redis::aio::PubSub> {
155 0 : Ok(self.get_client().await?.get_async_pubsub().await?)
156 0 : }
157 :
158 : // The connection lives for 12h.
159 : // It can be prolonged with sending `AUTH` commands with the refreshed token.
160 : // https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/auth-iam.html#auth-iam-limits
161 0 : async fn keep_connection(
162 0 : mut con: MultiplexedConnection,
163 0 : credentials_provider: Arc<CredentialsProvider>,
164 0 : ) -> anyhow::Result<()> {
165 : loop {
166 : // The connection lives for 12h, for the sanity check we refresh it every hour.
167 0 : tokio::time::sleep(Duration::from_secs(60 * 60)).await;
168 0 : match Self::refresh_token(&mut con, credentials_provider.clone()).await {
169 : Ok(()) => {
170 0 : info!("Token refreshed");
171 : }
172 0 : Err(e) => {
173 0 : error!("Error during token refresh: {e:?}");
174 : }
175 : }
176 : }
177 : }
178 0 : async fn refresh_token(
179 0 : con: &mut MultiplexedConnection,
180 0 : credentials_provider: Arc<CredentialsProvider>,
181 0 : ) -> anyhow::Result<()> {
182 0 : let (user, password) = credentials_provider.provide_credentials().await?;
183 0 : let _: () = redis::cmd("AUTH")
184 0 : .arg(user)
185 0 : .arg(password)
186 0 : .query_async(con)
187 0 : .await?;
188 0 : Ok(())
189 0 : }
190 : /// Sends an already encoded (packed) command into the TCP socket and
191 : /// reads the single response from it.
192 0 : pub(crate) async fn send_packed_command(
193 0 : &mut self,
194 0 : cmd: &redis::Cmd,
195 0 : ) -> RedisResult<redis::Value> {
196 : // Clone connection to avoid having to lock the ArcSwap in write mode
197 0 : let con = self.con.as_mut().ok_or(redis::RedisError::from((
198 0 : redis::ErrorKind::IoError,
199 0 : "Connection not established",
200 0 : )))?;
201 0 : con.send_packed_command(cmd).await
202 0 : }
203 :
204 : /// Sends multiple already encoded (packed) command into the TCP socket
205 : /// and reads `count` responses from it. This is used to implement
206 : /// pipelining.
207 0 : pub(crate) async fn send_packed_commands(
208 0 : &mut self,
209 0 : cmd: &redis::Pipeline,
210 0 : offset: usize,
211 0 : count: usize,
212 0 : ) -> RedisResult<Vec<redis::Value>> {
213 : // Clone shared connection future to avoid having to lock the ArcSwap in write mode
214 0 : let con = self.con.as_mut().ok_or(redis::RedisError::from((
215 0 : redis::ErrorKind::IoError,
216 0 : "Connection not established",
217 0 : )))?;
218 0 : con.send_packed_commands(cmd, offset, count).await
219 0 : }
220 : }
221 :
222 : impl ConnectionLike for ConnectionWithCredentialsProvider {
223 0 : fn req_packed_command<'a>(
224 0 : &'a mut self,
225 0 : cmd: &'a redis::Cmd,
226 0 : ) -> redis::RedisFuture<'a, redis::Value> {
227 0 : (async move { self.send_packed_command(cmd).await }).boxed()
228 0 : }
229 :
230 0 : fn req_packed_commands<'a>(
231 0 : &'a mut self,
232 0 : cmd: &'a redis::Pipeline,
233 0 : offset: usize,
234 0 : count: usize,
235 0 : ) -> redis::RedisFuture<'a, Vec<redis::Value>> {
236 0 : (async move { self.send_packed_commands(cmd, offset, count).await }).boxed()
237 0 : }
238 :
239 0 : fn get_db(&self) -> i64 {
240 0 : 0
241 0 : }
242 : }
|