Skip to content
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

Update RequestCache #112

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
15 changes: 6 additions & 9 deletions lib/src/services/api_call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,16 @@ class ApiCall extends BaseApiCall<Map<String, dynamic>> {
bool shouldCacheResult = false,
}) =>
shouldCacheResult && config.cachedSearchResultsTTL != Duration.zero
? _requestCache.cache(
? _requestCache.getResponse(
// SplayTreeMap ensures order of the parameters is maintained so
// cache key won't differ because of different ordering of
// parameters.
'$endpoint${SplayTreeMap.from(queryParams)}'.hashCode,
send,
'$endpoint${SplayTreeMap.from(queryParams)}',
(node) => node.client.get(
requestUri(node, endpoint, queryParams),
headers: defaultHeaders,
),
config.cachedSearchResultsTTL,
send,
)
: send((node) => node.client.get(
requestUri(node, endpoint, queryParams),
Expand Down Expand Up @@ -80,19 +79,17 @@ class ApiCall extends BaseApiCall<Map<String, dynamic>> {
bool shouldCacheResult = false,
}) =>
shouldCacheResult && config.cachedSearchResultsTTL != Duration.zero
? _requestCache.cache(
? _requestCache.getResponse(
// SplayTreeMap ensures order of the parameters is maintained so
// cache key won't differ because of different ordering of
// parameters.
'$endpoint${SplayTreeMap.from(queryParams)}${SplayTreeMap.from(additionalHeaders)}${json.encode(bodyParameters)}'
.hashCode,
send,
'$endpoint${SplayTreeMap.from(queryParams)}${SplayTreeMap.from(additionalHeaders)}${json.encode(bodyParameters)}',
(node) => node.client.post(
requestUri(node, endpoint, queryParams),
headers: {...defaultHeaders, ...additionalHeaders},
body: json.encode(bodyParameters),
),
config.cachedSearchResultsTTL,
send,
)
: send((node) => node.client.post(
requestUri(node, endpoint, queryParams),
Expand Down
3 changes: 2 additions & 1 deletion lib/src/services/base_api_call.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:http/http.dart' as http;

import 'typedefs.dart';
import 'node_pool.dart';
import '../configuration.dart';
import '../models/node.dart';
Expand Down Expand Up @@ -51,7 +52,7 @@ abstract class BaseApiCall<R extends Object> {
///
/// Also sets the health status of nodes after each request so it can be put
/// in/out of [NodePool]'s circulation.
Future<R> send(Future<http.Response> Function(Node) request) async {
Future<R> send(Request request) async {
http.Response response;
Node node;
for (var triesLeft = config.numRetries;;) {
Expand Down
50 changes: 21 additions & 29 deletions lib/src/services/request_cache.dart
Original file line number Diff line number Diff line change
@@ -1,44 +1,36 @@
import 'dart:collection';
import 'package:dcache/dcache.dart';

import 'package:http/http.dart' as http;

import '../models/node.dart';
import 'typedefs.dart';

/// Cache store which uses a [HashMap] internally to serve requests.
class RequestCache {
final _cachedResponses = HashMap<int, _Cache>();
Cache<String, Map<String, dynamic>> _cachedResponses;
final _cachedTimestamp = HashMap<String, DateTime>();
final Duration timeToUse;
final int size;

RequestCache(this.size, this.timeToUse) {
_cachedResponses = LruCache<String, Map<String, dynamic>>(storage: InMemoryStorage(size));
}

/// Caches the response of the [request], identified by [key]. The cached
/// response is valid till [cacheTTL].
Future<Map<String, dynamic>> cache(
int key,
Future<Map<String, dynamic>> Function(Future<http.Response> Function(Node))
send,
Future<http.Response> Function(Node) request,
Duration cacheTTL,
Future<Map<String, dynamic>> getResponse(
String key,
Request request,
Send<Map<String, dynamic>> send
) async {
if (_cachedResponses.containsKey(key)) {
if (_isCacheValid(_cachedResponses[key], cacheTTL)) {
harisarang marked this conversation as resolved.
Show resolved Hide resolved
// Cache entry is still valid, return it
return Future.value(_cachedResponses[key].data);
} else {
// Cache entry has expired, so delete it explicitly
_cachedResponses.remove(key);
}
if (_cachedResponses.containsKey(key) && _isCacheValid(key)) {
return Future<Map<String, dynamic>>.value(_cachedResponses.get(key));
}

final response = await send(request);
_cachedResponses[key] = _Cache(response, DateTime.now());
var response = await send(request);
_cachedResponses.set(key, response);
_cachedTimestamp[key] = DateTime.now();
return response;
}

bool _isCacheValid(_Cache cache, Duration cacheTTL) =>
DateTime.now().difference(cache.creationTime) < cacheTTL;
}

class _Cache {
final DateTime creationTime;
final Map<String, dynamic> data;

const _Cache(this.data, this.creationTime);
bool _isCacheValid(String key) =>
DateTime.now().difference(_cachedTimestamp[key]) < timeToUse;
}
6 changes: 6 additions & 0 deletions lib/src/services/typedefs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:http/http.dart' as http;

import '../models/node.dart';

typedef Request = Future<http.Response> Function(Node);
typedef Send<R> = Future<R> Function(Request);
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies:
http: ^0.13.3
crypto: ^3.0.1
equatable: ^2.0.2
dcache: ^0.4.0

dev_dependencies:
test: ^1.17.7
Expand Down
31 changes: 30 additions & 1 deletion test/configuration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ void main() {
retryInterval: Duration(seconds: 3),
sendApiKeyAsQueryParam: true,
cachedSearchResultsTTL: Duration(seconds: 30),
cacheCapacity: 101,
);

group('Configuration', () {
Expand Down Expand Up @@ -61,9 +62,12 @@ void main() {
test('has a sendApiKeyAsQueryParam field', () {
expect(config.sendApiKeyAsQueryParam, isTrue);
});
test('has a cacheSearchResults field', () {
test('has a cacheSearchResultsTTL field', () {
expect(config.cachedSearchResultsTTL, equals(Duration(seconds: 30)));
});
test('has a cacheCapacity field', () {
expect(config.cacheCapacity, equals(101));
});
});

group('Configuration initialization', () {
Expand Down Expand Up @@ -180,6 +184,31 @@ void main() {
);
expect(config.retryInterval, equals(Duration(milliseconds: 100)));
});
test('with missing cacheCapacity, sets cacheCapacity to 100', () {
final config = Configuration(
apiKey: 'abc123',
connectionTimeout: Duration(seconds: 10),
healthcheckInterval: Duration(seconds: 5),
nearestNode: Node(
protocol: 'http',
host: 'localhost',
path: '/path/to/service',
),
nodes: {
Node(
protocol: 'https',
host: 'localhost',
path: '/path/to/service',
),
},
numRetries: 5,
retryInterval: Duration(seconds: 3),
sendApiKeyAsQueryParam: true,
cachedSearchResultsTTL: Duration(seconds: 30),
);

expect(config.cacheCapacity, equals(100));
});
test(
'with missing sendApiKeyAsQueryParam, sets sendApiKeyAsQueryParam to false',
() {
Expand Down
Loading