use crate::crdt::{Result, TaskList, TaskListDelta};
use crate::gossip::wire::{decode_delta, encode_delta};
use crate::gossip::PubSubManager;
use saorsa_gossip_types::PeerId;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
const STATE_SYNC_TOPIC_SUFFIX: &str = "/state-sync";
const STATE_REQUEST_RETRY_SECS: [u64; 4] = [1, 5, 15, 30];
#[derive(Debug, Serialize, Deserialize)]
enum TaskListSyncMessage {
StateRequest { requester: PeerId },
}
pub struct TaskListSync {
task_list: Arc<RwLock<TaskList>>,
pubsub: Arc<PubSubManager>,
topic: String,
local_peer_id: PeerId,
}
impl TaskListSync {
pub fn new(
task_list: TaskList,
pubsub: Arc<PubSubManager>,
topic: String,
local_peer_id: PeerId,
) -> Result<Self> {
let task_list = Arc::new(RwLock::new(task_list));
Ok(Self {
task_list,
pubsub,
topic,
local_peer_id,
})
}
fn state_sync_topic(&self) -> String {
format!("{}{}", self.topic, STATE_SYNC_TOPIC_SUFFIX)
}
pub async fn start(&self) -> Result<()> {
let mut sub = self.pubsub.subscribe(self.topic.clone()).await;
let task_list = Arc::clone(&self.task_list);
tokio::spawn(async move {
while let Some(msg) = sub.recv().await {
match decode_delta::<TaskListDelta>(&msg.payload) {
Ok((peer_id, delta)) => {
let mut list = task_list.write().await;
if let Err(e) = list.merge_delta(&delta, peer_id) {
tracing::warn!("Failed to merge remote delta: {}", e);
}
}
Err(e) => {
tracing::warn!("Failed to deserialize delta from topic: {}", e);
}
}
}
});
let mut sync_sub = self.pubsub.subscribe(self.state_sync_topic()).await;
let responder_list = Arc::clone(&self.task_list);
let responder_pubsub = Arc::clone(&self.pubsub);
let responder_topic = self.topic.clone();
let local_peer_id = self.local_peer_id;
tokio::spawn(async move {
while let Some(msg) = sync_sub.recv().await {
let Ok(TaskListSyncMessage::StateRequest { requester }) =
bincode::deserialize::<TaskListSyncMessage>(&msg.payload)
else {
continue;
};
if requester == local_peer_id {
continue;
}
let full = {
let list = responder_list.read().await;
if list.task_count() == 0 {
continue;
}
list.full_delta()
};
let Ok(serialized) = encode_delta(local_peer_id, &full) else {
continue;
};
if let Err(e) = responder_pubsub
.publish(responder_topic.clone(), bytes::Bytes::from(serialized))
.await
{
tracing::warn!("TaskList state-response publish failed: {e}");
}
}
});
if self.task_list.read().await.task_count() == 0 {
let requester_pubsub = Arc::clone(&self.pubsub);
let sync_topic = self.state_sync_topic();
tokio::spawn(async move {
for delay_secs in STATE_REQUEST_RETRY_SECS {
tokio::time::sleep(std::time::Duration::from_secs(delay_secs)).await;
let request = TaskListSyncMessage::StateRequest {
requester: local_peer_id,
};
let Ok(serialized) = bincode::serialize(&request) else {
return;
};
if let Err(e) = requester_pubsub
.publish(sync_topic.clone(), bytes::Bytes::from(serialized))
.await
{
tracing::debug!("TaskList state-request publish failed: {e}");
}
}
});
}
Ok(())
}
pub async fn stop(&self) -> Result<()> {
self.pubsub.unsubscribe(&self.topic).await;
self.pubsub.unsubscribe(&self.state_sync_topic()).await;
Ok(())
}
pub async fn apply_remote_delta(&self, peer_id: PeerId, delta: TaskListDelta) -> Result<()> {
let mut task_list = self.task_list.write().await;
task_list.merge_delta(&delta, peer_id)?;
Ok(())
}
pub async fn publish_delta(&self, local_peer_id: PeerId, delta: TaskListDelta) -> Result<()> {
let serialized = encode_delta(local_peer_id, &delta).map_err(|e| {
crate::crdt::CrdtError::Gossip(format!("failed to serialize delta: {e}"))
})?;
self.pubsub
.publish(self.topic.clone(), bytes::Bytes::from(serialized))
.await
.map_err(|e| crate::crdt::CrdtError::Gossip(format!("failed to publish delta: {e}")))?;
Ok(())
}
pub async fn read(&self) -> tokio::sync::RwLockReadGuard<'_, TaskList> {
self.task_list.read().await
}
pub async fn write(&self) -> tokio::sync::RwLockWriteGuard<'_, TaskList> {
self.task_list.write().await
}
#[must_use]
pub fn topic(&self) -> &str {
&self.topic
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crdt::{TaskId, TaskItem, TaskListId, TaskMetadata};
use crate::identity::AgentId;
fn agent(n: u8) -> AgentId {
AgentId([n; 32])
}
fn peer(n: u8) -> PeerId {
PeerId::new([n; 32])
}
fn list_id(n: u8) -> TaskListId {
TaskListId::new([n; 32])
}
fn make_task(id_byte: u8, peer: PeerId) -> TaskItem {
let agent = agent(1);
let task_id = TaskId::from_bytes([id_byte; 32]);
let metadata = TaskMetadata::new(
format!("Task {}", id_byte),
format!("Description {}", id_byte),
128,
agent,
1000,
);
TaskItem::new(task_id, metadata, peer)
}
#[tokio::test]
async fn test_task_list_sync_creation() {
let peer = peer(1);
let id = list_id(1);
let task_list = TaskList::new(id, "Test List".to_string(), peer);
let _list_for_sync = task_list;
}
#[tokio::test]
async fn test_apply_delta() {
let peer1 = peer(1);
let peer2 = peer(2);
let id = list_id(1);
let task_list = TaskList::new(id, "Test".to_string(), peer1);
let task_list_arc = Arc::new(RwLock::new(task_list));
let mut delta = TaskListDelta::new(1);
let task = make_task(1, peer2);
let task_id = *task.id();
let tag = (peer2, 1);
delta.added_tasks.insert(task_id, (task, tag));
{
let mut list = task_list_arc.write().await;
let result = list.merge_delta(&delta, peer2);
assert!(result.is_ok());
}
{
let list = task_list_arc.read().await;
assert_eq!(list.task_count(), 1);
}
}
#[tokio::test]
async fn test_concurrent_access() {
let peer = peer(1);
let id = list_id(1);
let task_list = TaskList::new(id, "Test".to_string(), peer);
let task_list_arc = Arc::new(RwLock::new(task_list));
let list1 = task_list_arc.read().await;
let list2 = task_list_arc.read().await;
assert_eq!(list1.name(), "Test");
assert_eq!(list2.name(), "Test");
drop(list1);
drop(list2);
{
let mut list = task_list_arc.write().await;
list.update_name("Updated".to_string(), peer);
}
let list = task_list_arc.read().await;
assert_eq!(list.name(), "Updated");
}
}