fixed stalled requests after app resume
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:http/http.dart' as http;
|
|||||||
|
|
||||||
import '../../../model/endpoint_data.dart';
|
import '../../../model/endpoint_data.dart';
|
||||||
import '../../errors/server_exception.dart';
|
import '../../errors/server_exception.dart';
|
||||||
|
import '../../http_errors.dart';
|
||||||
import '../nextcloud_ocs.dart';
|
import '../nextcloud_ocs.dart';
|
||||||
import 'autocomplete_response.dart';
|
import 'autocomplete_response.dart';
|
||||||
|
|
||||||
@@ -30,7 +31,12 @@ class AutocompleteApi {
|
|||||||
'limit': '10',
|
'limit': '10',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
final response = await http.get(uri, headers: NextcloudOcs.headers());
|
final response = (await sendGuarded(
|
||||||
|
'Autocomplete $uri',
|
||||||
|
() => http
|
||||||
|
.get(uri, headers: NextcloudOcs.headers())
|
||||||
|
.timeout(const Duration(seconds: 20)),
|
||||||
|
))!;
|
||||||
if (response.statusCode != HttpStatus.ok) {
|
if (response.statusCode != HttpStatus.ok) {
|
||||||
throw ServerException(
|
throw ServerException(
|
||||||
statusCode: response.statusCode,
|
statusCode: response.statusCode,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import '../nextcloud_ocs.dart';
|
|||||||
import 'talk_error.dart';
|
import 'talk_error.dart';
|
||||||
|
|
||||||
abstract class TalkApi<T extends ApiResponse?> {
|
abstract class TalkApi<T extends ApiResponse?> {
|
||||||
|
// package:http has no timeout of its own; a request stuck on a dead
|
||||||
|
// connection (e.g. one started right before a suspend) would spin forever.
|
||||||
|
static const Duration _timeout = Duration(seconds: 30);
|
||||||
|
|
||||||
String path;
|
String path;
|
||||||
ApiParams? body;
|
ApiParams? body;
|
||||||
Map<String, String>? headers;
|
Map<String, String>? headers;
|
||||||
@@ -35,7 +39,7 @@ abstract class TalkApi<T extends ApiResponse?> {
|
|||||||
|
|
||||||
final data = await sendGuarded(
|
final data = await sendGuarded(
|
||||||
'Talk $endpoint',
|
'Talk $endpoint',
|
||||||
() => request(endpoint, body, mergedHeaders),
|
() => request(endpoint, body, mergedHeaders)?.timeout(_timeout),
|
||||||
);
|
);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
throw const NetworkException(
|
throw const NetworkException(
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ class MarianumConnectApi {
|
|||||||
|
|
||||||
static Dio dio() => _instance;
|
static Dio dio() => _instance;
|
||||||
|
|
||||||
|
/// Drops the pooled keep-alive sockets. After a suspend the OS or a NAT
|
||||||
|
/// has usually killed them already, but dart:io only notices once a request
|
||||||
|
/// is written into one — which then hangs until [_receiveTimeout].
|
||||||
|
/// In-flight requests finish on the old adapter.
|
||||||
|
static void resetConnections() {
|
||||||
|
final stale = _instance.httpClientAdapter;
|
||||||
|
_instance.httpClientAdapter = HttpClientAdapter();
|
||||||
|
stale.close();
|
||||||
|
}
|
||||||
|
|
||||||
/// A fresh dio with the standard JSON options but no interceptors — used by
|
/// A fresh dio with the standard JSON options but no interceptors — used by
|
||||||
/// the auth queries (login/verify) that must bypass the bearer/demo
|
/// the auth queries (login/verify) that must bypass the bearer/demo
|
||||||
/// interceptors to avoid a re-auth loop.
|
/// interceptors to avoid a re-auth loop.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
||||||
|
|
||||||
|
import 'api/marianumconnect/marianumconnect_api.dart';
|
||||||
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
||||||
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
|
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
|
||||||
import 'main.dart';
|
import 'main.dart';
|
||||||
@@ -98,6 +99,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
|||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
log('AppLifecycle: $state');
|
log('AppLifecycle: $state');
|
||||||
if (state == AppLifecycleState.resumed) {
|
if (state == AppLifecycleState.resumed) {
|
||||||
|
MarianumConnectApi.resetConnections();
|
||||||
_reportTelemetry();
|
_reportTelemetry();
|
||||||
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class ChatBloc
|
|||||||
final ChatListBloc? _chatListBloc;
|
final ChatListBloc? _chatListBloc;
|
||||||
|
|
||||||
String? _pollingToken;
|
String? _pollingToken;
|
||||||
|
// A loop parked in its request survives stop+start for the same token
|
||||||
|
// (pause/resume); the generation retires it so only one loop polls.
|
||||||
|
int _pollGeneration = 0;
|
||||||
int _backoffMs = 0;
|
int _backoffMs = 0;
|
||||||
int _lastKnownMessageId = 0;
|
int _lastKnownMessageId = 0;
|
||||||
bool _appResumed = true;
|
bool _appResumed = true;
|
||||||
@@ -265,23 +268,25 @@ class ChatBloc
|
|||||||
_pollingToken = token;
|
_pollingToken = token;
|
||||||
_backoffMs = 0;
|
_backoffMs = 0;
|
||||||
_lastKnownMessageId = _maxMessageId(innerState?.chatResponse);
|
_lastKnownMessageId = _maxMessageId(innerState?.chatResponse);
|
||||||
unawaited(_pollLoop(token));
|
unawaited(_pollLoop(token, ++_pollGeneration));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _stopLongPoll() {
|
void _stopLongPoll() {
|
||||||
_pollingToken = null;
|
_pollingToken = null;
|
||||||
|
_pollGeneration++;
|
||||||
_backoffMs = 0;
|
_backoffMs = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pollLoop(String token) async {
|
Future<void> _pollLoop(String token, int generation) async {
|
||||||
while (_pollingToken == token && !isClosed) {
|
bool active() => generation == _pollGeneration && !isClosed;
|
||||||
|
while (active()) {
|
||||||
try {
|
try {
|
||||||
final response = await LongPollChat(
|
final response = await LongPollChat(
|
||||||
chatToken: token,
|
chatToken: token,
|
||||||
lastKnownMessageId: _lastKnownMessageId,
|
lastKnownMessageId: _lastKnownMessageId,
|
||||||
).run();
|
).run();
|
||||||
|
|
||||||
if (_pollingToken != token || isClosed) return;
|
if (!active()) return;
|
||||||
_backoffMs = 0;
|
_backoffMs = 0;
|
||||||
|
|
||||||
if (response == null) continue;
|
if (response == null) continue;
|
||||||
@@ -305,7 +310,7 @@ class ChatBloc
|
|||||||
_chatListBloc?.markRoomAsRead(token, _lastKnownMessageId);
|
_chatListBloc?.markRoomAsRead(token, _lastKnownMessageId);
|
||||||
}
|
}
|
||||||
} on Object catch (e) {
|
} on Object catch (e) {
|
||||||
if (_pollingToken != token || isClosed) return;
|
if (!active()) return;
|
||||||
log('LongPoll error for $token: $e');
|
log('LongPoll error for $token: $e');
|
||||||
_backoffMs = _backoffMs == 0 ? 2000 : math.min(_backoffMs * 2, 30000);
|
_backoffMs = _backoffMs == 0 ? 2000 : math.min(_backoffMs * 2, 30000);
|
||||||
await Future.delayed(Duration(milliseconds: _backoffMs));
|
await Future.delayed(Duration(milliseconds: _backoffMs));
|
||||||
|
|||||||
Reference in New Issue
Block a user