fix(social): cap relay connect with a timeout so one hung relay can't stall the pool
This commit is contained in:
parent
3ec6ed2329
commit
981cf10048
2 changed files with 50 additions and 4 deletions
|
|
@ -26,19 +26,32 @@ class RelayPool implements NostrChannel {
|
||||||
/// Number of relays currently connected (for diagnostics/tests).
|
/// Number of relays currently connected (for diagnostics/tests).
|
||||||
int get relayCount => _connections.length;
|
int get relayCount => _connections.length;
|
||||||
|
|
||||||
/// Connects to each of [relayUrls], skipping any that fail. Throws
|
/// Connects to each of [relayUrls], skipping any that fail or take longer
|
||||||
/// [StateError] only when NONE are reachable, so the caller can treat that as
|
/// than [connectTimeout] — a relay on a silently-filtering network would
|
||||||
/// "offline".
|
/// otherwise hang the whole pool for minutes. Throws [StateError] only when
|
||||||
|
/// NONE are reachable, so the caller can treat that as "offline".
|
||||||
static Future<RelayPool> connect(
|
static Future<RelayPool> connect(
|
||||||
List<String> relayUrls, {
|
List<String> relayUrls, {
|
||||||
required NostrIdentity identity,
|
required NostrIdentity identity,
|
||||||
|
Duration connectTimeout = const Duration(seconds: 10),
|
||||||
}) async {
|
}) async {
|
||||||
final connections = <NostrConnection>[];
|
final connections = <NostrConnection>[];
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
relayUrls.map((url) async {
|
relayUrls.map((url) async {
|
||||||
try {
|
try {
|
||||||
|
final attempt = NostrConnection.connect(url, identity: identity);
|
||||||
connections.add(
|
connections.add(
|
||||||
await NostrConnection.connect(url, identity: identity),
|
await attempt.timeout(
|
||||||
|
connectTimeout,
|
||||||
|
onTimeout: () {
|
||||||
|
// Abandon the hung attempt; if it ever completes, close it so
|
||||||
|
// the socket doesn't leak.
|
||||||
|
unawaited(
|
||||||
|
attempt.then((c) => c.close()).catchError((_) {}),
|
||||||
|
);
|
||||||
|
throw TimeoutException('relay connect timed out', connectTimeout);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Unreachable relay — skip it, keep the rest.
|
// Unreachable relay — skip it, keep the rest.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:io';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:commons_core/commons_core.dart';
|
import 'package:commons_core/commons_core.dart';
|
||||||
|
|
@ -58,6 +59,38 @@ void main() {
|
||||||
await r1.stop();
|
await r1.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a hung relay cannot stall the pool past the connect timeout', () async {
|
||||||
|
// A server that accepts TCP but never answers the WebSocket handshake —
|
||||||
|
// the shape of a silently-filtered network, where connect hangs for minutes.
|
||||||
|
final hang = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||||
|
final held = <Socket>[];
|
||||||
|
hang.listen(held.add);
|
||||||
|
final r1 = await MiniRelay.start();
|
||||||
|
final alice = await idFor(5);
|
||||||
|
|
||||||
|
final sw = Stopwatch()..start();
|
||||||
|
final pool = await RelayPool.connect(
|
||||||
|
[r1.url, 'ws://127.0.0.1:${hang.port}'],
|
||||||
|
identity: alice,
|
||||||
|
connectTimeout: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
sw.stop();
|
||||||
|
|
||||||
|
expect(pool.relayCount, 1, reason: 'the live relay is kept');
|
||||||
|
expect(
|
||||||
|
sw.elapsed,
|
||||||
|
lessThan(const Duration(seconds: 5)),
|
||||||
|
reason: 'the hung relay must not stall the whole pool',
|
||||||
|
);
|
||||||
|
|
||||||
|
await pool.close();
|
||||||
|
for (final s in held) {
|
||||||
|
s.destroy();
|
||||||
|
}
|
||||||
|
await hang.close();
|
||||||
|
await r1.stop();
|
||||||
|
});
|
||||||
|
|
||||||
test('throws when no relay is reachable (caller treats as offline)', () async {
|
test('throws when no relay is reachable (caller treats as offline)', () async {
|
||||||
final alice = await idFor(4);
|
final alice = await idFor(4);
|
||||||
expect(
|
expect(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue