feat(distributed): serve children in the search tree

The client read the distributed network but never carried it: a peer
dialling with a D connection was logged and dropped, and AcceptChildren
was hardcoded false, so this client took from the tree without ever
relaying a search for anyone else.

With accept_children set, peers hang from us: each is told where our
branch sits when it connects and again when the branch moves, every
search that reaches us is passed down, and a child that goes away loses
its slot. Ten at a time, matching Nicotine+, since each costs a socket
and a copy of the network's whole search stream — which is also why it is
opt-in, through the new accept_children config key (ignored without a
listener, since a child would have nothing to dial).
This commit is contained in:
Michel de Graaf
2026-09-09 16:47:55 +00:00
parent f5a97744f8
commit 460031b532
12 changed files with 648 additions and 37 deletions
+52 -24
View File
@@ -29,10 +29,8 @@ impl Client {
if let Some(branch) =
ctx.leaf.due_announcement(Instant::now())
{
super::distributed::announce(
ctx.server_sender.as_ref(),
&branch,
true,
super::distributed::announce_move(
&mut ctx, &branch, true,
);
}
}
@@ -539,6 +537,16 @@ impl Client {
token,
query,
} => {
// Pass it down the tree first: carrying the search
// stream is a parent's whole duty, and it is owed
// even for a search we ourselves cannot answer.
if let Ok(mut ctx) = client_context.write_safe()
&& !ctx.children.is_empty()
{
ctx.children
.broadcast_search(&username, token, &query);
}
// Don't answer our own distributed search.
if username == own_username {
continue;
@@ -679,10 +687,8 @@ impl Client {
if let Some(branch) =
ctx.leaf.branch_level(&parent, link, level)
{
super::distributed::announce(
ctx.server_sender.as_ref(),
&branch,
true,
super::distributed::announce_move(
&mut ctx, &branch, true,
);
}
}
@@ -697,10 +703,8 @@ impl Client {
if let Some(branch) =
ctx.leaf.branch_root(&parent, link, &root)
{
super::distributed::announce(
ctx.server_sender.as_ref(),
&branch,
true,
super::distributed::announce_move(
&mut ctx, &branch, true,
);
}
}
@@ -717,10 +721,8 @@ impl Client {
if let Some(branch) =
ctx.leaf.search_from(&parent, link)
{
super::distributed::announce(
ctx.server_sender.as_ref(),
&branch,
true,
super::distributed::announce_move(
&mut ctx, &branch, true,
);
}
if ctx.leaf.is_parent(&parent, link)
@@ -739,10 +741,9 @@ impl Client {
continue;
};
if ctx.leaf.closed(&parent, link) {
super::distributed::announce(
ctx.server_sender.as_ref(),
&ctx.leaf.branch(),
false,
let branch = ctx.leaf.branch();
super::distributed::announce_move(
&mut ctx, &branch, false,
);
}
}
@@ -753,10 +754,9 @@ impl Client {
continue;
};
ctx.leaf.reset();
super::distributed::announce(
ctx.server_sender.as_ref(),
&ctx.leaf.branch(),
false,
let branch = ctx.leaf.branch();
super::distributed::announce_move(
&mut ctx, &branch, false,
);
}
ClientOperation::PrivilegedUsers(users) => {
@@ -816,6 +816,34 @@ impl Client {
ctx.apply_user_interests(interests);
}
}
ClientOperation::ChildConnected { username, stream } => {
// A peer wants to hang from us. Take it on if this
// client serves children and has room, then tell it
// where our branch sits so it can report its own
// place; otherwise the socket is dropped here and the
// peer looks for another parent.
let Ok(mut ctx) = client_context.write_safe() else {
continue;
};
if !ctx.children.has_room() {
continue;
}
if stream.set_nodelay(true).is_err() {
continue;
}
if ctx.children.accept(&username, stream) {
let branch = ctx.leaf.branch();
ctx.children.send_stance_to(
&username,
&branch.root,
branch.level,
);
debug!(
"[distributed] carrying {} children",
ctx.children.len()
);
}
}
ClientOperation::ExcludedSearchPhrases(phrases) => {
if let Ok(mut ctx) = client_context.write_safe() {
ctx.set_excluded_search_phrases(phrases);