Skip to content

Commit

Permalink
Merge pull request #401 from atsign-foundation/feat/gkc-add-root-look…
Browse files Browse the repository at this point in the history
…up-retry
  • Loading branch information
gkc authored Sep 3, 2023
2 parents 99a0c79 + c7e1836 commit a1ffec7
Show file tree
Hide file tree
Showing 8 changed files with 276 additions and 119 deletions.
7 changes: 7 additions & 0 deletions packages/at_lookup/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 3.0.40
- feat: make `SecondaryUrlFinder` (atServer address lookup) resilient to
transient failures to reach an atDirectory
- feat: made `retryDelaysMillis` a public static variable
in `SecondaryUrlFinder`; this allows clients to control
- (1) how many retries are done and
- (2) the delay after each subsequent retry
## 3.0.39
- feat: Changes for apkam
- chore: Upgraded at_commons to 3.0.53 and at_utils to 3.0.15
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:at_commons/at_commons.dart';
import 'package:at_lookup/src/cache/secondary_address_finder.dart';
import 'package:at_lookup/at_lookup.dart';
import 'package:at_lookup/src/util/lookup_util.dart';
import 'package:at_utils/at_logger.dart';

Expand Down Expand Up @@ -116,10 +116,30 @@ class SecondaryAddressCacheEntry {
SecondaryAddressCacheEntry(this.secondaryAddress, this.expiresAt);
}

/// When SecondaryUrlFinder tries to lookup the atServer's address from an
/// atDirectory, it may encounter intermittent failures for various reasons -
/// 'network weather', service glitches, etc.
///
/// In order to be resilient to such failures, we implement retries.
///
/// The static variable [retryDelaysMillis] controls
/// (a) how many retries are done, and
/// (b) the delay before each retry
class SecondaryUrlFinder {
final String _rootDomain;
final int _rootPort;
SecondaryUrlFinder(this._rootDomain, this._rootPort);
late final AtLookupSecureSocketFactory _socketFactory;

SecondaryUrlFinder(this._rootDomain, this._rootPort, {AtLookupSecureSocketFactory? socketFactory}) {
_socketFactory = socketFactory ?? AtLookupSecureSocketFactory();
}

final _logger = AtSignLogger('SecondaryUrlFinder');

/// Controls
/// (a) how many retries are done, and
/// (b) the delay before each retry
static List<int> retryDelaysMillis = [50, 100, 150, 200];

Future<String?> findSecondaryUrl(String atSign) async {
if (_rootDomain.startsWith("proxy:")) {
Expand All @@ -130,26 +150,47 @@ class SecondaryUrlFinder {
// and the secondary port will be deemed to be the rootPort
return '${_rootDomain.substring("proxy:".length)}:$_rootPort';
} else {
return await _findSecondary(atSign);
String? address;
for (int i = 0; i <= retryDelaysMillis.length; i++) {
try {
address = await _findSecondary(atSign);
return address;
} catch (e) {
if (i == retryDelaysMillis.length) {
_logger.severe('AtLookup.findSecondary $atSign failed with $e'
' : ${retryDelaysMillis.length + 1} failures, giving up');
rethrow;
} else {
_logger.info('AtLookup.findSecondary $atSign failed with $e'
' : will retry in ${retryDelaysMillis[i]} milliseconds');
await Future.delayed(Duration(milliseconds: retryDelaysMillis[i]));
}
}
}
throw AtConnectException(
'CacheableSecondaryAddressFinder.SecondaryUrlFinder.findSecondaryUrl'
' : ${retryDelaysMillis.length + 1} failures, giving up');
}
}

Future<String?> _findSecondary(String atsign) async {
String? response;
SecureSocket? socket;
try {
AtSignLogger('AtLookup')
.finer('AtLookup.findSecondary received atsign: $atsign');
_logger.finer('AtLookup.findSecondary received atsign: $atsign');
if (atsign.startsWith('@')) atsign = atsign.replaceFirst('@', '');
var answer = '';
String? secondary;
var ans = false;
var prompt = false;
var once = true;
// ignore: omit_local_variable_types
socket = await SecureSocket.connect(_rootDomain, _rootPort);

socket = await _socketFactory.createSocket(
_rootDomain, '$_rootPort', SecureSocketConfig());

// listen to the received data event stream
socket.listen((List<int> event) async {
_logger.finest('root socket listener received: $event');
answer = utf8.decode(event);

if (answer.endsWith('@') && prompt == false && once == true) {
Expand Down Expand Up @@ -178,7 +219,7 @@ class SecondaryUrlFinder {
socket.write('@exit\n');
await socket.flush();
socket.destroy();
AtSignLogger('AtLookup').finer(
_logger.finer(
'AtLookup.findSecondary got answer: $secondary and closing connection');
return response;
}
Expand All @@ -187,11 +228,11 @@ class SecondaryUrlFinder {
await socket.flush();
socket.destroy();
throw AtTimeoutException('AtLookup.findSecondary timed out');
} on SocketException {
} on SocketException catch (se) {
throw RootServerConnectivityException(
'Failed connecting to root server url $_rootDomain on port $_rootPort');
'_findSecondary caught exception [$se] while connecting to root server url $_rootDomain on port $_rootPort');
} on Exception catch (exception) {
AtSignLogger('AtLookup').severe('AtLookup.findSecondary connection to ' +
_logger.severe('AtLookup.findSecondary connection to ' +
_rootDomain +
' exception: ' +
exception.toString());
Expand All @@ -202,9 +243,10 @@ class SecondaryUrlFinder {
_rootDomain +
' exception: ' +
exception.toString());
} catch (error) {
AtSignLogger('AtLookup').severe(
} catch (error, stackTrace) {
_logger.severe(
'AtLookup.findSecondary connection to root server failed with error: $error');
_logger.severe(stackTrace);
if (socket != null) {
socket.destroy();
}
Expand Down
2 changes: 1 addition & 1 deletion packages/at_lookup/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: at_lookup
description: A Dart library that contains the core commands that can be used with a secondary server (scan, update, lookup, llookup, plookup, etc.)
version: 3.0.39
version: 3.0.40
repository: https://github.com/atsign-foundation/at_libraries
homepage: https://atsign.com
documentation: https://docs.atsign.com/
Expand Down
68 changes: 14 additions & 54 deletions packages/at_lookup/test/at_lookup_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,54 +8,12 @@ import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:at_utils/at_logger.dart';

import 'connection_management_test.dart';

class MockOutboundConnectionImpl extends Mock
implements OutboundConnectionImpl {}

class MockSecondaryAddressFinder extends Mock
implements SecondaryAddressFinder {}

class MockOutboundMessageListener extends Mock
implements OutboundMessageListener {}

late int mockSocketNumber;

class MockSecureSocket extends Mock implements SecureSocket {
bool destroyed = false;
int mockNumber = mockSocketNumber++;
}

class MockSecureSocketFactory extends Mock
implements AtLookupSecureSocketFactory {}

class MockSecureSocketListenerFactory extends Mock
implements AtLookupSecureSocketListenerFactory {}

class MockOutboundConnectionFactory extends Mock
implements AtLookupOutboundConnectionFactory {}

class MockAtChops extends Mock implements AtChopsImpl {}
import 'at_lookup_test_utils.dart';

class FakeAtSigningInput extends Fake implements AtSigningInput {}

SecureSocket createMockSecureSocket() {
SecureSocket mss = MockSecureSocket();
when(() => mss.destroy()).thenAnswer((invocation) {
(mss as MockSecureSocket).destroyed = true;
});
when(() => mss.setOption(SocketOption.tcpNoDelay, true)).thenReturn(true);
when(() => mss.remoteAddress).thenReturn(InternetAddress('127.0.0.1'));
when(() => mss.remotePort).thenReturn(12345);
when(() => mss.listen(any(),
onError: any(named: "onError"),
onDone: any(named: "onDone"))).thenReturn(MockStreamSubscription());
return mss;
}

void main() {
AtSignLogger.root_level = 'finest';
mockSocketNumber = 1;
late OutboundConnection mockOutBoundConnection;
late SecondaryAddressFinder mockSecondaryAddressFinder;
late OutboundMessageListener mockOutboundListener;
Expand All @@ -66,6 +24,9 @@ void main() {
late AtChops mockAtChops;
late SecureSocket mockSecureSocket;

String atServerHost = '127.0.0.1';
int atServerPort = 12345;

setUp(() {
mockOutBoundConnection = MockOutboundConnectionImpl();
mockSecondaryAddressFinder = MockSecondaryAddressFinder();
Expand All @@ -75,15 +36,14 @@ void main() {
mockOutboundConnectionFactory = MockOutboundConnectionFactory();
mockAtChops = MockAtChops();
registerFallbackValue(SecureSocketConfig());
mockSecureSocket = createMockSecureSocket();
mockSecureSocket = createMockAtServerSocket(atServerHost, atServerPort);

when(() => mockSecondaryAddressFinder.findSecondary('@alice'))
.thenAnswer((_) async {
return SecondaryAddress('127.0.0.1', 12345);
return SecondaryAddress(atServerHost, atServerPort);
});
when(() => mockSocketFactory.createSocket('127.0.0.1', '12345', any()))
when(() => mockSocketFactory.createSocket(atServerHost, '12345', any()))
.thenAnswer((invocation) {
print('Mock SecureSocketFactory returning mock socket');
return Future<SecureSocket>.value(mockSecureSocket);
});
when(() => mockOutboundConnectionFactory
Expand Down Expand Up @@ -130,7 +90,7 @@ void main() {
return Future.value();
});

final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand Down Expand Up @@ -166,7 +126,7 @@ void main() {
return Future.value();
});

final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand Down Expand Up @@ -202,7 +162,7 @@ void main() {
return Future.value();
});

final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand Down Expand Up @@ -239,7 +199,7 @@ void main() {
return Future.value();
});

final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand All @@ -254,7 +214,7 @@ void main() {
});
group('A group of tests to verify executeCommand method', () {
test('executeCommand - from verb - auth false', () async {
final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand All @@ -268,7 +228,7 @@ void main() {
});
test('executeCommand -llookup verb - auth true - auth key not set',
() async {
final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand All @@ -283,7 +243,7 @@ void main() {
});

test('executeCommand -llookup verb - auth true - at_chops set', () async {
final atLookup = AtLookupImpl('@alice', '127.0.0.1', 64,
final atLookup = AtLookupImpl('@alice', atServerHost, 64,
secondaryAddressFinder: mockSecondaryAddressFinder,
secureSocketFactory: mockSocketFactory,
socketListenerFactory: mockSecureSocketListenerFactory,
Expand Down
52 changes: 52 additions & 0 deletions packages/at_lookup/test/at_lookup_test_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:async';
import 'dart:io';

import 'package:at_chops/at_chops.dart';
import 'package:at_lookup/at_lookup.dart';
import 'package:at_lookup/src/connection/outbound_message_listener.dart';
import 'package:mocktail/mocktail.dart';

int mockSocketNumber = 1;

class MockSecondaryAddressFinder extends Mock
implements SecondaryAddressFinder {}

class MockSecondaryUrlFinder extends Mock implements SecondaryUrlFinder {}

class MockSecureSocketFactory extends Mock
implements AtLookupSecureSocketFactory {}

class MockStreamSubscription<T> extends Mock implements StreamSubscription<T> {}

class MockSecureSocket extends Mock implements SecureSocket {
bool destroyed = false;
int mockNumber = mockSocketNumber++;
}

class MockSecureSocketListenerFactory extends Mock
implements AtLookupSecureSocketListenerFactory {}

class MockOutboundConnectionFactory extends Mock
implements AtLookupOutboundConnectionFactory {}

class MockOutboundMessageListener extends Mock
implements OutboundMessageListener {}

class MockAtChops extends Mock implements AtChopsImpl {}

class MockOutboundConnectionImpl extends Mock
implements OutboundConnectionImpl {}

SecureSocket createMockAtServerSocket(String address, int port) {
SecureSocket mss = MockSecureSocket();
when(() => mss.destroy()).thenAnswer((invocation) {
(mss as MockSecureSocket).destroyed = true;
});
when(() => mss.setOption(SocketOption.tcpNoDelay, true)).thenReturn(true);
when(() => mss.remoteAddress).thenReturn(InternetAddress('127.0.0.66'));
when(() => mss.remotePort).thenReturn(port);
when(() => mss.listen(any(),
onError: any(named: "onError"),
onDone: any(named: "onDone"))).thenReturn(MockStreamSubscription());
return mss;
}
Loading

0 comments on commit a1ffec7

Please sign in to comment.