kevy_embedded/
ops_feed.rs1use crate::KevyResult;
16use std::sync::{Arc, Mutex};
17
18use kevy_replicate::feed::{FeedRead, FeedSource};
19
20use crate::store::Store;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Change {
25 pub offset: u64,
27 pub argv: Vec<Vec<u8>>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ChangeBatch {
34 pub changes: Vec<Change>,
36 pub next: (u64, u64),
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum FeedError {
43 Resync {
46 generation: u64,
48 tail: u64,
50 },
51 Future,
53 Disabled,
55}
56
57const FILTER_DENYLIST: &[&[u8]] = &[
60 b"DEL",
61 b"UNLINK",
62 b"MSET",
63 b"COPY",
64 b"RENAME",
65 b"FLUSHALL",
66 b"BITOP",
67 b"SINTERSTORE",
68 b"SUNIONSTORE",
69 b"SDIFFSTORE",
70 b"ZINTERSTORE",
71 b"ZUNIONSTORE",
72 b"ZDIFFSTORE",
73];
74
75fn matches_prefixes(argv: &[Vec<u8>], prefixes: &[&[u8]]) -> bool {
76 if prefixes.is_empty() {
77 return true;
78 }
79 let Some(verb) = argv.first() else { return true };
80 if FILTER_DENYLIST.iter().any(|d| verb.eq_ignore_ascii_case(d)) {
81 return true; }
83 match argv.get(1) {
84 Some(key) => prefixes.iter().any(|p| key.starts_with(p)),
85 None => true,
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct PrefixInfo {
92 pub keys: u64,
94 pub expires: u64,
96}
97
98impl Store {
99 pub fn info_prefix(&self, prefix: &[u8]) -> PrefixInfo {
103 let mut keys = 0u64;
104 let mut expires = 0u64;
105 for shard in self.shards.iter() {
106 let g = crate::store::lock_read(shard);
107 let (k, e) = g.store.prefix_stats(prefix);
108 keys += k;
109 expires += e;
110 }
111 PrefixInfo { keys, expires }
112 }
113
114 pub fn feed_shards(&self) -> usize {
117 1
118 }
119
120 pub fn changes_tail(&self) -> Result<(u64, u64), FeedError> {
123 let feed = self.feed_handle().ok_or(FeedError::Disabled)?;
124 let g = feed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
125 Ok(g.tail())
126 }
127
128 pub fn changes_since(
133 &self,
134 generation: u64,
135 offset: u64,
136 limit: usize,
137 prefixes: &[&[u8]],
138 ) -> Result<ChangeBatch, FeedError> {
139 let feed = self.feed_handle().ok_or(FeedError::Disabled)?;
140 let g = feed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
141 let frames = match g.read(generation, offset, limit.clamp(1, 65536)) {
142 Ok(v) => v,
143 Err(FeedRead::Resync { generation, tail }) => {
144 return Err(FeedError::Resync { generation, tail });
145 }
146 Err(FeedRead::Future) => return Err(FeedError::Future),
147 };
148 let next_off = frames.last().map_or(offset, |f| f.offset + 1);
149 let mut changes = Vec::with_capacity(frames.len());
150 for f in &frames {
151 let Ok((foff, argv, _)) = kevy_replicate::wire::decode_frame(f.bytes) else {
152 continue;
153 };
154 let owned: Vec<Vec<u8>> = (0..argv.len()).map(|i| argv[i].to_vec()).collect();
155 if !matches_prefixes(&owned, prefixes) {
156 continue;
157 }
158 changes.push(Change { offset: foff, argv: owned });
159 }
160 Ok(ChangeBatch { changes, next: (g.generation(), next_off) })
161 }
162
163 pub(crate) fn feed_handle(&self) -> Option<&Arc<Mutex<FeedSource>>> {
166 self.feed.as_ref()
167 }
168
169 pub(crate) fn feed_bump_on_flush(&self) {
172 let Some(feed) = self.feed_handle() else { return };
173 let mut g = feed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
174 g.bump_generation();
175 if let Some(dir) = &self.config.data_dir
176 && let Err(e) = kevy_persist::feed_meta::write_feed_gen(dir, 0, g.generation())
177 {
178 eprintln!("kevy-embedded: feed gen write failed: {e}");
179 }
180 }
181
182 pub(crate) fn feed_write_close_marker(
185 shards_feed: &Arc<Mutex<FeedSource>>,
186 dir: &std::path::Path,
187 ) {
188 let g = shards_feed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
189 let (generation, next) = g.tail();
190 if let Err(e) = kevy_persist::feed_meta::write_feed_meta(dir, 0, generation, next) {
191 eprintln!("kevy-embedded: feed marker write failed: {e}");
192 }
193 }
194
195 pub(crate) fn feed_push(feed: &Arc<Mutex<FeedSource>>, parts: &[&[u8]]) {
198 let mut g = feed.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
199 let mut argv = kevy_resp::Argv::default();
200 for p in parts {
201 argv.push(p);
202 }
203 let _ = g.source_mut().push_mutation(&argv);
204 }
205
206 pub(crate) fn feed_open(
209 config: &crate::config::Config,
210 ) -> KevyResult<Option<Arc<Mutex<FeedSource>>>> {
211 if !config.feed_enabled {
212 return Ok(None);
213 }
214 let budget = usize::try_from(config.feed_buffer_size).unwrap_or(usize::MAX);
215 let (generation, next_offset) = match &config.data_dir {
216 Some(dir) => {
217 let b = kevy_persist::feed_meta::load_feed_boot(dir, 0)?;
218 (b.generation, b.next_offset)
219 }
220 None => (1, 0),
221 };
222 let mut src = kevy_replicate::source::ReplicationSource::new(budget);
223 src.set_next_offset(next_offset);
224 Ok(Some(Arc::new(Mutex::new(FeedSource::new(generation, src)))))
225 }
226}