-
Notifications
You must be signed in to change notification settings - Fork 11
add secondary address cache #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
a039f65
faa818f
1cafca7
9e639a4
7150154
dabcc96
39a488e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import 'package:at_commons/at_commons.dart'; | ||
import 'package:at_lookup/at_lookup.dart'; | ||
import 'package:at_lookup/src/util/lookup_util.dart'; | ||
import 'package:at_utils/at_logger.dart'; | ||
|
||
class SecondaryAddressCache { | ||
static const Duration defaultCacheDuration = Duration(hours: 1); | ||
|
||
final Map<String, SecondaryAddressCacheEntry> _map = {}; | ||
final _logger = AtSignLogger('SecondaryAddressCacheImpl'); | ||
|
||
final String _rootDomain; | ||
final int _rootPort; | ||
late SecondaryAddressFinder _secondaryFinder; | ||
|
||
SecondaryAddressCache(this._rootDomain, this._rootPort, {SecondaryAddressFinder? secondaryFinder}) { | ||
_secondaryFinder = secondaryFinder ?? SecondaryAddressFinder(); | ||
} | ||
|
||
bool cacheContains(String atSign) { | ||
return _map.containsKey(stripAtSignFromAtSign(atSign)); | ||
} | ||
|
||
/// Returns the expiry time of this entry in millisecondsSinceEpoch, or null if this atSign is not in cache | ||
int? getCacheExpiryTime(String atSign) { | ||
atSign = stripAtSignFromAtSign(atSign); | ||
if (_map.containsKey(atSign)) { | ||
return _map[atSign]!.expiresAt; | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
Future<SecondaryAddress> getAddress(String atSign, {bool refreshCacheNow = false, Duration cacheFor = defaultCacheDuration}) async { | ||
atSign = stripAtSignFromAtSign(atSign); | ||
|
||
if (refreshCacheNow || _cacheIsEmptyOrExpired(atSign)) { | ||
// _updateCache will either populate the cache, or throw an exception | ||
await _updateCache(atSign, cacheFor); | ||
} | ||
|
||
if (_map.containsKey(atSign)) { | ||
// should always be true, since _updateCache will throw an exception if it fails | ||
return _map[atSign]!.secondaryAddress; | ||
} else { | ||
// but just in case, we'll throw an exception if it's not | ||
throw Exception("Failed to find secondary, in a theoretically impossible way"); | ||
} | ||
} | ||
|
||
bool _cacheIsEmptyOrExpired(String atSign) { | ||
if (_map.containsKey(atSign)) { | ||
SecondaryAddressCacheEntry entry = _map[atSign]!; | ||
if (entry.expiresAt < DateTime.now().millisecondsSinceEpoch) { | ||
// expiresAt is in the past - cache has expired | ||
return true; | ||
} else { | ||
// cache has not yet expired | ||
return false; | ||
} | ||
} else { | ||
// cache is empty | ||
return true; | ||
} | ||
} | ||
|
||
static String stripAtSignFromAtSign(String atSign) { | ||
if (atSign.startsWith('@')) { | ||
atSign = atSign.replaceFirst('@', ''); | ||
} | ||
return atSign; | ||
} | ||
static String getNotFoundExceptionMessage(String atSign) { | ||
return 'Unable to find secondary address for atSign:$atSign'; | ||
} | ||
Future<void> _updateCache(String atSign, Duration cacheFor) async { | ||
try { | ||
String? secondaryUrl = await _secondaryFinder.findSecondary(atSign, _rootDomain, _rootPort); | ||
if (secondaryUrl == null || | ||
secondaryUrl.isEmpty || | ||
secondaryUrl == 'data:null') { | ||
throw SecondaryNotFoundException(getNotFoundExceptionMessage(atSign)); | ||
} | ||
var secondaryInfo = LookUpUtil.getSecondaryInfo(secondaryUrl); | ||
String secondaryHost = secondaryInfo[0]; | ||
int secondaryPort = int.parse(secondaryInfo[1]); | ||
|
||
SecondaryAddress addr = SecondaryAddress(secondaryHost, secondaryPort); | ||
_map[atSign] = SecondaryAddressCacheEntry(addr, DateTime.now().add(cacheFor).millisecondsSinceEpoch); | ||
} on Exception catch (e) { | ||
_logger.severe('${getNotFoundExceptionMessage(atSign)} - ${e.toString()}'); | ||
rethrow; | ||
} | ||
} | ||
} | ||
|
||
class SecondaryAddress { | ||
final String host; | ||
final int port; | ||
SecondaryAddress(this.host, this.port); | ||
|
||
@override | ||
String toString() { | ||
return '$host:$port'; | ||
} | ||
} | ||
|
||
class SecondaryAddressCacheEntry { | ||
final SecondaryAddress secondaryAddress; | ||
/// milliseconds since epoch | ||
final int expiresAt; | ||
|
||
SecondaryAddressCacheEntry(this.secondaryAddress, this.expiresAt); | ||
} | ||
|
||
class SecondaryAddressFinder { | ||
Future<String?> findSecondary(String atSign, String rootDomain, int rootPort) async { | ||
return await AtLookupImpl.findSecondary(atSign, rootDomain, rootPort); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import 'package:at_commons/at_commons.dart'; | ||
import 'package:at_lookup/src/cache/secondary_address_cache.dart'; | ||
import 'package:test/expect.dart'; | ||
import 'package:test/scaffolding.dart'; | ||
import 'package:mocktail/mocktail.dart'; | ||
|
||
class MockSecondaryFinder extends Mock implements SecondaryAddressFinder {} | ||
|
||
void main() async { | ||
String rootDomain = 'root.atsign.unit.tests'; | ||
int rootPort = 64; | ||
SecondaryAddressFinder mockSecondaryFinder = MockSecondaryFinder(); | ||
|
||
String _addressFromAtSign(String atSign) { | ||
if (atSign.startsWith('@')) { | ||
atSign = atSign.replaceFirst('@', ''); | ||
} | ||
return '$atSign.secondaries.unit.tests:1001'; | ||
} | ||
|
||
group('this should be moved to functional tests', () { | ||
test('look up @cicd1 from root.atsign.wtf:64', () async { | ||
var secondaryAddress = await SecondaryAddressCache('root.atsign.wtf', 64) | ||
.getAddress('@cicd1'); | ||
expect(secondaryAddress.port, isNotNull); | ||
expect(secondaryAddress.host, isNotNull); | ||
print(secondaryAddress.toString()); | ||
}); | ||
}); | ||
|
||
group('some cache tests', () { | ||
late SecondaryAddressCache cache; | ||
|
||
setUp(() { | ||
reset(mockSecondaryFinder); | ||
when(() => mockSecondaryFinder.findSecondary( | ||
any(that: startsWith('registered')), rootDomain, rootPort)) | ||
.thenAnswer((invocation) async => | ||
_addressFromAtSign(invocation.positionalArguments.first)); | ||
when(() => mockSecondaryFinder.findSecondary( | ||
any(that: startsWith('notCached')), rootDomain, rootPort)) | ||
.thenAnswer((invocation) async => | ||
_addressFromAtSign(invocation.positionalArguments.first)); | ||
when(() => mockSecondaryFinder.findSecondary( | ||
any(that: startsWith('notRegistered')), rootDomain, rootPort)) | ||
.thenAnswer((invocation) async { | ||
throw SecondaryNotFoundException( | ||
SecondaryAddressCache.getNotFoundExceptionMessage( | ||
invocation.positionalArguments.first)); | ||
}); | ||
|
||
cache = SecondaryAddressCache(rootDomain, rootPort, | ||
secondaryFinder: mockSecondaryFinder); | ||
}); | ||
|
||
test('test simple lookup for @registeredAtSign1', () async { | ||
var atSign = '@registeredAtSign1'; | ||
var secondaryAddress = await cache.getAddress(atSign); | ||
expect(secondaryAddress.port, isNotNull); | ||
expect(secondaryAddress.host, isNotNull); | ||
expect(secondaryAddress.toString(), _addressFromAtSign(atSign)); | ||
}); | ||
test('test simple lookup for registeredAtSign1', () async { | ||
var atSign = 'registeredAtSign1'; | ||
var secondaryAddress = await cache.getAddress(atSign); | ||
expect(secondaryAddress.port, isNotNull); | ||
expect(secondaryAddress.host, isNotNull); | ||
expect(secondaryAddress.toString(), _addressFromAtSign(atSign)); | ||
}); | ||
test('test simple lookup for notRegisteredAtSign1', () async { | ||
var atSign = 'notRegisteredAtSign1'; | ||
expect( | ||
() async => await cache.getAddress(atSign), | ||
throwsA(predicate((e) => | ||
e is SecondaryNotFoundException && | ||
e.message == | ||
SecondaryAddressCache.getNotFoundExceptionMessage(atSign)))); | ||
}); | ||
test('test isCached for registeredAtSign1', () async { | ||
var atSign = 'registeredAtSign1'; | ||
await cache.getAddress(atSign); | ||
expect(cache.cacheContains(atSign), true); | ||
}); | ||
test('test isCached for notRegisteredAtSign1', () async { | ||
var atSign = 'notRegisteredAtSign1'; | ||
expect(cache.cacheContains(atSign), false); | ||
}); | ||
|
||
test('test expiry time - default cache expiry for registeredAtSign1', | ||
() async { | ||
var atSign = 'registeredAtSign1'; | ||
await cache.getAddress(atSign); | ||
final approxExpiry = | ||
DateTime.now().add(Duration(hours: 1)).millisecondsSinceEpoch; | ||
expect(cache.getCacheExpiryTime(atSign), isNotNull); | ||
expect((approxExpiry - cache.getCacheExpiryTime(atSign)!) < 100, true); | ||
}); | ||
|
||
test('test expiry time - custom cache expiry for registeredAtSign1', | ||
() async { | ||
var atSign = 'registeredAtSign1'; | ||
await cache.getAddress(atSign, cacheFor: Duration(seconds: 30)); | ||
final approxExpiry = | ||
DateTime.now().add(Duration(seconds: 30)).millisecondsSinceEpoch; | ||
expect(cache.getCacheExpiryTime(atSign), isNotNull); | ||
expect((approxExpiry - cache.getCacheExpiryTime(atSign)!) < 100, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice! |
||
}); | ||
|
||
test('test update cache for atsign which is not yet cached', () async { | ||
var atSign = 'notCachedAtSign1'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe add an assertion that the atsign is not cached before calling cache.getAddress There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
await cache.getAddress(atSign, refreshCacheNow: true); | ||
expect(cache.cacheContains(atSign), true); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have been wondering more about this. One of the problems we have is when we throw an exception. So I think we should just return the cached value (even if it is stale, it's better than throwing an exception) - and perhaps for visibility, add another field to the CacheEntry like 'lookupException' which would be 'most recent cache refresh fail reason'; would be set when we catch an exception, would be cleared when a lookup succeeds.