blob: ba172fbbbb1485651955bf5f0835f94a50c57cb7 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. 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:typed_data';
import 'package:meta/meta.dart';
import 'package:neat_cache/cache_provider.dart';
import 'package:neat_cache/neat_cache.dart';
import 'package:mutex/mutex.dart';
import 'package:retry/retry.dart';
import 'logging.dart';
/// Service for reading and writing values to a cache for quick access of data.
///
/// If [inMemory] is true, a cache with [inMemoryMaxNumberEntries] number
/// of entries will be created. Otherwise, it will use the default redis cache.
class CacheService {
CacheService({
bool inMemory = false,
int inMemoryMaxNumberEntries = 256,
}) : _provider =
inMemory ? Cache.inMemoryCacheProvider(inMemoryMaxNumberEntries) : Cache.redisCacheProvider(memorystoreUri);
final Mutex m = Mutex();
final CacheProvider<List<int>> _provider;
Cache<Uint8List> get cache => cacheValue ?? Cache<List<int>>(_provider).withCodec<Uint8List>(const _CacheCodec());
@visibleForTesting
Cache<Uint8List>? cacheValue;
/// Google Cloud Memorystore default url.
static Uri memorystoreUri = Uri.parse('redis://10.0.0.4:6379');
/// An arbritary number for how many times we should try to get from cache
/// before giving up.
///
/// Writing to the cache creates a racy condition for when another operation
/// is trying to get the same key. This race condition throws an exception.
@visibleForTesting
static const int maxCacheGetAttempts = 3;
/// Get value of [key] from the subcache [subcacheName]. If the key has no
/// value, call [createFn] to create a value for it, set it, and return it.
///
/// The underlying cache get function is inherently racy as if there is a
/// write operation while a read operation, getting the value can fail. To
/// handle this racy condition, this attempts to get the value [maxCacheGetAttempts]
/// times before giving up. This is because the cache is magnitudes faster
/// than the fallback operation (usually a Datastore query).
Future<Uint8List?> getOrCreate(
String subcacheName,
String key, {
required Future<Uint8List> Function()? createFn,
Duration ttl = const Duration(minutes: 1),
}) async {
Uint8List? value = await _readValue(subcacheName, key);
// If given createFn, update the cache value if the value returned was null.
if (createFn != null && value == null) {
// Try creating the value
value = await createFn();
await set(
subcacheName,
key,
value,
ttl: ttl,
);
}
return value;
}
/// This method is the same as the [getOrCreate] method above except that it
/// enforces locking access.
///
/// Note: these methods are intended to prevent issues around race conditions
/// when storing and retrieving github tokens locally only for this instance.
/// Care should be taken to use the locking methods together when accessing
/// data from an entity using the cache.
Future<Uint8List?> getOrCreateWithLocking(
String subcacheName,
String key, {
required Future<Uint8List> Function()? createFn,
Duration ttl = const Duration(minutes: 1),
}) async {
Uint8List? value = await _readValue(subcacheName, key);
// If given createFn, update the cache value if the value returned was null.
if (createFn != null && value == null) {
// Try creating the value
value = await createFn();
await setWithLocking(
subcacheName,
key,
value,
ttl: ttl,
);
}
return value;
}
Future<Uint8List?> _readValue(
String subcacheName,
String key,
) async {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
Uint8List? value;
const RetryOptions r = RetryOptions(
maxAttempts: maxCacheGetAttempts,
delayFactor: Duration(milliseconds: 50),
);
try {
await r.retry(
() async {
value = await subcache[key].get();
},
);
} catch (e) {
// If the last retry is unsuccessful on an exception we do not want to die
// here.
log.warning('Unable to retrieve value for $key from cache.');
value = null;
}
return value;
}
/// Set [value] for [key] in the subcache [subcacheName] with [ttl].
Future<Uint8List?> set(
String subcacheName,
String key,
Uint8List? value, {
Duration ttl = const Duration(minutes: 1),
}) async {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
final Entry<Uint8List> entry = subcache[key];
return entry.set(value, ttl);
}
/// Set [value] for [key] in the subcache [subcacheName] with [ttl] but
/// enforce locking accessing.
///
/// Note: these methods are intended to prevent issues around race conditions
/// when storing and retrieving github tokens. Care should be taken to use the
/// locking methods together when accessing data from an entity using the
/// cache.
Future<Uint8List?> setWithLocking(
String subcacheName,
String key,
Uint8List? value, {
Duration ttl = const Duration(minutes: 1),
}) async {
await m.acquire();
try {
return set(
subcacheName,
key,
value,
ttl: ttl,
);
} finally {
m.release();
}
}
/// Clear the value stored in subcache [subcacheName] for key [key].
///
/// Note: these methods are intended to prevent issues around race conditions
/// when storing and retrieving github tokens. Care should be taken to use the
/// locking methods together when accessing data from an entity using the
/// cache.
Future<void> purge(String subcacheName, String key) async {
await m.acquire();
try {
final Cache<Uint8List> subcache = cache.withPrefix(subcacheName);
return subcache[key].purge(retries: maxCacheGetAttempts);
} finally {
m.release();
}
}
void dispose() {
_provider.close();
}
}
class _CacheCodec extends Codec<Uint8List, List<int>> {
const _CacheCodec();
@override
Converter<Uint8List, List<int>> get encoder => const _ListIntConverter();
@override
Converter<List<int>, Uint8List> get decoder => const _Uint8ListConverter();
}
class _ListIntConverter extends Converter<Uint8List, List<int>> {
const _ListIntConverter();
@override
List<int> convert(Uint8List input) => input;
}
class _Uint8ListConverter extends Converter<List<int>, Uint8List> {
const _Uint8ListConverter();
@override
Uint8List convert(List<int> input) => Uint8List.fromList(input);
}