blob: 690f1de897bc931d148ff9ccc4246961a6d47e17 [file] [log] [blame]
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart';
import 'byte_store.dart';
import 'cache.dart';
import 'file_byte_store.dart';
/// The function that returns current time in milliseconds.
typedef int GetCurrentTime();
/// [ByteStore] that stores values as files, allows to mark some of keys as
/// temporary protected, and supports periodical [flush] of unprotected keys.
///
/// The set of protected keys is stored in a file, which is locked during
/// updating to prevent races across multiple processes.
class ProtectedFileByteStore implements ByteStore {
@visibleForTesting
static const PROTECTED_FILE_NAME = '.temporary_protected_keys';
final String _cachePath;
final Duration _protectionDuration;
final GetCurrentTime _getCurrentTimeFunction;
final FileByteStore _fileByteStore;
final Cache<String, List<int>> _cache;
/// Create a new instance of the [ProtectedFileByteStore].
///
/// The [protectionDuration] specifies how long temporary protected keys
/// stay protected.
ProtectedFileByteStore(this._cachePath,
{Duration protectionDuration,
GetCurrentTime getCurrentTime,
int cacheSizeBytes: 128 * 1024 * 1024})
: _protectionDuration = protectionDuration,
_getCurrentTimeFunction = getCurrentTime ?? _getCurrentTimeDefault,
_fileByteStore = new FileByteStore(_cachePath),
_cache = new Cache(cacheSizeBytes, (bytes) => bytes.length);
/// Remove all not protected keys.
void flush() {
var protectedKeysText = _keysReadTextLocked();
var protectedKeys = new ProtectedKeys.decode(protectedKeysText);
List<FileSystemEntity> files = new Directory(_cachePath).listSync();
for (var file in files) {
if (file is File) {
String key = basename(file.path);
if (key == PROTECTED_FILE_NAME) {
continue;
}
if (protectedKeys.containsKey(key)) {
continue;
}
try {
file.deleteSync();
} catch (e) {}
}
}
}
@override
List<int> get(String key) {
return _cache.get(key, () => _fileByteStore.get(key));
}
@override
void put(String key, List<int> bytes) {
if (key == PROTECTED_FILE_NAME) {
throw new ArgumentError('The key $key is reserved.');
}
_fileByteStore.put(key, bytes);
_cache.put(key, bytes);
}
/// The [add] keys are added to the set of temporary protected keys, and
/// their age is reset to zero.
///
/// The [remove] keys are removed from the set of temporary protected keys,
/// and become subjects of LRU cached eviction.
void updateProtectedKeys(
{List<String> add: const <String>[],
List<String> remove: const <String>[]}) {
_withProtectedKeysLockSync(_cachePath, (ProtectedKeys protectedKeys) {
var now = _getCurrentTimeFunction();
if (_protectionDuration != null) {
var maxAge = _protectionDuration.inMilliseconds;
protectedKeys.removeOlderThan(maxAge, now);
}
for (var addedKey in add) {
protectedKeys.add(addedKey, now);
}
for (var removedKey in remove) {
protectedKeys.remove(removedKey);
}
});
}
/// Read the protected keys, but don't keep the lock.
///
/// We do this before performing any long running operation that
/// just read, and where it is important to keep system unlocked.
String _keysReadTextLocked() {
File keysFile = new File(join(_cachePath, PROTECTED_FILE_NAME));
RandomAccessFile keysLock = keysFile.openSync(mode: FileMode.APPEND);
keysLock.lockSync(FileLock.BLOCKING_EXCLUSIVE);
try {
return _keysReadText(keysLock);
} finally {
keysLock.unlockSync();
keysLock.closeSync();
}
}
/// The default implementation of [GetCurrentTime].
static int _getCurrentTimeDefault() {
return new DateTime.now().millisecondsSinceEpoch;
}
static ProtectedKeys _keysRead(RandomAccessFile file) {
String text = _keysReadText(file);
return new ProtectedKeys.decode(text);
}
static String _keysReadText(RandomAccessFile file) {
file.setPositionSync(0);
List<int> bytes = file.readSync(file.lengthSync());
return utf8.decode(bytes);
}
static void _keysWrite(RandomAccessFile file, ProtectedKeys keys) {
String text = keys.encode();
file.setPositionSync(0);
file.writeStringSync(text);
file.truncateSync(file.positionSync());
}
/// Perform [f] over the locked keys file, decoded into [ProtectedKeys].
static void _withProtectedKeysLockSync(
String cachePath, void f(ProtectedKeys keys)) {
String path = join(cachePath, PROTECTED_FILE_NAME);
RandomAccessFile file = new File(path).openSync(mode: FileMode.APPEND);
file.lockSync(FileLock.BLOCKING_EXCLUSIVE);
try {
ProtectedKeys keys = _keysRead(file);
f(keys);
_keysWrite(file, keys);
} finally {
file.unlockSync();
file.closeSync();
}
}
}
/// Container with protected keys.
@visibleForTesting
class ProtectedKeys {
/// The map from a key in [ByteStore] to the time in milliseconds when the
/// key was marked as temporary protected.
final Map<String, int> map;
ProtectedKeys(this.map);
factory ProtectedKeys.decode(String text) {
var map = <String, int>{};
try {
List<String> lines = text.split('\n').toList();
if (lines.length % 2 == 0) {
for (int i = 0; i < lines.length; i += 2) {
String key = lines[i];
String startMillisecondsStr = lines[i + 1];
int startMilliseconds = int.parse(startMillisecondsStr);
map[key] = startMilliseconds;
}
}
} catch (e) {}
return new ProtectedKeys(map);
}
/// Add the given [key] with the current time.
void add(String key, int time) {
map[key] = time;
}
bool containsKey(String key) => map.containsKey(key);
String encode() {
var buffer = new StringBuffer();
map.forEach((key, start) {
buffer.writeln(key);
buffer.writeln(start);
});
return buffer.toString().trim();
}
void remove(String key) {
map.remove(key);
}
/// If the time is [now] milliseconds, remove all keys that are older than
/// the given [maxAge] is milliseconds.
void removeOlderThan(int maxAge, int now) {
var keysToRemove = <String>[];
for (var key in map.keys) {
if (now - map[key] > maxAge) {
keysToRemove.add(key);
}
}
keysToRemove.forEach(map.remove);
}
}