fix(social): cap relay connect with a timeout so one hung relay can't stall the pool
This commit is contained in:
parent
3b84ecc7f4
commit
83ffc5cb1d
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).
|
||||
int get relayCount => _connections.length;
|
||||
|
||||
/// Connects to each of [relayUrls], skipping any that fail. Throws
|
||||
/// [StateError] only when NONE are reachable, so the caller can treat that as
|
||||
/// "offline".
|
||||
/// Connects to each of [relayUrls], skipping any that fail or take longer
|
||||
/// than [connectTimeout] — a relay on a silently-filtering network would
|
||||
/// 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(
|
||||
List<String> relayUrls, {
|
||||
required NostrIdentity identity,
|
||||
Duration connectTimeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final connections = <NostrConnection>[];
|
||||
await Future.wait(
|
||||
relayUrls.map((url) async {
|
||||
try {
|
||||
final attempt = NostrConnection.connect(url, identity: identity);
|
||||
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 (_) {
|
||||
// Unreachable relay — skip it, keep the rest.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:commons_core/commons_core.dart';
|
||||
|
|
@ -58,6 +59,38 @@ void main() {
|
|||
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 {
|
||||
final alice = await idFor(4);
|
||||
expect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue