netget/client/redis/
pubsub.rs1use super::resp::{self, Reply};
21use anyhow::{bail, Context, Result};
22use serde_json::{json, Value};
23use std::collections::BTreeSet;
24use std::sync::atomic::{AtomicBool, Ordering};
25use std::sync::Mutex;
26use std::time::Duration;
27use tokio::io::{AsyncWriteExt, BufReader};
28use tokio::net::TcpStream;
29
30pub const SIDE_PUBLISH_TIMEOUT: Duration = Duration::from_secs(10);
32
33const SUBSCRIBE_FAMILY: &[&str] = &[
35 "SUBSCRIBE",
36 "PSUBSCRIBE",
37 "SSUBSCRIBE",
38 "UNSUBSCRIBE",
39 "PUNSUBSCRIBE",
40 "SUNSUBSCRIBE",
41];
42
43#[derive(Default)]
44struct Subscriptions {
45 channels: BTreeSet<String>,
46 patterns: BTreeSet<String>,
47 shard: BTreeSet<String>,
48}
49
50pub struct Session {
52 pub remote_addr: String,
54 resp3: AtomicBool,
55 engaged: AtomicBool,
56 subs: Mutex<Subscriptions>,
57}
58
59#[derive(Debug, PartialEq)]
61pub enum Classified {
62 Message {
64 kind: String,
65 pattern: Option<String>,
66 channel: String,
67 message: String,
68 },
69 Subscription {
71 kind: String,
72 channel: Option<String>,
73 count: i64,
74 },
75 Reply,
77}
78
79impl Session {
80 pub fn new(remote_addr: String) -> Self {
81 Session {
82 remote_addr,
83 resp3: AtomicBool::new(false),
84 engaged: AtomicBool::new(false),
85 subs: Mutex::new(Subscriptions::default()),
86 }
87 }
88
89 fn subs(&self) -> std::sync::MutexGuard<'_, Subscriptions> {
90 self.subs.lock().unwrap_or_else(|p| p.into_inner())
91 }
92
93 pub fn is_resp3(&self) -> bool {
94 self.resp3.load(Ordering::Relaxed)
95 }
96
97 pub fn set_resp3(&self, on: bool) {
98 self.resp3.store(on, Ordering::Relaxed);
99 }
100
101 pub fn subscribed_resp2(&self) -> bool {
103 !self.is_resp3() && self.engaged.load(Ordering::Relaxed)
104 }
105
106 pub fn note_sent(&self, args: &[Vec<u8>]) {
108 let name = args
109 .first()
110 .map(|n| String::from_utf8_lossy(n).to_ascii_uppercase())
111 .unwrap_or_default();
112 if SUBSCRIBE_FAMILY.contains(&name.as_str()) {
113 self.engaged.store(true, Ordering::Relaxed);
114 }
115 }
116
117 pub fn unsubscribe_all_commands(&self) -> Vec<Vec<Vec<u8>>> {
119 let subs = self.subs();
120 let mut out = Vec::new();
121 if !subs.channels.is_empty() {
122 out.push(vec![b"UNSUBSCRIBE".to_vec()]);
123 }
124 if !subs.patterns.is_empty() {
125 out.push(vec![b"PUNSUBSCRIBE".to_vec()]);
126 }
127 if !subs.shard.is_empty() {
128 out.push(vec![b"SUNSUBSCRIBE".to_vec()]);
129 }
130 out
131 }
132
133 pub fn snapshot(&self) -> Value {
135 let subs = self.subs();
136 json!({
137 "channels": subs.channels.iter().collect::<Vec<_>>(),
138 "patterns": subs.patterns.iter().collect::<Vec<_>>(),
139 "shard_channels": subs.shard.iter().collect::<Vec<_>>(),
140 })
141 }
142
143 pub fn classify(&self, reply: &Reply) -> Classified {
146 if let Some(proto) = hello_proto(reply) {
147 self.set_resp3(proto == 3);
148 return Classified::Reply;
149 }
150 let pubsub_shaped = match reply.reply_type {
151 "push" => true,
152 "array" => self.engaged.load(Ordering::Relaxed),
153 _ => false,
154 };
155 if !pubsub_shaped {
156 return Classified::Reply;
157 }
158 let Some(items) = reply.value.as_array() else {
159 return Classified::Reply;
160 };
161 let text = |v: &Value| v.as_str().map(str::to_string);
162 let kind = items.first().and_then(text).unwrap_or_default();
163 match (kind.as_str(), items.len()) {
164 ("message" | "smessage", 3) => Classified::Message {
165 kind,
166 pattern: None,
167 channel: text(&items[1]).unwrap_or_default(),
168 message: text(&items[2]).unwrap_or_default(),
169 },
170 ("pmessage", 4) => Classified::Message {
171 kind,
172 pattern: text(&items[1]),
173 channel: text(&items[2]).unwrap_or_default(),
174 message: text(&items[3]).unwrap_or_default(),
175 },
176 (
177 "subscribe" | "psubscribe" | "ssubscribe" | "unsubscribe" | "punsubscribe"
178 | "sunsubscribe",
179 3,
180 ) => {
181 let channel = text(&items[1]);
182 let count = items[2].as_i64().unwrap_or(0);
183 let mut subs = self.subs();
184 if let Some(name) = &channel {
185 let set = match kind.as_str() {
186 "subscribe" | "unsubscribe" => &mut subs.channels,
187 "psubscribe" | "punsubscribe" => &mut subs.patterns,
188 _ => &mut subs.shard,
189 };
190 if kind.ends_with("unsubscribe") {
191 set.remove(name);
192 } else {
193 set.insert(name.clone());
194 }
195 }
196 if kind.ends_with("unsubscribe")
197 && count == 0
198 && subs.channels.is_empty()
199 && subs.patterns.is_empty()
200 && subs.shard.is_empty()
201 {
202 self.engaged.store(false, Ordering::Relaxed);
203 }
204 Classified::Subscription {
205 kind,
206 channel,
207 count,
208 }
209 }
210 _ => Classified::Reply,
211 }
212 }
213}
214
215fn hello_proto(reply: &Reply) -> Option<i64> {
218 match &reply.value {
219 Value::Object(map) if map.contains_key("server") => map.get("proto")?.as_i64(),
220 Value::Array(items) if reply.reply_type == "array" => {
221 let at = |key: &str| items.iter().position(|v| v.as_str() == Some(key));
222 at("server")?;
223 items.get(at("proto")? + 1)?.as_i64()
224 }
225 _ => None,
226 }
227}
228
229pub async fn publish_on_side_connection(addr: &str, args: &[Vec<u8>]) -> Result<i64> {
232 let work = async {
233 let stream = TcpStream::connect(addr)
234 .await
235 .with_context(|| format!("side connection to {addr}"))?;
236 let (read_half, mut write_half) = tokio::io::split(stream);
237 write_half.write_all(&resp::encode_command(args)).await?;
238 write_half.flush().await?;
239 let mut reader = BufReader::new(read_half);
240 let reply = resp::read_reply(&mut reader)
241 .await?
242 .context("the server closed the side connection without answering")?;
243 let _ = write_half.shutdown().await;
244 match (reply.reply_type, reply.value.as_i64()) {
245 ("integer", Some(n)) => Ok(n),
246 _ => bail!("the server answered the publish with {}", reply.response),
247 }
248 };
249 tokio::time::timeout(SIDE_PUBLISH_TIMEOUT, work)
250 .await
251 .context("the side connection's publish timed out")?
252}