Route similarity search through the DHT

This commit is contained in:
Aleksandr Bogomiakov
2026-08-10 19:40:37 +01:00
parent e95d2e7fe1
commit 87cb7fe74c
11 changed files with 374 additions and 50 deletions
+125 -4
View File
@@ -25,6 +25,8 @@ use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;
use anyhow::{Context, Result};
use music_dht::similarity_dht::SimilarityDht;
use music_dht::similarity_lsh::SIMILARITY_DHT_ALPN;
use music_dht::{
ByteStream, ByteStreamConnectionStats, EndpointId, ItemKind, ItemSpec, LibraryItem,
MusicDhtConfig, MusicDhtService, NetworkId, PeerTicket, PublishStats, RendezvousConfig,
@@ -413,6 +415,7 @@ pub struct NetworkLibrarySource {
struct Running {
service: Arc<MusicDhtService>,
similarity_dht: Arc<SimilarityDht>,
network_name: String,
network_id: NetworkId,
tasks: Vec<tokio::task::JoinHandle<()>>,
@@ -697,12 +700,15 @@ impl Federation {
.stream_protocol(AUDIO_ALPN)
// ...and browse each other's per-artist catalogs over this one.
.stream_protocol(CATALOG_ALPN)
// Anonymous, bounded direct embedding queries.
.stream_protocol(SIMILARITY_ALPN)
// Anonymous, bounded direct embedding queries have their own
// versioned contract and survive catalog-schema upgrades.
.schema_independent_stream_protocol(SIMILARITY_ALPN)
// Personal-device sync (likes, playlists, trusted devices).
.stream_protocol(crate::devices::SYNC_ALPN)
// Capability-scoped shared playback control.
.stream_protocol(crate::jam::JAM_ALPN)
// Signed LSH summaries form their own upgrade-safe DHT overlay.
.schema_independent_stream_protocol(SIMILARITY_DHT_ALPN)
// Informational application/protocol versions.
.schema_independent_stream_protocol(capabilities::CAPABILITIES_ALPN)
.build()
@@ -717,6 +723,25 @@ impl Federation {
"federation started"
);
let similarity_dht = SimilarityDht::open(
Arc::clone(&service),
self.data_dir.join("similarity-routing.sqlite3"),
)
.await
.map_err(|err| anyhow::anyhow!("failed to start the similarity DHT: {err}"))?;
let similarity_dht_acceptor = service
.stream_acceptor(SIMILARITY_DHT_ALPN)
.map_err(|err| anyhow::anyhow!("failed to take similarity DHT acceptor: {err}"))?;
let similarity_dht_serve_task =
tokio::spawn(Arc::clone(&similarity_dht).serve(similarity_dht_acceptor));
let similarity_dht_maintenance_task =
tokio::spawn(Arc::clone(&similarity_dht).maintenance());
let similarity_dht_sync_task = tokio::spawn(similarity_route_sync_loop(
Arc::clone(&similarity_dht),
Arc::clone(&self.similarity),
Arc::clone(&self.library),
));
// Drain DHT events into the log; the channel is bounded.
let event_task = tokio::spawn(async move {
while let Some(event) = events.recv().await {
@@ -797,6 +822,7 @@ impl Federation {
*guard = Some(Running {
service,
similarity_dht,
network_name,
network_id,
tasks: vec![
@@ -811,6 +837,9 @@ impl Federation {
jam_poll_task,
capabilities_serve_task,
capabilities_probe_task,
similarity_dht_serve_task,
similarity_dht_maintenance_task,
similarity_dht_sync_task,
],
});
self.set_error(None);
@@ -835,6 +864,20 @@ impl Federation {
.context("federation is not running")
}
async fn similarity_services(&self) -> Result<(Arc<MusicDhtService>, Arc<SimilarityDht>)> {
self.running
.lock()
.await
.as_ref()
.map(|running| {
(
Arc::clone(&running.service),
Arc::clone(&running.similarity_dht),
)
})
.context("federation is not running")
}
fn ensure_connected_devices_enabled(&self) -> Result<()> {
let settings = self.settings();
anyhow::ensure!(
@@ -1175,8 +1218,15 @@ impl Federation {
self.similarity.network_allowed(),
"similarity federation has no consent"
);
let service = self.service().await?;
similarity::search(service, query, limit, Arc::clone(&self.transport_stats)).await
let (service, similarity_dht) = self.similarity_services().await?;
similarity::search(
service,
similarity_dht,
query,
limit,
Arc::clone(&self.transport_stats),
)
.await
}
/// Resolves a share-link content id to one playable federated track.
@@ -2565,6 +2615,77 @@ fn sort_fed_appearances(appearances: &mut [FedAppearsOn]) {
});
}
async fn similarity_route_sync_loop(
routing: Arc<SimilarityDht>,
similarity: Arc<crate::similarity::Manager>,
library: Arc<Library>,
) {
let mut interval = tokio::time::interval(SYNC_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
let mut published_marker: Option<(String, blake3::Hash)> = None;
loop {
interval.tick().await;
if !similarity.network_allowed() {
if published_marker.take().is_some() {
routing.clear_local_signatures();
tracing::info!("local similarity DHT publication disabled");
}
continue;
}
let status = similarity.status();
let Some(profile_id) = status.active_profile else {
continue;
};
if status.phase != crate::similarity::Phase::Ready {
continue;
}
let library = Arc::clone(&library);
let profile_for_task = profile_id.clone();
let loaded = tokio::task::spawn_blocking(move || {
let signatures = library.similarity_routing_signatures(&profile_for_task)?;
let mut hasher = blake3::Hasher::new();
for signature in &signatures {
hasher.update(signature);
}
Ok::<_, anyhow::Error>((signatures, hasher.finalize()))
})
.await;
let (signatures, fingerprint) = match loaded {
Ok(Ok(loaded)) => loaded,
Ok(Err(error)) => {
tracing::warn!(%error, %profile_id, "similarity routing signatures unavailable");
continue;
}
Err(error) => {
tracing::warn!(%error, "similarity routing signature task failed");
continue;
}
};
let marker = (profile_id.clone(), fingerprint);
if published_marker.as_ref() == Some(&marker) {
continue;
}
match routing
.sync_local_signatures(profile_id.clone(), signatures)
.await
{
Ok(stats) => {
tracing::info!(
profile = %profile_id,
records = stats.records,
keys = stats.keys,
remote_nodes = stats.remote_nodes,
"local similarity DHT index synchronized"
);
published_marker = Some(marker);
}
Err(error) => {
tracing::warn!(%error, %profile_id, "similarity DHT synchronization failed");
}
}
}
}
async fn stop_running(running: Option<Running>) {
let Some(running) = running else { return };
for task in &running.tasks {