solid_mcp 0.2.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/solid_mcp_native/Cargo.toml +12 -0
- data/ext/solid_mcp_native/core/Cargo.toml +32 -0
- data/ext/solid_mcp_native/core/src/config.rs +133 -0
- data/ext/solid_mcp_native/core/src/db/mod.rs +154 -0
- data/ext/solid_mcp_native/core/src/db/postgres.rs +242 -0
- data/ext/solid_mcp_native/core/src/db/sqlite.rs +276 -0
- data/ext/solid_mcp_native/core/src/error.rs +38 -0
- data/ext/solid_mcp_native/core/src/lib.rs +25 -0
- data/ext/solid_mcp_native/core/src/message.rs +191 -0
- data/ext/solid_mcp_native/core/src/pubsub.rs +309 -0
- data/ext/solid_mcp_native/core/src/subscriber.rs +298 -0
- data/ext/solid_mcp_native/core/src/writer.rs +252 -0
- data/ext/solid_mcp_native/extconf.rb +3 -0
- data/ext/solid_mcp_native/ffi/Cargo.toml +20 -0
- data/ext/solid_mcp_native/ffi/extconf.rb +67 -0
- data/ext/solid_mcp_native/ffi/src/lib.rs +224 -0
- data/lib/solid_mcp/configuration.rb +5 -2
- data/lib/solid_mcp/message_writer.rb +80 -45
- data/lib/solid_mcp/native_speedup.rb +140 -0
- data/lib/solid_mcp/pub_sub.rb +10 -8
- data/lib/solid_mcp/subscriber.rb +18 -7
- data/lib/solid_mcp/version.rb +1 -1
- data/lib/solid_mcp.rb +3 -0
- metadata +57 -19
- data/.release-please-manifest.json +0 -1
- data/CHANGELOG.md +0 -34
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -140
- data/Rakefile +0 -8
- data/app/models/solid_mcp/message.rb +0 -25
- data/app/models/solid_mcp/record.rb +0 -10
- data/bin/console +0 -11
- data/bin/rails +0 -15
- data/bin/setup +0 -8
- data/bin/test +0 -8
- data/db/migrate/20250624000001_create_solid_mcp_messages.rb +0 -28
- data/release-please-config.json +0 -8
- data/solid_mcp.gemspec +0 -39
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
//! High-level pub/sub API
|
|
2
|
+
//!
|
|
3
|
+
//! This is the main interface for solid-mcp-core, providing:
|
|
4
|
+
//! - Session-based subscriptions
|
|
5
|
+
//! - Non-blocking message broadcasting
|
|
6
|
+
//! - Graceful shutdown
|
|
7
|
+
|
|
8
|
+
use crate::db::{Database, DbPool};
|
|
9
|
+
use crate::subscriber::{MessageCallback, Subscriber};
|
|
10
|
+
use crate::writer::MessageWriter;
|
|
11
|
+
use crate::{Config, Error, Message, Result};
|
|
12
|
+
use std::collections::HashMap;
|
|
13
|
+
use std::sync::Arc;
|
|
14
|
+
use tokio::sync::RwLock;
|
|
15
|
+
use tracing::{debug, info};
|
|
16
|
+
|
|
17
|
+
/// The main pub/sub engine
|
|
18
|
+
pub struct PubSub {
|
|
19
|
+
db: Arc<DbPool>,
|
|
20
|
+
config: Config,
|
|
21
|
+
writer: Arc<MessageWriter>,
|
|
22
|
+
subscribers: RwLock<HashMap<String, Subscriber>>,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl PubSub {
|
|
26
|
+
/// Create a new pub/sub engine
|
|
27
|
+
pub async fn new(config: Config) -> Result<Self> {
|
|
28
|
+
let db = Arc::new(DbPool::new(&config).await?);
|
|
29
|
+
let writer = Arc::new(MessageWriter::new(db.clone(), &config).await?);
|
|
30
|
+
|
|
31
|
+
info!("PubSub engine initialized");
|
|
32
|
+
|
|
33
|
+
Ok(Self {
|
|
34
|
+
db,
|
|
35
|
+
config,
|
|
36
|
+
writer,
|
|
37
|
+
subscribers: RwLock::new(HashMap::new()),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Create a new pub/sub engine with an existing database pool
|
|
42
|
+
pub async fn with_db(db: Arc<DbPool>, config: Config) -> Result<Self> {
|
|
43
|
+
let writer = Arc::new(MessageWriter::new(db.clone(), &config).await?);
|
|
44
|
+
|
|
45
|
+
Ok(Self {
|
|
46
|
+
db,
|
|
47
|
+
config,
|
|
48
|
+
writer,
|
|
49
|
+
subscribers: RwLock::new(HashMap::new()),
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Broadcast a message to a session (non-blocking)
|
|
54
|
+
///
|
|
55
|
+
/// Returns `true` if the message was enqueued, `false` if the queue was full.
|
|
56
|
+
pub fn broadcast(
|
|
57
|
+
&self,
|
|
58
|
+
session_id: impl Into<String>,
|
|
59
|
+
event_type: impl Into<String>,
|
|
60
|
+
data: impl Into<String>,
|
|
61
|
+
) -> Result<bool> {
|
|
62
|
+
let message = Message::new(session_id, event_type, data);
|
|
63
|
+
self.writer.enqueue(message)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Broadcast a message to a session (async, waits if queue is full)
|
|
67
|
+
pub async fn broadcast_async(
|
|
68
|
+
&self,
|
|
69
|
+
session_id: impl Into<String>,
|
|
70
|
+
event_type: impl Into<String>,
|
|
71
|
+
data: impl Into<String>,
|
|
72
|
+
) -> Result<()> {
|
|
73
|
+
let message = Message::new(session_id, event_type, data);
|
|
74
|
+
self.writer.enqueue_async(message).await
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Subscribe to messages for a session
|
|
78
|
+
///
|
|
79
|
+
/// The callback will be invoked for each new message.
|
|
80
|
+
/// Returns an error if already subscribed to this session.
|
|
81
|
+
pub async fn subscribe(
|
|
82
|
+
&self,
|
|
83
|
+
session_id: impl Into<String>,
|
|
84
|
+
callback: MessageCallback,
|
|
85
|
+
) -> Result<()> {
|
|
86
|
+
let session_id = session_id.into();
|
|
87
|
+
|
|
88
|
+
let mut subscribers = self.subscribers.write().await;
|
|
89
|
+
|
|
90
|
+
if subscribers.contains_key(&session_id) {
|
|
91
|
+
return Err(Error::Config(format!(
|
|
92
|
+
"Already subscribed to session {}",
|
|
93
|
+
session_id
|
|
94
|
+
)));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let subscriber =
|
|
98
|
+
Subscriber::new(&session_id, self.db.clone(), &self.config, callback).await?;
|
|
99
|
+
subscribers.insert(session_id, subscriber);
|
|
100
|
+
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Unsubscribe from a session
|
|
105
|
+
pub async fn unsubscribe(&self, session_id: &str) -> Result<()> {
|
|
106
|
+
let mut subscribers = self.subscribers.write().await;
|
|
107
|
+
|
|
108
|
+
if let Some(subscriber) = subscribers.remove(session_id) {
|
|
109
|
+
subscriber.stop().await?;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
Ok(())
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Check if subscribed to a session
|
|
116
|
+
pub async fn is_subscribed(&self, session_id: &str) -> bool {
|
|
117
|
+
let subscribers = self.subscribers.read().await;
|
|
118
|
+
subscribers.contains_key(session_id)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Get the number of active subscriptions
|
|
122
|
+
pub async fn subscription_count(&self) -> usize {
|
|
123
|
+
let subscribers = self.subscribers.read().await;
|
|
124
|
+
subscribers.len()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Flush all pending messages to the database
|
|
128
|
+
pub async fn flush(&self) -> Result<()> {
|
|
129
|
+
self.writer.flush().await
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Mark messages as delivered
|
|
133
|
+
pub async fn mark_delivered(&self, ids: &[i64]) -> Result<()> {
|
|
134
|
+
self.db.mark_delivered(ids).await
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Cleanup old messages
|
|
138
|
+
pub async fn cleanup(&self) -> Result<(u64, u64)> {
|
|
139
|
+
let delivered = self
|
|
140
|
+
.db
|
|
141
|
+
.cleanup_delivered(self.config.delivered_retention)
|
|
142
|
+
.await?;
|
|
143
|
+
let undelivered = self
|
|
144
|
+
.db
|
|
145
|
+
.cleanup_undelivered(self.config.undelivered_retention)
|
|
146
|
+
.await?;
|
|
147
|
+
debug!(
|
|
148
|
+
"Cleanup complete: {} delivered, {} undelivered messages removed",
|
|
149
|
+
delivered, undelivered
|
|
150
|
+
);
|
|
151
|
+
Ok((delivered, undelivered))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Shutdown the pub/sub engine gracefully
|
|
155
|
+
pub async fn shutdown(self) -> Result<()> {
|
|
156
|
+
info!("PubSub engine shutting down...");
|
|
157
|
+
|
|
158
|
+
// Stop all subscribers
|
|
159
|
+
let mut subscribers = self.subscribers.write().await;
|
|
160
|
+
for (session_id, subscriber) in subscribers.drain() {
|
|
161
|
+
debug!("Stopping subscriber for session {}", session_id);
|
|
162
|
+
if let Err(e) = subscriber.stop().await {
|
|
163
|
+
tracing::error!("Error stopping subscriber for {}: {}", session_id, e);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
drop(subscribers);
|
|
167
|
+
|
|
168
|
+
// Shutdown writer (flushes remaining messages)
|
|
169
|
+
// Need to unwrap the Arc - this only works if we're the last holder
|
|
170
|
+
match Arc::try_unwrap(self.writer) {
|
|
171
|
+
Ok(writer) => writer.shutdown().await?,
|
|
172
|
+
Err(_) => {
|
|
173
|
+
tracing::warn!("Could not unwrap writer Arc, forcing flush");
|
|
174
|
+
// Best effort - can't fully shutdown
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
info!("PubSub engine shutdown complete");
|
|
179
|
+
Ok(())
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#[cfg(test)]
|
|
184
|
+
mod tests {
|
|
185
|
+
use super::*;
|
|
186
|
+
use crate::db::sqlite::SqlitePool;
|
|
187
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
188
|
+
use std::time::Duration;
|
|
189
|
+
|
|
190
|
+
async fn create_test_pubsub(config: Config) -> PubSub {
|
|
191
|
+
let sqlite = SqlitePool::new("sqlite::memory:").await.unwrap();
|
|
192
|
+
sqlite.setup_test_schema().await.unwrap();
|
|
193
|
+
let db = Arc::new(DbPool::Sqlite(sqlite));
|
|
194
|
+
PubSub::with_db(db, config).await.unwrap()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[tokio::test]
|
|
198
|
+
async fn test_pubsub_basic() {
|
|
199
|
+
let config = Config::new("sqlite::memory:")
|
|
200
|
+
.batch_size(10)
|
|
201
|
+
.polling_interval(Duration::from_millis(10));
|
|
202
|
+
|
|
203
|
+
let pubsub = create_test_pubsub(config).await;
|
|
204
|
+
|
|
205
|
+
let received = Arc::new(AtomicUsize::new(0));
|
|
206
|
+
let received_clone = received.clone();
|
|
207
|
+
|
|
208
|
+
pubsub
|
|
209
|
+
.subscribe(
|
|
210
|
+
"session-1",
|
|
211
|
+
Box::new(move |_| {
|
|
212
|
+
received_clone.fetch_add(1, Ordering::SeqCst);
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
.await
|
|
216
|
+
.unwrap();
|
|
217
|
+
|
|
218
|
+
// Broadcast messages
|
|
219
|
+
for i in 0..5 {
|
|
220
|
+
pubsub
|
|
221
|
+
.broadcast("session-1", "message", format!(r#"{{"i":{}}}"#, i))
|
|
222
|
+
.unwrap();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Flush and wait for delivery
|
|
226
|
+
pubsub.flush().await.unwrap();
|
|
227
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
228
|
+
|
|
229
|
+
assert_eq!(received.load(Ordering::SeqCst), 5);
|
|
230
|
+
|
|
231
|
+
pubsub.shutdown().await.unwrap();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#[tokio::test]
|
|
235
|
+
async fn test_pubsub_multiple_sessions() {
|
|
236
|
+
let config = Config::new("sqlite::memory:").polling_interval(Duration::from_millis(10));
|
|
237
|
+
|
|
238
|
+
let pubsub = create_test_pubsub(config).await;
|
|
239
|
+
|
|
240
|
+
let received1 = Arc::new(AtomicUsize::new(0));
|
|
241
|
+
let received2 = Arc::new(AtomicUsize::new(0));
|
|
242
|
+
|
|
243
|
+
let r1 = received1.clone();
|
|
244
|
+
let r2 = received2.clone();
|
|
245
|
+
|
|
246
|
+
pubsub
|
|
247
|
+
.subscribe(
|
|
248
|
+
"session-1",
|
|
249
|
+
Box::new(move |_| {
|
|
250
|
+
r1.fetch_add(1, Ordering::SeqCst);
|
|
251
|
+
}),
|
|
252
|
+
)
|
|
253
|
+
.await
|
|
254
|
+
.unwrap();
|
|
255
|
+
|
|
256
|
+
pubsub
|
|
257
|
+
.subscribe(
|
|
258
|
+
"session-2",
|
|
259
|
+
Box::new(move |_| {
|
|
260
|
+
r2.fetch_add(1, Ordering::SeqCst);
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
.await
|
|
264
|
+
.unwrap();
|
|
265
|
+
|
|
266
|
+
// Broadcast to different sessions
|
|
267
|
+
pubsub.broadcast("session-1", "msg", "{}").unwrap();
|
|
268
|
+
pubsub.broadcast("session-1", "msg", "{}").unwrap();
|
|
269
|
+
pubsub.broadcast("session-2", "msg", "{}").unwrap();
|
|
270
|
+
|
|
271
|
+
pubsub.flush().await.unwrap();
|
|
272
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
273
|
+
|
|
274
|
+
assert_eq!(received1.load(Ordering::SeqCst), 2);
|
|
275
|
+
assert_eq!(received2.load(Ordering::SeqCst), 1);
|
|
276
|
+
|
|
277
|
+
pubsub.shutdown().await.unwrap();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
#[tokio::test]
|
|
281
|
+
async fn test_pubsub_unsubscribe() {
|
|
282
|
+
let config = Config::new("sqlite::memory:").polling_interval(Duration::from_millis(10));
|
|
283
|
+
|
|
284
|
+
let pubsub = create_test_pubsub(config).await;
|
|
285
|
+
|
|
286
|
+
let received = Arc::new(AtomicUsize::new(0));
|
|
287
|
+
let r = received.clone();
|
|
288
|
+
|
|
289
|
+
pubsub
|
|
290
|
+
.subscribe(
|
|
291
|
+
"session-1",
|
|
292
|
+
Box::new(move |_| {
|
|
293
|
+
r.fetch_add(1, Ordering::SeqCst);
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
.await
|
|
297
|
+
.unwrap();
|
|
298
|
+
|
|
299
|
+
assert!(pubsub.is_subscribed("session-1").await);
|
|
300
|
+
assert_eq!(pubsub.subscription_count().await, 1);
|
|
301
|
+
|
|
302
|
+
pubsub.unsubscribe("session-1").await.unwrap();
|
|
303
|
+
|
|
304
|
+
assert!(!pubsub.is_subscribed("session-1").await);
|
|
305
|
+
assert_eq!(pubsub.subscription_count().await, 0);
|
|
306
|
+
|
|
307
|
+
pubsub.shutdown().await.unwrap();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
//! Async subscriber for session-based message delivery
|
|
2
|
+
//!
|
|
3
|
+
//! Supports two modes:
|
|
4
|
+
//! - PostgreSQL: Uses LISTEN/NOTIFY for real-time delivery (no polling)
|
|
5
|
+
//! - SQLite: Falls back to efficient async polling
|
|
6
|
+
|
|
7
|
+
#[cfg(feature = "postgres")]
|
|
8
|
+
use crate::db::postgres::PostgresPool;
|
|
9
|
+
use crate::db::{Database, DbPool};
|
|
10
|
+
use crate::{Config, Message, Result};
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
|
13
|
+
use std::time::Duration;
|
|
14
|
+
use tokio::task::JoinHandle;
|
|
15
|
+
use tracing::{debug, error, info, warn};
|
|
16
|
+
|
|
17
|
+
/// Callback type for message delivery
|
|
18
|
+
pub type MessageCallback = Box<dyn Fn(Message) + Send + Sync + 'static>;
|
|
19
|
+
|
|
20
|
+
/// A subscriber for a specific session
|
|
21
|
+
pub struct Subscriber {
|
|
22
|
+
session_id: String,
|
|
23
|
+
handle: JoinHandle<()>,
|
|
24
|
+
shutdown: Arc<AtomicBool>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl Subscriber {
|
|
28
|
+
/// Create a new subscriber for a session
|
|
29
|
+
///
|
|
30
|
+
/// The callback will be invoked for each new message.
|
|
31
|
+
pub async fn new(
|
|
32
|
+
session_id: impl Into<String>,
|
|
33
|
+
db: Arc<DbPool>,
|
|
34
|
+
config: &Config,
|
|
35
|
+
callback: MessageCallback,
|
|
36
|
+
) -> Result<Self> {
|
|
37
|
+
let session_id = session_id.into();
|
|
38
|
+
let shutdown = Arc::new(AtomicBool::new(false));
|
|
39
|
+
let shutdown_clone = shutdown.clone();
|
|
40
|
+
let session_clone = session_id.clone();
|
|
41
|
+
let polling_interval = config.polling_interval;
|
|
42
|
+
|
|
43
|
+
// Get initial last_id
|
|
44
|
+
let last_id = Arc::new(AtomicI64::new(db.max_id().await?));
|
|
45
|
+
|
|
46
|
+
let handle = match &*db {
|
|
47
|
+
#[cfg(feature = "postgres")]
|
|
48
|
+
DbPool::Postgres(pg) => {
|
|
49
|
+
// Use LISTEN/NOTIFY for PostgreSQL
|
|
50
|
+
let pg_clone = pg.clone();
|
|
51
|
+
let db_clone = db.clone();
|
|
52
|
+
tokio::spawn(async move {
|
|
53
|
+
postgres_subscriber_loop(
|
|
54
|
+
session_clone,
|
|
55
|
+
pg_clone,
|
|
56
|
+
db_clone,
|
|
57
|
+
last_id,
|
|
58
|
+
shutdown_clone,
|
|
59
|
+
callback,
|
|
60
|
+
)
|
|
61
|
+
.await
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
#[cfg(feature = "sqlite")]
|
|
65
|
+
DbPool::Sqlite(_) => {
|
|
66
|
+
// Use polling for SQLite
|
|
67
|
+
tokio::spawn(async move {
|
|
68
|
+
polling_subscriber_loop(
|
|
69
|
+
session_clone,
|
|
70
|
+
db,
|
|
71
|
+
last_id,
|
|
72
|
+
polling_interval,
|
|
73
|
+
shutdown_clone,
|
|
74
|
+
callback,
|
|
75
|
+
)
|
|
76
|
+
.await
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
info!("Subscriber started for session {}", session_id);
|
|
82
|
+
|
|
83
|
+
Ok(Self {
|
|
84
|
+
session_id,
|
|
85
|
+
handle,
|
|
86
|
+
shutdown,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Stop the subscriber
|
|
91
|
+
pub async fn stop(self) -> Result<()> {
|
|
92
|
+
info!("Stopping subscriber for session {}", self.session_id);
|
|
93
|
+
self.shutdown.store(true, Ordering::SeqCst);
|
|
94
|
+
|
|
95
|
+
// Wait for the task to complete (with timeout)
|
|
96
|
+
tokio::select! {
|
|
97
|
+
_ = self.handle => {
|
|
98
|
+
debug!("Subscriber task completed");
|
|
99
|
+
}
|
|
100
|
+
_ = tokio::time::sleep(Duration::from_secs(5)) => {
|
|
101
|
+
warn!("Subscriber task did not complete in time, aborting");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Get the session ID
|
|
109
|
+
pub fn session_id(&self) -> &str {
|
|
110
|
+
&self.session_id
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Polling-based subscriber loop (for SQLite)
|
|
115
|
+
async fn polling_subscriber_loop(
|
|
116
|
+
session_id: String,
|
|
117
|
+
db: Arc<DbPool>,
|
|
118
|
+
last_id: Arc<AtomicI64>,
|
|
119
|
+
polling_interval: Duration,
|
|
120
|
+
shutdown: Arc<AtomicBool>,
|
|
121
|
+
callback: MessageCallback,
|
|
122
|
+
) {
|
|
123
|
+
debug!(
|
|
124
|
+
"Starting polling subscriber for session {} (interval: {:?})",
|
|
125
|
+
session_id, polling_interval
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
while !shutdown.load(Ordering::SeqCst) {
|
|
129
|
+
// Fetch new messages
|
|
130
|
+
let current_last_id = last_id.load(Ordering::SeqCst);
|
|
131
|
+
match db.fetch_after(&session_id, current_last_id, 100).await {
|
|
132
|
+
Ok(messages) => {
|
|
133
|
+
for msg in messages {
|
|
134
|
+
let msg_id = msg.id;
|
|
135
|
+
|
|
136
|
+
// Deliver to callback
|
|
137
|
+
callback(msg);
|
|
138
|
+
|
|
139
|
+
// Update last_id
|
|
140
|
+
last_id.store(msg_id, Ordering::SeqCst);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
Err(e) => {
|
|
144
|
+
error!("Error fetching messages for session {}: {}", session_id, e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Sleep until next poll (interruptible)
|
|
149
|
+
tokio::select! {
|
|
150
|
+
_ = tokio::time::sleep(polling_interval) => {}
|
|
151
|
+
_ = async {
|
|
152
|
+
while !shutdown.load(Ordering::SeqCst) {
|
|
153
|
+
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
154
|
+
}
|
|
155
|
+
} => {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
debug!("Polling subscriber for session {} stopped", session_id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// LISTEN/NOTIFY-based subscriber loop (for PostgreSQL)
|
|
165
|
+
#[cfg(feature = "postgres")]
|
|
166
|
+
async fn postgres_subscriber_loop(
|
|
167
|
+
session_id: String,
|
|
168
|
+
pg: PostgresPool,
|
|
169
|
+
db: Arc<DbPool>,
|
|
170
|
+
last_id: Arc<AtomicI64>,
|
|
171
|
+
shutdown: Arc<AtomicBool>,
|
|
172
|
+
callback: MessageCallback,
|
|
173
|
+
) {
|
|
174
|
+
debug!(
|
|
175
|
+
"Starting LISTEN/NOTIFY subscriber for session {}",
|
|
176
|
+
session_id
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// First, catch up on any missed messages
|
|
180
|
+
let current_last_id = last_id.load(Ordering::SeqCst);
|
|
181
|
+
match db.fetch_after(&session_id, current_last_id, 1000).await {
|
|
182
|
+
Ok(messages) => {
|
|
183
|
+
for msg in messages {
|
|
184
|
+
let msg_id = msg.id;
|
|
185
|
+
callback(msg);
|
|
186
|
+
last_id.store(msg_id, Ordering::SeqCst);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
Err(e) => {
|
|
190
|
+
error!(
|
|
191
|
+
"Error catching up messages for session {}: {}",
|
|
192
|
+
session_id, e
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Set up LISTEN
|
|
198
|
+
let mut listener = match pg.listen(&session_id).await {
|
|
199
|
+
Ok(l) => l,
|
|
200
|
+
Err(e) => {
|
|
201
|
+
error!(
|
|
202
|
+
"Failed to create listener for session {}: {}",
|
|
203
|
+
session_id, e
|
|
204
|
+
);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Listen for notifications
|
|
210
|
+
while !shutdown.load(Ordering::SeqCst) {
|
|
211
|
+
tokio::select! {
|
|
212
|
+
notification = listener.recv() => {
|
|
213
|
+
match notification {
|
|
214
|
+
Ok(notif) => {
|
|
215
|
+
// Notification payload is the message ID
|
|
216
|
+
if let Ok(msg_id) = notif.payload().parse::<i64>() {
|
|
217
|
+
// Fetch the specific message
|
|
218
|
+
let current = last_id.load(Ordering::SeqCst);
|
|
219
|
+
if msg_id > current {
|
|
220
|
+
match db.fetch_after(&session_id, current, 100).await {
|
|
221
|
+
Ok(messages) => {
|
|
222
|
+
for msg in messages {
|
|
223
|
+
let id = msg.id;
|
|
224
|
+
callback(msg);
|
|
225
|
+
last_id.store(id, Ordering::SeqCst);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
Err(e) => {
|
|
229
|
+
error!("Error fetching message {}: {}", msg_id, e);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
Err(e) => {
|
|
236
|
+
error!("Listener error for session {}: {}", session_id, e);
|
|
237
|
+
// Reconnect logic could go here
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
_ = tokio::time::sleep(Duration::from_secs(1)) => {
|
|
243
|
+
// Periodic check for shutdown
|
|
244
|
+
if shutdown.load(Ordering::SeqCst) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
debug!(
|
|
252
|
+
"LISTEN/NOTIFY subscriber for session {} stopped",
|
|
253
|
+
session_id
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[cfg(test)]
|
|
258
|
+
mod tests {
|
|
259
|
+
use super::*;
|
|
260
|
+
use crate::db::sqlite::SqlitePool;
|
|
261
|
+
use std::sync::atomic::AtomicUsize;
|
|
262
|
+
|
|
263
|
+
async fn create_test_db() -> Arc<DbPool> {
|
|
264
|
+
let sqlite = SqlitePool::new("sqlite::memory:").await.unwrap();
|
|
265
|
+
sqlite.setup_test_schema().await.unwrap();
|
|
266
|
+
Arc::new(DbPool::Sqlite(sqlite))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#[tokio::test]
|
|
270
|
+
async fn test_polling_subscriber() {
|
|
271
|
+
let db = create_test_db().await;
|
|
272
|
+
let config = Config::new("sqlite::memory:").polling_interval(Duration::from_millis(10));
|
|
273
|
+
|
|
274
|
+
let received = Arc::new(AtomicUsize::new(0));
|
|
275
|
+
let received_clone = received.clone();
|
|
276
|
+
|
|
277
|
+
let callback: MessageCallback = Box::new(move |_msg| {
|
|
278
|
+
received_clone.fetch_add(1, Ordering::SeqCst);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
let subscriber = Subscriber::new("session-1", db.clone(), &config, callback)
|
|
282
|
+
.await
|
|
283
|
+
.unwrap();
|
|
284
|
+
|
|
285
|
+
// Insert some messages
|
|
286
|
+
let messages: Vec<Message> = (0..5)
|
|
287
|
+
.map(|i| Message::new("session-1", "message", format!(r#"{{"i":{}}}"#, i)))
|
|
288
|
+
.collect();
|
|
289
|
+
db.insert_batch(&messages).await.unwrap();
|
|
290
|
+
|
|
291
|
+
// Wait for subscriber to pick them up
|
|
292
|
+
tokio::time::sleep(Duration::from_millis(100)).await;
|
|
293
|
+
|
|
294
|
+
assert_eq!(received.load(Ordering::SeqCst), 5);
|
|
295
|
+
|
|
296
|
+
subscriber.stop().await.unwrap();
|
|
297
|
+
}
|
|
298
|
+
}
|