Skip to content

Commit

Permalink
Reconnection
Browse files Browse the repository at this point in the history
  • Loading branch information
PlugFox committed May 14, 2024
1 parent 1d0494d commit 2c2114a
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 8 deletions.
83 changes: 75 additions & 8 deletions lib/src/spinify_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'transport_ws_pb_stub.dart'
if (dart.library.html) 'transport_ws_pb_js.dart'
// ignore: uri_does_not_exist
if (dart.library.io) 'transport_ws_pb_vm.dart';
import 'util/backoff.dart';

/// Base class for Spinify client.
abstract base class SpinifyBase implements ISpinify {
Expand Down Expand Up @@ -130,11 +131,6 @@ base mixin SpinifyStateMixin on SpinifyBase {
);
}

@override
Future<void> _onConnected() async {
await super._onConnected();
}

@override
Future<void> _onDisconnected() async {
await super._onDisconnected();
Expand Down Expand Up @@ -300,6 +296,8 @@ base mixin SpinifyConnectionMixin
/// Used for reconnecting after connection lost.
/// If null, then client is not connected or interractively disconnected.
String? _reconnectUrl;
Timer? _reconnectTimer;
int? _reconnectAttempt;
Completer<void>? _readyCompleter;

@protected
Expand Down Expand Up @@ -385,6 +383,7 @@ base mixin SpinifyConnectionMixin
'stackTrace': stackTrace,
},
);
_setUpReconnectTimer();
rethrow;
}
}
Expand Down Expand Up @@ -481,6 +480,74 @@ base mixin SpinifyConnectionMixin
}
}

@override
Future<void> _onConnected() async {
await super._onConnected();
_tearDownReconnectTimer();
}

void _setUpReconnectTimer() {
_reconnectTimer?.cancel();
final lastUrl = _reconnectUrl;
if (lastUrl == null) return;
final attempt = _reconnectAttempt ?? 0;
final delay = Backoff.nextDelay(
attempt,
config.connectionRetryInterval.min.inMilliseconds,
config.connectionRetryInterval.max.inMilliseconds,
);
_reconnectAttempt = attempt + 1;
if (delay <= Duration.zero) {
if (!state.isDisconnected) return;
config.logger?.call(
const SpinifyLogLevel.config(),
'reconnect_attempt',
'Reconnecting to $lastUrl immediately.',
{
'url': lastUrl,
'delay': delay,
},
);
Future<void>.sync(() => connect(lastUrl)).ignore();
return;
}
config.logger?.call(
const SpinifyLogLevel.debug(),
'reconnect_delayed',
'Setting up reconnect timer to $lastUrl '
'after ${delay.inMilliseconds} ms.',
{
'url': lastUrl,
'delay': delay,
},
);
/* _nextReconnectionAttempt = DateTime.now().add(delay); */
_reconnectTimer = Timer(
delay,
() {
//_nextReconnectionAttempt = null;
if (!state.isDisconnected) return;
config.logger?.call(
const SpinifyLogLevel.config(),
'reconnect_attempt',
'Reconnecting to $lastUrl after ${delay.inMilliseconds} ms.',
{
'url': lastUrl,
'delay': delay,
},
);
Future<void>.sync(() => connect(lastUrl)).ignore();
},
);
//connect(_reconnectUrl!);
}

void _tearDownReconnectTimer() {
_reconnectAttempt = null;
_reconnectTimer?.cancel();
_reconnectTimer = null;
}

@override
Future<void> ready() async {
if (state.isConnected) return;
Expand All @@ -490,8 +557,7 @@ base mixin SpinifyConnectionMixin
@override
Future<void> disconnect() async {
_reconnectUrl = null;
// TODO(plugfox): tear down reconnect timer
// tearDownReconnectTimer();
_tearDownReconnectTimer();
if (state.isDisconnected) return Future.value();
await _transport?.disconnect(1000, 'Client disconnecting');
await _onDisconnected();
Expand All @@ -501,7 +567,8 @@ base mixin SpinifyConnectionMixin
Future<void> _onDisconnected() async {
_refreshTimer?.cancel();
_transport = null;
// TODO(plugfox): setup reconnect if reconnectUrl is not null
// Reconnect if that callback called not from disconnect method.
if (_reconnectUrl != null) _setUpReconnectTimer();
await super._onDisconnected();
}

Expand Down
21 changes: 21 additions & 0 deletions lib/src/util/backoff.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// ignore_for_file: avoid_classes_with_only_static_members

import 'dart:math' as math;

import 'package:meta/meta.dart';

/// Backoff strategy for reconnection.
@internal
abstract final class Backoff {
/// Randomizer for full jitter technique.
static final math.Random _rnd = math.Random();

/// Full jitter technique.
/// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
static Duration nextDelay(int step, int minDelay, int maxDelay) {
if (minDelay >= maxDelay) return Duration(milliseconds: maxDelay);
final val = math.min(maxDelay, minDelay * math.pow(2, step.clamp(0, 31)));
final interval = _rnd.nextInt(val.toInt());
return Duration(milliseconds: math.min(maxDelay, minDelay + interval));
}
}

0 comments on commit 2c2114a

Please sign in to comment.