blob: a597c987a11291b788c83ef7b9b8c17752513c12 [file] [log] [blame]
// Copyright (c) 2014, 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.
library services.redis_cache_test;
import 'dart:async';
import 'dart:io';
import 'package:dart_services/src/common_server_impl.dart';
import 'package:dart_services/src/sdk_manager.dart';
import 'package:dart_services/src/server_cache.dart';
import 'package:logging/logging.dart';
import 'package:pedantic/pedantic.dart';
import 'package:synchronized/synchronized.dart';
import 'package:test/test.dart';
void main() => defineTests();
void defineTests() {
/// Integration tests for the RedisCache implementation.
///
/// We basically assume that redis and dartis work correctly -- this is
/// exercising the connection maintenance and exception handling.
group('RedisCache', () {
// Note: all caches share values between them.
RedisCache redisCache, redisCacheAlt;
Process redisProcess, redisAltProcess;
var logMessages = <String>[];
// Critical section handling -- do not run more than one test at a time
// since they talk to the same redis instances.
final singleTestOnly = Lock();
// Prevent cases where we might try to reenter addStream for either stdout
// or stderr (which will throw a BadState).
final singleStreamOnly = Lock();
Future<Process> startRedisProcessAndDrainIO(int port) async {
final newRedisProcess =
await Process.start('redis-server', ['--port', port.toString()]);
unawaited(singleStreamOnly.synchronized(() async {
await stdout.addStream(newRedisProcess.stdout);
}));
unawaited(singleStreamOnly.synchronized(() async {
await stderr.addStream(newRedisProcess.stderr);
}));
return newRedisProcess;
}
setUpAll(() async {
await SdkManager.sdk.init();
redisProcess = await startRedisProcessAndDrainIO(9501);
log.onRecord.listen((LogRecord rec) {
logMessages.add('${rec.level.name}: ${rec.time}: ${rec.message}');
print(logMessages.last);
});
redisCache = RedisCache('redis://localhost:9501', 'aversion');
redisCacheAlt = RedisCache('redis://localhost:9501', 'bversion');
await Future.wait([redisCache.connected, redisCacheAlt.connected]);
});
tearDown(() async {
if (redisAltProcess != null) {
redisAltProcess.kill();
await redisAltProcess.exitCode;
redisAltProcess = null;
}
});
tearDownAll(() async {
log.clearListeners();
await Future.wait([redisCache.shutdown(), redisCacheAlt.shutdown()]);
redisProcess.kill();
await redisProcess.exitCode;
});
test('Verify basic operation of RedisCache', () async {
await singleTestOnly.synchronized(() async {
logMessages = [];
await expectLater(await redisCache.get('unknownkey'), isNull);
await redisCache.set('unknownkey', 'value');
await expectLater(await redisCache.get('unknownkey'), equals('value'));
await redisCache.remove('unknownkey');
await expectLater(await redisCache.get('unknownkey'), isNull);
expect(logMessages, isEmpty);
});
});
test('Verify values expire', () async {
await singleTestOnly.synchronized(() async {
logMessages = [];
await redisCache.set('expiringkey', 'expiringValue',
expiration: Duration(milliseconds: 1));
await Future.delayed(Duration(milliseconds: 100));
await expectLater(await redisCache.get('expiringkey'), isNull);
expect(logMessages, isEmpty);
});
});
test(
'Verify two caches with different versions give different results for keys',
() async {
await singleTestOnly.synchronized(() async {
logMessages = [];
await redisCache.set('differentVersionKey', 'value1');
await redisCacheAlt.set('differentVersionKey', 'value2');
await expectLater(
await redisCache.get('differentVersionKey'), 'value1');
await expectLater(
await redisCacheAlt.get('differentVersionKey'), 'value2');
expect(logMessages, isEmpty);
});
});
test('Verify disconnected cache logs errors and returns nulls', () async {
await singleTestOnly.synchronized(() async {
logMessages = [];
final redisCacheBroken =
RedisCache('redis://localhost:9502', 'cversion');
try {
await redisCacheBroken.set('aKey', 'value');
await expectLater(await redisCacheBroken.get('aKey'), isNull);
await redisCacheBroken.remove('aKey');
expect(
logMessages.join('\n'),
stringContainsInOrder([
'no cache available when setting key server:cversion:dart:',
'+aKey',
'no cache available when getting key server:cversion:dart:',
'+aKey',
'no cache available when removing key server:cversion:dart:',
'+aKey',
]));
} finally {
await redisCacheBroken.shutdown();
}
});
});
test('Verify cache that starts out disconnected retries and works (slow)',
() async {
await singleTestOnly.synchronized(() async {
logMessages = [];
final redisCacheRepairable =
RedisCache('redis://localhost:9503', 'cversion');
try {
// Wait for a retry message.
while (logMessages.length < 2) {
await (Future.delayed(Duration(milliseconds: 50)));
}
expect(
logMessages.join('\n'),
stringContainsInOrder([
'reconnecting to redis://localhost:9503...\n',
'Unable to connect to redis server, reconnecting in',
]));
// Start a redis server.
redisAltProcess = await startRedisProcessAndDrainIO(9503);
// Wait for connection.
await redisCacheRepairable.connected;
expect(logMessages.join('\n'), contains('Connected to redis server'));
} finally {
await redisCacheRepairable.shutdown();
}
});
});
test(
'Verify that cache that stops responding temporarily times out and can recover',
() async {
await singleTestOnly.synchronized(() async {
logMessages = [];
await redisCache.set('beforeStop', 'truth');
redisProcess.kill(ProcessSignal.sigstop);
// Don't fail the test before sending sigcont.
final beforeStop = await redisCache.get('beforeStop');
await redisCache.disconnected;
redisProcess.kill(ProcessSignal.sigcont);
expect(beforeStop, isNull);
await redisCache.connected;
await expectLater(await redisCache.get('beforeStop'), equals('truth'));
expect(
logMessages.join('\n'),
stringContainsInOrder([
'timeout on get operation for key server:aversion:dart:',
'+beforeStop',
'(aversion): reconnecting',
'(aversion): Connected to redis server',
]));
});
}, onPlatform: {'windows': Skip('Windows does not have sigstop/sigcont')});
test(
'Verify cache that starts out connected but breaks retries until reconnection (slow)',
() async {
await singleTestOnly.synchronized(() async {
logMessages = [];
redisAltProcess = await startRedisProcessAndDrainIO(9504);
final redisCacheHealing =
RedisCache('redis://localhost:9504', 'cversion');
try {
await redisCacheHealing.connected;
await redisCacheHealing.set('missingKey', 'value');
// Kill process out from under the cache.
redisAltProcess.kill();
await redisAltProcess.exitCode;
redisAltProcess = null;
// Try to talk to the cache and get an error. Wait for the disconnect
// to be recognized.
await expectLater(await redisCacheHealing.get('missingKey'), isNull);
await redisCacheHealing.disconnected;
// Start the server and verify we connect appropriately.
redisAltProcess = await startRedisProcessAndDrainIO(9504);
await redisCacheHealing.connected;
expect(
logMessages.join('\n'),
stringContainsInOrder([
'Connected to redis server',
'connection terminated with error SocketException',
'reconnecting to redis://localhost:9504',
]));
expect(logMessages.last, contains('Connected to redis server'));
} finally {
await redisCacheHealing.shutdown();
}
});
});
});
}