blob: 577efe07c5ddec7aea6b85a04b202d7b79329729 [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_gae;
import 'dart:io' as io;
import 'dart:math';
import 'package:appengine/appengine.dart' as ae;
import 'package:args/args.dart';
import 'package:logging/logging.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'src/common_server_api.dart';
import 'src/common_server_impl.dart';
import 'src/sdk_manager.dart';
import 'src/server_cache.dart';
const String _API_PREFIX = '/api/dartservices/';
const String _livenessCheck = '/liveness_check';
const String _readinessCheck = '/readiness_check';
// Serve content for 1.5 hours, +- 30 minutes.
final DateTime _serveUntil = DateTime.now()
.add(Duration(hours: 1))
.add(Duration(minutes: Random().nextInt(60)));
final Logger _logger = Logger('gae_server');
void main(List<String> args) {
final parser = ArgParser()
..addOption('proxy-target',
help: 'URL base to proxy compilation requests to')
..addOption('port',
abbr: 'p', defaultsTo: '8080', help: 'Port to attach to');
final results = parser.parse(args);
final gaePort = int.tryParse(results['port'] as String ?? '') ?? 8080;
if (SdkManager.sdk.sdkPath == null) {
throw 'No Dart SDK is available; set the DART_SDK env var.';
}
// Log to stdout/stderr. AppEngine's logging package is disabled in 0.6.0
// and AppEngine copies stdout/stderr to the dashboards.
_logger.onRecord.listen((LogRecord rec) {
final out = ('${rec.level.name}: ${rec.time}: ${rec.message}\n');
if (rec.level > Level.INFO) {
io.stderr.write(out);
} else {
io.stdout.write(out);
}
});
log.info('''Initializing dart-services:
--port: $gaePort
--proxy-target: ${results['proxy-target']}
sdkPath: ${SdkManager.sdk?.sdkPath}
\$REDIS_SERVER_URI: ${io.Platform.environment['REDIS_SERVER_URI']}
\$GAE_VERSION: ${io.Platform.environment['GAE_VERSION']}
''');
final server = GaeServer(io.Platform.environment['REDIS_SERVER_URI'],
results['proxy-target'].toString());
server.start(gaePort);
}
class GaeServer {
final String redisServerUri;
CommonServerImpl commonServerImpl;
CommonServerApi commonServerApi;
bool proxying = false;
GaeServer(this.redisServerUri, String proxyTarget) {
hierarchicalLoggingEnabled = true;
recordStackTraceAtLevel = Level.SEVERE;
_logger.level = Level.ALL;
if (proxyTarget != null && proxyTarget.isNotEmpty) {
commonServerImpl = CommonServerImplProxy(proxyTarget);
proxying = true;
} else {
commonServerImpl = CommonServerImpl(
GaeServerContainer(),
redisServerUri == null
? InMemoryCache()
: RedisCache(
redisServerUri,
io.Platform.environment['GAE_VERSION'],
),
);
}
commonServerApi = CommonServerApi(commonServerImpl);
}
Future<dynamic> start([int gaePort = 8080]) async {
await commonServerImpl.init();
return ae.runAppEngine(requestHandler, port: gaePort);
}
Future<void> requestHandler(io.HttpRequest request) async {
request.response.headers
..add('Access-Control-Allow-Origin', '*')
..add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
..add('Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept');
if (request.method == 'OPTIONS') {
await _processOptionsRequest(request);
} else if (request.uri.path == _readinessCheck) {
await _processReadinessRequest(request);
} else if (request.uri.path == _livenessCheck) {
await _processLivenessRequest(request);
} else if (request.uri.path.startsWith(_API_PREFIX)) {
await shelf_io.handleRequest(request, commonServerApi.router);
} else {
await _processDefaultRequest(request);
}
}
Future<void> _processOptionsRequest(io.HttpRequest request) async {
final statusCode = io.HttpStatus.ok;
request.response.statusCode = statusCode;
await request.response.close();
}
Future<void> _processReadinessRequest(io.HttpRequest request) async {
_logger.info('Processing readiness check');
if (proxying) {
request.response.statusCode = io.HttpStatus.ok;
} else if (!commonServerImpl.isRestarting &&
DateTime.now().isBefore(_serveUntil)) {
request.response.statusCode = io.HttpStatus.ok;
} else {
request.response.statusCode = io.HttpStatus.serviceUnavailable;
_logger.severe('CommonServer not running - failing readiness check.');
}
await request.response.close();
}
Future<void> _processLivenessRequest(io.HttpRequest request) async {
_logger.info('Processing liveness check');
if (proxying) {
request.response.statusCode = io.HttpStatus.ok;
} else if (!commonServerImpl.isHealthy ||
DateTime.now().isAfter(_serveUntil)) {
_logger.severe('CommonServer is no longer healthy.'
' Intentionally failing health check.');
request.response.statusCode = io.HttpStatus.serviceUnavailable;
} else {
try {
final tempDir = await io.Directory.systemTemp.createTemp('healthz');
try {
final file = io.File('${tempDir.path}/livecheck.txt');
await file.writeAsString('testing123\n' * 1000, flush: true);
final stat = await file.stat();
if (stat.size > 10000) {
_logger.info('CommonServer healthy and file system working.'
' Passing health check.');
request.response.statusCode = io.HttpStatus.ok;
} else {
_logger.severe('CommonServer healthy, but filesystem is not.'
' Intentionally failing health check.');
request.response.statusCode = io.HttpStatus.serviceUnavailable;
}
} finally {
await tempDir.delete(recursive: true);
}
} catch (e) {
_logger.severe('CommonServer healthy, but failed to create temporary'
' file: $e');
request.response.statusCode = io.HttpStatus.serviceUnavailable;
}
}
await request.response.close();
}
Future<void> _processDefaultRequest(io.HttpRequest request) async {
request.response.statusCode = io.HttpStatus.notFound;
await request.response.close();
}
}
class GaeServerContainer implements ServerContainer {
@override
String get version => io.Platform.version;
}