Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/core/extensions/rpc/json_rpc_stdio_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class JsonRpcStdioClient {
StreamSubscription<String>? _subscription;
Object? _fatalError;

/// Number of RPC requests currently in-flight waiting for a response.
int get pendingRequestCount => _pending.length;

/// Returns true when there is at least one active RPC request in-flight.
bool get hasPendingRequests => _pending.isNotEmpty;

/// Serializes async line handling so large-line isolate decode stays ordered.
Future<void> _lineChain = Future<void>.value();

Expand Down
1 change: 1 addition & 0 deletions lib/core/extensions/rpc/plugin_rpc_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class PluginRpcBridge {
if (enableWatchdog) {
_watchdog = SandboxWatchdog(
recovery: _recovery,
isBusy: () => client.hasPendingRequests,
onStopped: (reason) {
if (reason == SandboxWatchdogStopReason.deadlock) {
unawaited(_audit?.record(
Expand Down
8 changes: 6 additions & 2 deletions lib/core/extensions/sandbox/sandbox_watchdog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ enum SandboxWatchdogStopReason {
class SandboxWatchdog {
SandboxWatchdog({
this.pingInterval = const Duration(seconds: 30),
this.pongTimeout = const Duration(seconds: 5),
this.pongTimeout = const Duration(seconds: 15),
this.recovery,
bool Function()? isBusy,
Future<Object?> Function()? ping,
void Function(SandboxWatchdogStopReason reason)? onStopped,
}) : _pingOverride = ping,
}) : _isBusy = isBusy,
_pingOverride = ping,
_onStopped = onStopped;

final Duration pingInterval;
final Duration pongTimeout;
final SandboxAutoRecovery? recovery;
final bool Function()? _isBusy;
final Future<Object?> Function()? _pingOverride;
final void Function(SandboxWatchdogStopReason reason)? _onStopped;

Expand Down Expand Up @@ -107,6 +110,7 @@ class SandboxWatchdog {

Future<void> _tick() async {
if (!_running || _pingInFlight) return;
if (_isBusy?.call() ?? false) return;
_pingInFlight = true;
try {
final result = await _sendPing().timeout(pongTimeout);
Expand Down
28 changes: 28 additions & 0 deletions test/core/extensions/sandbox/sandbox_watchdog_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,34 @@ void main() {
await client.close();
await handle.dispose();
});

test('skips ping tick when isBusy returns true', () async {
final process = _FakeProcess();
final handle = await _handle(process, tempBase);
var pings = 0;
var busy = true;

final watchdog = SandboxWatchdog(
pingInterval: const Duration(milliseconds: 15),
pongTimeout: const Duration(seconds: 1),
isBusy: () => busy,
ping: () async {
pings++;
return 'pong';
},
);

watchdog.start(handle);
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(pings, 0, reason: 'Pings should be skipped while busy');

busy = false;
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(pings, greaterThan(0), reason: 'Pings should resume when idle');

watchdog.stop();
await handle.dispose();
});
});

group('SandboxWatchdog.isPong', () {
Expand Down
Loading