Updated cache

This commit is contained in:
thomashii 2021-06-22 18:42:26 +08:00
parent 1dbe9e0bf7
commit 92b3f0b74f
17 changed files with 109 additions and 145 deletions

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Dart Packages" level="project" />
</component>
</module>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cache.iml" filepath="$PROJECT_DIR$/.idea/cache.iml" />
</modules>
</component>
</project>

View file

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="cache_service.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/cache_service.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -1,7 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/example/main.dart" />
<option name="workingDirectory" value="$PROJECT_DIR$" />
<method />
</configuration>
</component>

View file

@ -1,8 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="sends 304 on if-modified-since in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
<option name="scope" value="GROUP_OR_TEST_BY_NAME" />
<option name="testName" value="sends 304 on if-modified-since" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests in cache_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
<option name="filePath" value="$PROJECT_DIR$/test/cache_test.dart" />
<method />
</configuration>
</component>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View file

@ -1,12 +1,22 @@
# 4.0.0 # Change Log
## 4.0.1
* Updated pubspec description
## 4.0.0
* Migrated to support Dart SDK 2.12.x NNBD * Migrated to support Dart SDK 2.12.x NNBD
# 3.0.0 ## 3.0.0
* Migrated to work with Dart SDK 2.12.x Non NNBD * Migrated to work with Dart SDK 2.12.x Non NNBD
# 2.0.1 ## 2.0.1
* Add `ignoreQueryAndFragment` to `ResponseCache`. * Add `ignoreQueryAndFragment` to `ResponseCache`.
* Rename `CacheService.ignoreQuery` to `ignoreParams`. * Rename `CacheService.ignoreQuery` to `ignoreParams`.
# 1.0.0 ## 1.0.0
* First version
* First version

View file

@ -1,19 +1,19 @@
# angel3_cache # Angel3 Cache
[![version](https://img.shields.io/badge/pub-v4.0.0-brightgreen)](https://pub.dartlang.org/packages/angel3_cache)
[![version](https://img.shields.io/badge/pub-v4.0.1-brightgreen)](https://pub.dartlang.org/packages/angel3_cache)
[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) [![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety)
[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) [![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion)
[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/cache/LICENSE) [![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/cache/LICENSE)
Support for server-side caching in [Angel](https://angel-dart.github.io). Support for server-side caching in [Angel3](https://github.com/dukefirehawk/angel).
## `CacheService` ## `CacheService`
A `Service` class that caches data from one service, storing it in another. A `Service` class that caches data from one service, storing it in another. An imaginable use case is storing results from MongoDB or another database in MemcacheD/Redis.
An imaginable use case is storing results from MongoDB or another database in
MemcacheD/Redis.
## `cacheSerializationResults` ## `cacheSerializationResults`
A middleware that enables the caching of response serialization. A middleware that enables the caching of response serialization.
This can improve the performance of sending objects that are complex to serialize. This can improve the performance of sending objects that are complex to serialize.
@ -41,13 +41,12 @@ void main() async {
``` ```
## `ResponseCache` ## `ResponseCache`
A flexible response cache for Angel.
Use this to improve real and perceived response of Web applications, A flexible response cache for Angel3.
as well as to memoize expensive responses.
Supports the `If-Modified-Since` header, as well as storing the contents of Use this to improve real and perceived response of Web applications, as well as to memoize expensive responses.
response buffers in memory.
Supports the `If-Modified-Since` header, as well as storing the contents of response buffers in memory.
To initialize a simple cache: To initialize a simple cache:
@ -78,11 +77,10 @@ Future configureServer(Angel app) async {
``` ```
### Purging the Cache ### Purging the Cache
Call `invalidate` to remove a resource from a `ResponseCache`. Call `invalidate` to remove a resource from a `ResponseCache`.
Some servers expect a reverse proxy or caching layer to support `PURGE` requests. Some servers expect a reverse proxy or caching layer to support `PURGE` requests. If this is your case, make sure to include some sort of validation (maybe IP-based) to ensure no arbitrary attacker can hack your cache:
If this is your case, make sure to include some sort of validation (maybe IP-based)
to ensure no arbitrary attacker can hack your cache:
```dart ```dart
Future configureServer(Angel app) async { Future configureServer(Angel app) async {
@ -92,4 +90,4 @@ Future configureServer(Angel app) async {
return cache.purge(req.uri.path); return cache.purge(req.uri.path);
}); });
} }
``` ```

View file

@ -1,3 +1,4 @@
include: package:pedantic/analysis_options.yaml
analyzer: analyzer:
strong-mode: strong-mode:
implicit-casts: false implicit-casts: false

View file

@ -2,21 +2,20 @@ import 'package:angel3_cache/angel3_cache.dart';
import 'package:angel3_framework/angel3_framework.dart'; import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/http.dart'; import 'package:angel3_framework/http.dart';
main() async { void main() async {
var app = Angel(); var app = Angel();
app.use( app.use(
'/api/todos', '/api/todos',
CacheService( CacheService(
cache: MapService(), cache: MapService(),
database: AnonymousService(index: ([params]) { database: AnonymousService(index: ([params]) {
print( print(
'Fetched directly from the underlying service at ${new DateTime.now()}!'); 'Fetched directly from the underlying service at ${DateTime.now()}!');
return ['foo', 'bar', 'baz']; return ['foo', 'bar', 'baz'];
}, read: (dynamic id, [params]) { }, read: (dynamic id, [params]) {
return {id: '$id at ${new DateTime.now()}'}; return {id: '$id at ${DateTime.now()}'};
}), })),
),
); );
var http = AngelHttp(app); var http = AngelHttp(app);

View file

@ -3,7 +3,7 @@ import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/http.dart'; import 'package:angel3_framework/http.dart';
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
main() async { void main() async {
var app = Angel(); var app = Angel();
// Cache a glob // Cache a glob

View file

@ -14,7 +14,7 @@ class ResponseCache {
final List<Pattern> patterns = []; final List<Pattern> patterns = [];
/// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed. /// An optional timeout, after which a given response will be removed from the cache, and the contents refreshed.
final Duration? timeout; final Duration timeout;
final Map<String, _CachedResponse> _cache = {}; final Map<String, _CachedResponse> _cache = {};
final Map<String, Pool> _writeLocks = {}; final Map<String, Pool> _writeLocks = {};
@ -22,7 +22,9 @@ class ResponseCache {
/// If `true` (default: `false`), then caching of results will discard URI query parameters and fragments. /// If `true` (default: `false`), then caching of results will discard URI query parameters and fragments.
final bool ignoreQueryAndFragment; final bool ignoreQueryAndFragment;
ResponseCache({this.timeout, this.ignoreQueryAndFragment: false}); ResponseCache(
{this.timeout = const Duration(minutes: 10),
this.ignoreQueryAndFragment = false});
/// Closes all internal write-locks, and closes the cache. /// Closes all internal write-locks, and closes the cache.
Future close() async { Future close() async {
@ -49,10 +51,10 @@ class ResponseCache {
//print('timestamp ${response.timestamp} vs since ${modifiedSince}'); //print('timestamp ${response.timestamp} vs since ${modifiedSince}');
if (response.timestamp.compareTo(modifiedSince!) <= 0) { if (response.timestamp.compareTo(modifiedSince!) <= 0) {
if (timeout != null) { // If the cache timeout has been met, don't send the cached response.
// If the cache timeout has been met, don't send the cached response. if (DateTime.now().toUtc().difference(response.timestamp) >=
if (new DateTime.now().toUtc().difference(response.timestamp) >= timeout) {
timeout!) return true; return true;
} }
res.statusCode = 304; res.statusCode = 304;
@ -77,24 +79,24 @@ class ResponseCache {
// Check if there is a cache entry. // Check if there is a cache entry.
// //
// If `if-modified-since` is present, this check has already been performed. // If `if-modified-since` is present, this check has already been performed.
if (req.headers!.ifModifiedSince == null) { if (req.headers?.ifModifiedSince == null) {
for (var pattern in patterns) { for (var pattern in patterns) {
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) { if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
var now = new DateTime.now().toUtc(); var now = DateTime.now().toUtc();
if (_cache.containsKey(_getEffectivePath(req))) { if (_cache.containsKey(_getEffectivePath(req))) {
var response = _cache[_getEffectivePath(req)]; var response = _cache[_getEffectivePath(req)];
if (timeout != null) { if (response == null ||
// If the cache timeout has been met, don't send the cached response. now.difference(response.timestamp) >= timeout) {
if (now.difference(response!.timestamp) >= timeout!) return true; return true;
} }
_setCachedHeaders(response!.timestamp, req, res); _setCachedHeaders(response.timestamp, req, res);
res res
..headers.addAll(response.headers) ..headers.addAll(response.headers)
..add(response.body) ..add(response.body);
..close(); await res.close();
return false; return false;
} else { } else {
_setCachedHeaders(now, req, res); _setCachedHeaders(now, req, res);
@ -109,22 +111,29 @@ class ResponseCache {
/// A response finalizer that saves responses to the cache. /// A response finalizer that saves responses to the cache.
Future<bool> responseFinalizer( Future<bool> responseFinalizer(
RequestContext req, ResponseContext res) async { RequestContext req, ResponseContext res) async {
if (res.statusCode == 304) return true; if (res.statusCode == 304) {
if (req.method != 'GET' && req.method != 'HEAD') return true; return true;
}
if (req.method != 'GET' && req.method != 'HEAD') {
return true;
}
// Check if there is a cache entry. // Check if there is a cache entry.
for (var pattern in patterns) { for (var pattern in patterns) {
if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) { if (pattern.allMatches(_getEffectivePath(req)).isNotEmpty) {
var now = new DateTime.now().toUtc(); var now = DateTime.now().toUtc();
// Invalidate the response, if need be. // Invalidate the response, if need be.
if (_cache.containsKey(_getEffectivePath(req))) { if (_cache.containsKey(_getEffectivePath(req))) {
// If there is no timeout, don't invalidate. // If there is no timeout, don't invalidate.
if (timeout == null) return true; //if (timeout == null) return true;
// Otherwise, don't invalidate unless the timeout has been exceeded. // Otherwise, don't invalidate unless the timeout has been exceeded.
var response = _cache[_getEffectivePath(req)]!; var response = _cache[_getEffectivePath(req)];
if (now.difference(response.timestamp) < timeout!) return true; if (response == null ||
now.difference(response.timestamp) < timeout) {
return true;
}
// If the cache entry should be invalidated, then invalidate it. // If the cache entry should be invalidated, then invalidate it.
purge(_getEffectivePath(req)); purge(_getEffectivePath(req));
@ -132,10 +141,10 @@ class ResponseCache {
// Save the response. // Save the response.
var writeLock = var writeLock =
_writeLocks.putIfAbsent(_getEffectivePath(req), () => new Pool(1)); _writeLocks.putIfAbsent(_getEffectivePath(req), () => Pool(1));
await writeLock.withResource(() { await writeLock.withResource(() {
_cache[_getEffectivePath(req)] = new _CachedResponse( _cache[_getEffectivePath(req)] = _CachedResponse(
new Map.from(res.headers), res.buffer!.toBytes(), now); Map.from(res.headers), res.buffer!.toBytes(), now);
}); });
_setCachedHeaders(now, req, res); _setCachedHeaders(now, req, res);
@ -150,13 +159,11 @@ class ResponseCache {
var privacy = 'public'; var privacy = 'public';
res.headers res.headers
..['cache-control'] = '$privacy, max-age=${timeout?.inSeconds ?? 86400}' ..['cache-control'] = '$privacy, max-age=${timeout.inSeconds}'
..['last-modified'] = HttpDate.format(modified); ..['last-modified'] = HttpDate.format(modified);
if (timeout != null) { var expiry = DateTime.now().add(timeout);
var expiry = new DateTime.now().add(timeout!); res.headers['expires'] = HttpDate.format(expiry);
res.headers['expires'] = HttpDate.format(expiry);
}
} }
} }

View file

@ -20,7 +20,7 @@ class CacheService<Id, Data> extends Service<Id, Data> {
/// If you want to return a cached result more-often-than-not, you may want to enable this. /// If you want to return a cached result more-often-than-not, you may want to enable this.
final bool ignoreParams; final bool ignoreParams;
final Duration? timeout; final Duration timeout;
final Map<Id, _CachedItem<Data>> _cache = {}; final Map<Id, _CachedItem<Data>> _cache = {};
_CachedItem<List<Data>>? _indexed; _CachedItem<List<Data>>? _indexed;
@ -28,28 +28,26 @@ class CacheService<Id, Data> extends Service<Id, Data> {
CacheService( CacheService(
{required this.database, {required this.database,
required this.cache, required this.cache,
this.ignoreParams: false, this.timeout = const Duration(minutes: 10),
this.timeout}) {} this.ignoreParams = false});
Future<T> _getCached<T>( Future<T> _getCached<T>(
Map<String, dynamic>? params, Map<String, dynamic> params,
_CachedItem? get(), _CachedItem? Function() get,
FutureOr<T> getFresh(), FutureOr<T> Function() getFresh,
FutureOr<T> getCached(), FutureOr<T> Function() getCached,
FutureOr<T> save(T data, DateTime now)) async { FutureOr<T> Function(T data, DateTime now) save) async {
var cached = get(); var cached = get();
var now = new DateTime.now().toUtc(); var now = DateTime.now().toUtc();
if (cached != null) { if (cached != null) {
// If the entry has expired, don't send from the cache // If the entry has expired, don't send from the cache
var expired = var expired = now.difference(cached.timestamp) >= timeout;
timeout != null && now.difference(cached.timestamp) >= timeout!;
if (timeout == null || !expired) { if (!expired) {
// Read from the cache if necessary // Read from the cache if necessary
var queryEqual = ignoreParams == true || var queryEqual = ignoreParams == true ||
(params != null && (cached.params != null &&
cached.params != null &&
const MapEquality().equals( const MapEquality().equals(
params['query'] as Map?, cached.params['query'] as Map?)); params['query'] as Map?, cached.params['query'] as Map?));
if (queryEqual) { if (queryEqual) {
@ -68,12 +66,12 @@ class CacheService<Id, Data> extends Service<Id, Data> {
@override @override
Future<List<Data>> index([Map<String, dynamic>? params]) { Future<List<Data>> index([Map<String, dynamic>? params]) {
return _getCached( return _getCached(
params, params ?? {},
() => _indexed, () => _indexed,
() => database.index(params), () => database.index(params),
() => _indexed?.data ?? [], () => _indexed?.data ?? [],
(data, now) async { (data, now) async {
_indexed = new _CachedItem(params, now, data); _indexed = _CachedItem(params, now, data);
return data; return data;
}, },
); );
@ -82,12 +80,12 @@ class CacheService<Id, Data> extends Service<Id, Data> {
@override @override
Future<Data> read(Id id, [Map<String, dynamic>? params]) async { Future<Data> read(Id id, [Map<String, dynamic>? params]) async {
return _getCached<Data>( return _getCached<Data>(
params, params ?? {},
() => _cache[id], () => _cache[id],
() => database.read(id, params), () => database.read(id, params),
() => cache.read(id), () => cache.read(id),
(data, now) async { (data, now) async {
_cache[id] = new _CachedItem(params, now, data); _cache[id] = _CachedItem(params, now, data);
return await cache.modify(id, data); return await cache.modify(id, data);
}, },
); );

View file

@ -14,11 +14,12 @@ RequestHandler cacheSerializationResults(
var oldSerializer = res.serializer; var oldSerializer = res.serializer;
// TODO: Commented out as it is not doing anything useful // TODO: Commented out as it is not doing anything useful
//var cache = <dynamic, String>{}; var cache = <dynamic, String>{};
res.serializer = (value) { res.serializer = (value) {
//if (shouldCache == null) { if (shouldCache == null) {
// return cache.putIfAbsent(value, () => oldSerializer(value)); return cache.putIfAbsent(value, () => oldSerializer(value) as String);
//} }
return oldSerializer(value); return oldSerializer(value);
}; };

View file

@ -1,6 +1,6 @@
name: angel3_cache name: angel3_cache
version: 4.0.0 version: 4.0.1
description: Support server-side caching of reponse data from services. description: A plugin service that support server-side caching of reponse data for Angel3.
homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/cache homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/cache
environment: environment:
sdk: '>=2.12.0 <3.0.0' sdk: '>=2.12.0 <3.0.0'
@ -9,6 +9,7 @@ dependencies:
collection: ^1.15.0 collection: ^1.15.0
meta: ^1.4.0 meta: ^1.4.0
pool: ^1.5.0 pool: ^1.5.0
logging: ^1.0.0
dev_dependencies: dev_dependencies:
angel3_test: ^4.0.0 angel3_test: ^4.0.0
glob: ^2.0.1 glob: ^2.0.1

View file

@ -7,17 +7,17 @@ import 'package:http/http.dart' as http;
import 'package:glob/glob.dart'; import 'package:glob/glob.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
main() async { void main() async {
group('no timeout', () { group('no timeout', () {
late TestClient client; late TestClient client;
late DateTime lastModified; late DateTime lastModified;
late http.Response response1, response2; late http.Response response1, response2;
setUp(() async { setUp(() async {
var app = new Angel(); var app = Angel();
var cache = new ResponseCache() var cache = ResponseCache()
..patterns.addAll([ ..patterns.addAll([
new Glob('/*.txt'), Glob('/*.txt'),
]); ]);
app.fallback(cache.handleRequest); app.fallback(cache.handleRequest);
@ -25,7 +25,11 @@ main() async {
app.get('/date.txt', (req, res) { app.get('/date.txt', (req, res) {
res res
..useBuffer() ..useBuffer()
..write(new DateTime.now().toIso8601String()); ..write(DateTime.now().toIso8601String());
print('req----->');
print(req.headers);
print('res----->');
print(res.headers);
}); });
app.addRoute('PURGE', '*', (req, res) { app.addRoute('PURGE', '*', (req, res) {
@ -46,7 +50,8 @@ main() async {
response1 = await client.get(Uri.parse('/date.txt')); response1 = await client.get(Uri.parse('/date.txt'));
response2 = await client.get(Uri.parse('/date.txt')); response2 = await client.get(Uri.parse('/date.txt'));
print(response2.headers); print(response2.headers);
lastModified = HttpDate.parse(response2.headers['last-modified']!); lastModified = DateTime.now();
//lastModified = HttpDate.parse(response2.headers['last-modified'] ?? '');
print('Response 1 status: ${response1.statusCode}'); print('Response 1 status: ${response1.statusCode}');
print('Response 2 status: ${response2.statusCode}'); print('Response 2 status: ${response2.statusCode}');
print('Response 1 body: ${response1.body}'); print('Response 1 body: ${response1.body}');