blob: 2917f54f8e05aed0490a18c0f751e8d72f0f236b [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:async';
import 'package:logging/logging.dart';
import '../logging/human_readable_duration.dart';
var _logger = Logger('Heartbeat');
/// Base class for a heartbeat implementation.
///
/// Once [start]ed, if [waitDuration] passes between calls to [ping], then
/// [onTimeout] will be invoked with the duration.
abstract class Heartbeat {
Stopwatch _intervalWatch;
Timer _timer;
/// The interval at which to check if [waitDuration] has passed.
final Duration checkInterval;
/// The amount of time between heartbeats.
final Duration waitDuration;
Heartbeat({Duration checkInterval, Duration waitDuration})
: checkInterval = checkInterval ?? const Duration(milliseconds: 100),
waitDuration = waitDuration ?? const Duration(seconds: 5);
/// Invoked if [waitDuration] time has elapsed since the last call to [ping].
void onTimeout(Duration elapsed);
/// Resets the internal timers. If more than [waitDuration] elapses without
/// this method being called, then [onTimeout] will be invoked with the
/// duration since the last ping.
void ping() {
_intervalWatch.reset();
}
/// Starts this heartbeat logger, must not already be started.
///
/// This method can be overridden to add additional logic for handling calls
/// to [ping], but you must call `super.start()`.
void start() {
if (_intervalWatch != null || _timer != null) {
throw StateError('HeartbeatLogger already started');
}
_intervalWatch = Stopwatch()..start();
ping();
_timer = Timer.periodic(checkInterval, _checkDuration);
}
/// Stops this heartbeat logger, must already be started.
///
/// This method can be overridden to add additional logic for cleanup
/// purposes, but you must call `super.stop()`.
void stop() {
if (_intervalWatch == null || _timer == null) {
throw StateError('HeartbeatLogger was never started');
}
_intervalWatch.stop();
_intervalWatch = null;
_timer.cancel();
_timer = null;
}
void _checkDuration(void _) {
if (_intervalWatch.elapsed < waitDuration) return;
onTimeout(_intervalWatch.elapsed);
ping();
}
}
/// Watches [Logger.root] and if there are no logs for [waitDuration] then it
/// will log a heartbeat message with the current elapsed time since [start] was
/// originally invoked.
class HeartbeatLogger extends Heartbeat {
StreamSubscription<LogRecord> _listener;
Stopwatch _totalWatch;
/// Will be invoked with each original log message and the returned value will
/// be logged instead.
final String Function(String original) transformLog;
HeartbeatLogger(
{Duration checkInterval, Duration waitDuration, this.transformLog})
: super(checkInterval: checkInterval, waitDuration: waitDuration);
/// Start listening to logs.
@override
void start() {
_totalWatch = Stopwatch()..start();
super.start();
_listener = Logger.root.onRecord.listen((_) => ping());
}
/// Stops listenting to the logger;
@override
void stop() {
super.stop();
_listener.cancel();
_listener = null;
_totalWatch.stop();
_totalWatch = null;
}
/// Logs a heartbeat message if we reach the timeout.
@override
void onTimeout(void _) {
var formattedTime = humanReadable(_totalWatch.elapsed);
var message = '$formattedTime elapsed';
if (transformLog != null) {
message = transformLog(message);
}
_logger.info(message);
}
}
class HungActionsHeartbeat extends Heartbeat {
/// Returns a description of pending actions
final String Function() listActions;
HungActionsHeartbeat(this.listActions,
{Duration checkInterval, Duration waitDuration})
: super(
checkInterval: checkInterval,
waitDuration: waitDuration ?? Duration(seconds: 15));
@override
void onTimeout(Duration elapsed) {
var formattedTime = humanReadable(elapsed);
var message = 'No actions completed for $formattedTime, '
'waiting on:\n${listActions()}';
_logger.warning(message);
}
}