Manage duplicate connections more effectively.

Thebroadcast function now transparently avoids sending the same message 
to the same peer over multiple connections at once.
This commit is contained in:
Starbeamrainbowlabs 2022-02-21 21:55:20 +00:00
parent e4a381b915
commit da33a146e2
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
4 changed files with 84 additions and 17 deletions

View file

@ -12,6 +12,26 @@ import ItemQueue from './async/ItemQueue.mjs';
class SystemQuery extends EventEmitter {
/**
* Returns an object representing our local information that looks and
* even quacks a bit like a real Peer object, but isn't.
* @return {Object}
*/
get peer_local() {
return {
id: this.agent.peer_id,
id_short: (this.agent.peer_id || "").substring(0, 7),
name: this.agent.peer_name,
address: this.agent.local_bind_address,
port: this.agent.local_port,
self: true
}
}
// TODO: Handle duplicate connections better by both skipping counting them here, and also implementing a more robust reaper for killing duplicate connections that always kills the newest/oldest connection to avoid issues.
constructor(config, mode = "agent") {
super();
@ -24,6 +44,10 @@ class SystemQuery extends EventEmitter {
this.http = new HttpSubsystem(this);
}
/**
* Initialises the SystemQuery agent.
* @return {Promise} A Promise that resolves when initialisation is complete.
*/
async init() {
///
// 1: Create agent
@ -66,6 +90,17 @@ class SystemQuery extends EventEmitter {
return await once(this.agent, `message-${event_name}`, { signal: ac });
}
/**
* Fetches the table with the given name from all peers in the swarm
* (including this local instance).
* While this function iteratively yields tables as they are received, it
* will wait until all peers respond before finally ending.
* If 1 of more peers do not respond, then if no answers are received
* within the time limit specified in the config object (supplied at
* startup) this function will return.
* @param {string} name The name of the table to fetch.
* @return {AsyncGenerator<Object>} An asynchronous generator that yields the tables as they are received.
*/
async *fetch_table(name) {
// If it isn't valid for us, it ain't gonna be valid for anyone else....
if(!this.info.is_valid_table(name)) return null;
@ -73,8 +108,7 @@ class SystemQuery extends EventEmitter {
const queue = new ItemQueue();
const handle_response = (peer, msg) => {
// TODO: Validate response. Is it the right table? Is it even a table? Note that multiple fetch_table calls may be running in parallel, so we should not make too much of a fuss if we get the wrong table by accident.
// TODO: It would be seriously cool to have fetch_table() be an async generator that yields pairs of peer ids and tables as they come in.
// Note that multiple fetch_table calls may be running in parallel, so we should not make too much of a fuss if we get the wrong table by accident.
if(typeof msg !== "object"
|| typeof msg.table !== "object"
|| typeof msg.name !== "string") {
@ -96,8 +130,12 @@ class SystemQuery extends EventEmitter {
this.agent.broadcast(`query`, { name });
// TODO: Yield a table value for us too
// TODO: Handle duplicate connections better by both skipping counting them here, and also implementing a more robust reaper for killing duplicate connections that always kills the newest/oldest connection to avoid issues.
// Yield our result first
let table_ours = await this.info.fetch_table(name);
yield {
peer: this.peer_local,
table: table_ours
};
let peers_seen = [];
while(peers_seen.length < this.agent.connected_peers.length) {

View file

@ -14,6 +14,9 @@ import parse_peer_name from '../parse/peer_name.mjs';
import hostuuid from '../io/hostuuid.mjs';
class Agent extends EventEmitter {
get local_port() { return this.config.net.port; }
get local_bind_address() { return this.config.net.bind_address; }
get connected_peers() { return this.server.connected_peers; }
get connecting_peers() { return this.server.connecting_peers; }
@ -101,7 +104,7 @@ class Agent extends EventEmitter {
async __send_peer_list(peer, _msg) {
l.log(`Sending peer list to ${peer.id_short}`);
await peer.send("peerlist-response", {
peers: this.server.peers()
peers: this.server.peers().map(peer_next => peer_next.info)
});
}

View file

@ -17,6 +17,16 @@ class Peer extends EventEmitter {
return this.id.substring(0, 7);
}
get info() {
return {
address: this.address,
port: this.port,
id: this.id,
listening_address: this.listening_address,
listening_port: this.listening_port
};
}
constructor(server, connection) {
super();

View file

@ -82,7 +82,7 @@ class PeerServer extends EventEmitter {
peer.on("message", this.handle_message.bind(this, peer));
peer.on("destroy", this.handle_destroy.bind(this, peer));
l.log(`Peer ${peer.id_short} from ${peer.address}:${peer.port} connected`);
l.log(`Peer ${peer.id_short} from ${peer.address}:${peer.port} connected (currently connected: ${this.peers_dedupe().map(peer_next => peer_next.id_short).join(", ")})`);
this.emit("peer", peer);
}
@ -109,13 +109,22 @@ class PeerServer extends EventEmitter {
* @return {{address:string,port:number}[]}
*/
peers() {
return this.connected_peers.map(peer => { return {
address: peer.address,
port: peer.port,
id: peer.id,
listening_address: peer.listening_address,
listening_port: peer.listening_port
}}).filter(el => typeof el.address === "string" && typeof el.port === "number");
return this.connected_peers
.filter(el => typeof el.address === "string" && typeof el.port === "number");
}
/**
* Like .peers(), but deduplicates peers by their ID.
* @return {Object[]} [description]
*/
peers_dedupe() {
let deduped = new Map();
for(let peer of this.peers()) {
deduped.set(peer.id, peer);
}
return Array.from(deduped.values());
}
/**
@ -125,6 +134,9 @@ class PeerServer extends EventEmitter {
*/
peer_resolve(peer_id) {
if(peer_id instanceof Peer) return peer_id;
if(typeof peer_id !== "string")
throw new Error(`Expected variable of type string or Peer, got variable of type ${typeof peer_id}`);
for (let peer of this.connected_peers) {
if(peer.id === peer_id) return peer;
}
@ -138,7 +150,7 @@ class PeerServer extends EventEmitter {
* @return {Peer[]} A list of Peer instances.
*/
peers_resolve(...peers) {
return peers.map(this.peer_resolve);
return peers.map(peer => this.peer_resolve(peer));
}
/**
@ -228,8 +240,11 @@ class PeerServer extends EventEmitter {
// connection
if(this.peers().some(existing_peer => existing_peer.id === peer.id)) {
l.info(`Closing duplicate connection to ${peer.id_short}`);
this.remove_peers(peer);
await this.remove_peers(peer);
}
if(this.connecting_peers.filter(other_peer => other_peer.id !== null)
.some(other_peer => other_peer.id === peer.id))
await this.remove_peers(peer);
this.peer_initialise(peer);
return peer;
@ -265,7 +280,8 @@ class PeerServer extends EventEmitter {
* @return {Promise} A Promise that resolves (or potentially rejects) when the message has been sent.
*/
async broadcast(event_name, msg) {
await this.send(this.connected_peers, event_name, msg);
let peers = this.peers_dedupe();
await this.send(peers, event_name, msg);
}
/**