Skip to content

Commit

Permalink
Add fallback backends with respect to load + Backend rework (#613)
Browse files Browse the repository at this point in the history
* Start rework load service

* Fix wordinf

* Remove sendStartNotification

* Add first failover implementation

* Add failover implementation when loading the app

* Remove option to use beta deployment for normal users

* Start reworking load service

* Rework failover implementation

* Fix comment

* Refactor code

* Refactor code

* Add recommendFallback to model

* Fix typo

* WIP

* Fix syntax

* Refactor load servi e

* Fix title in home view

* Fix persistence of city selection

* Remove second get backend function in settings and refactor code

* Rename node to backend

* fix prev value set city

* Remove unused load service

* Remove unused isLoading and reset in load service

* Refactor checkLoad function to check both default and fallback backend and use default if both are not usable

* Add naming convention for backends

* Fix auth service to support multiple configs

* Refactor load service

* Undo backend renaming

* Fix fallback logic

---------

Co-authored-by: PaulPickhardt <s2137275@msx.tu-dresden.de>
  • Loading branch information
adeveloper-wq and PaulPickhardt authored Jun 25, 2024
1 parent c1befc4 commit ac48e2d
Show file tree
Hide file tree
Showing 48 changed files with 641 additions and 1,280 deletions.
4 changes: 2 additions & 2 deletions lib/common/layout/dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ void showInvalidShortcutSheet(context) {
),
),
pageBuilder: (BuildContext dialogContext, Animation<double> animation, Animation<double> secondaryAnimation) {
final backend = getIt<Settings>().backend;
final city = getIt<Settings>().city;
return DialogLayout(
title: 'Ungültige Strecke',
text:
"Die ausgewählte Strecke ist ungültig, da sie Wegpunkte enthält, die außerhalb des Stadtgebietes von ${backend.region} liegen.\nPrioBike wird aktuell nur innerhalb von ${backend.region} unterstützt.",
"Die ausgewählte Strecke ist ungültig, da sie Wegpunkte enthält, die außerhalb des Stadtgebietes von ${city.nameDE} liegen.\nPrioBike wird aktuell nur innerhalb von ${city.nameDE} unterstützt.",
actions: [
BigButtonPrimary(
label: 'Schließen',
Expand Down
3 changes: 2 additions & 1 deletion lib/common/map/image_cache.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:priobike/http.dart';
import 'package:priobike/logging/logger.dart';
import 'package:priobike/logging/toast.dart';
import 'package:priobike/main.dart';
import 'package:priobike/settings/models/backend.dart';
import 'package:priobike/settings/services/auth.dart';
import 'package:priobike/settings/services/settings.dart';
import 'package:shared_preferences/shared_preferences.dart';
Expand Down Expand Up @@ -58,7 +59,7 @@ class MapboxTileImageCache {
try {
// See: https://docs.mapbox.com/api/maps/static-images/
final settings = getIt<Settings>();
final auth = await Auth.load(settings.backend);
final auth = await Auth.load(settings.city.selectedBackend(true));
final accessTokenHeader = "access_token=${auth.mapboxAccessToken}";
String styleId = "";
// remove prefix "mapbox://styles/" from the styles
Expand Down
8 changes: 4 additions & 4 deletions lib/common/map/layers/poi_layers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class ParkingStationsLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_parking_v2.geojson"),
);
Expand Down Expand Up @@ -129,7 +129,7 @@ class RentalStationsLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_rental_v2.geojson"),
);
Expand Down Expand Up @@ -308,7 +308,7 @@ class BikeShopLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bicycle_shop_v2.geojson"),
);
Expand Down Expand Up @@ -482,7 +482,7 @@ class BikeAirStationLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/bike_air_station_v2.geojson"),
);
Expand Down
6 changes: 3 additions & 3 deletions lib/common/map/layers/prio_layers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class GreenWaveLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/static_green_waves_v2.geojson"),
);
Expand Down Expand Up @@ -83,7 +83,7 @@ class VeloRoutesLayer {
/// Install the source of the layer on the map controller.
_installSource(mapbox.MapboxMap mapController) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
await mapController.style.addSource(
mapbox.GeoJsonSource(id: sourceId, data: "https://$baseUrl/map-data/velo_routes_v2.geojson", tolerance: 1),
);
Expand Down Expand Up @@ -135,7 +135,7 @@ class IntersectionsLayer {
_installSource(mapbox.MapboxMap mapController) async {
try {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;

final url = "https://$baseUrl/sg-selector-nginx/intersections.json.gz";
final endpoint = Uri.parse(url);
Expand Down
4 changes: 2 additions & 2 deletions lib/common/map/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,8 @@ class AppMapState extends State<AppMap> {
cameraOptions: mapbox.CameraOptions(
center: mapbox.Point(
coordinates: mapbox.Position(
settings.backend.center.longitude,
settings.backend.center.latitude,
settings.city.center.longitude,
settings.city.center.latitude,
),
),
zoom: 12,
Expand Down
2 changes: 1 addition & 1 deletion lib/feedback/services/feedback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class Feedback with ChangeNotifier {

// Send all of the answered questions to the backend.
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
final endpoint = Uri.parse('https://$baseUrl/tracking-service/answers/post/');
for (final entry in pending.values.toList().asMap().entries) {
final request = PostAnswerRequest(
Expand Down
24 changes: 24 additions & 0 deletions lib/home/models/backend_status.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
class BackendStatus {
/// If another backend should be used.
final bool recommendOtherBackend;

/// If the backend has a load warning.
final bool warning;

/// The timestamp of the current load status data.
final DateTime timestamp;

BackendStatus({required this.recommendOtherBackend, required this.warning, required this.timestamp});

factory BackendStatus.fromJson(Map<String, dynamic> json) => BackendStatus(
recommendOtherBackend: json['recommendOtherBackend'],
warning: json['warning'],
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] * 1000),
);

Map<String, dynamic> toJson() => {
'recommendOtherBackend': recommendOtherBackend,
'warning': warning,
'timestamp': timestamp.millisecondsSinceEpoch,
};
}
4 changes: 2 additions & 2 deletions lib/home/services/link_shortener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'package:priobike/settings/services/settings.dart';
class LinkShortener {
/// Shorten long link.
static Future<String?> createShortLink(String longLink) async {
final backend = getIt<Settings>().backend;
final backend = getIt<Settings>().city.selectedBackend(false);
String backendPath = backend.path;
final linkShortenerUrl = 'https://$backendPath/link/rest/v3/short-urls';
final linkShortenerEndpoint = Uri.parse(linkShortenerUrl);
Expand Down Expand Up @@ -41,7 +41,7 @@ class LinkShortener {
// Shortcuts from production and release should be working with each others backend.
// Therefore, try fetch from the current backend and (if failed) from the other backend.
// Only staging is not compatible with the other backends.
final backend = getIt<Settings>().backend;
final backend = getIt<Settings>().city.selectedBackend(false);
final String? result = await _fetch(backend, shortLink);
if (result != null) return result;
if (backend == Backend.staging) return null;
Expand Down
118 changes: 64 additions & 54 deletions lib/home/services/load.dart
Original file line number Diff line number Diff line change
@@ -1,92 +1,102 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:priobike/home/models/backend_status.dart';
import 'package:priobike/http.dart';
import 'package:priobike/logging/logger.dart';
import 'package:priobike/logging/toast.dart';
import 'package:priobike/main.dart';
import 'package:priobike/settings/models/backend.dart';
import 'package:priobike/settings/services/features.dart';
import 'package:priobike/settings/services/settings.dart';

class LoadStatus with ChangeNotifier {
/// If the service is currently loading the status history.
bool isLoading = false;

/// The warning that should be displayed.
String? text;

/// If there exists a warning.
bool hasWarning = false;

/// If the fallback backend should be used.
bool useFallback = false;

/// Logger for the status history.
final log = Logger("Load");

LoadStatus();

/// Fetches the status data from the priobike-load-service.
Future<void> fetch() async {
if (isLoading) return;
isLoading = true;
/// Fetches the status data and returns if the given backend is usable.
/// Otherwise also checking the fallback backend.
/// If both backends are not usable, the default is used.
Future<void> checkLoad() async {
final baseUrl = getIt<Settings>().city.defaultBackend.path;
var (hasWarning, recommendOtherBackend) = await fetchLoad(baseUrl);
useFallback = recommendOtherBackend;
this.hasWarning = hasWarning;

if (recommendOtherBackend) {
// If the default backend should not be used, we try to fetch the status of the fallback backend.
// If the fallback backend is not usable, we use the default backend anyway.
try {
final baseUrl = getIt<Settings>().city.fallbackBackend?.path;

if (baseUrl == null) {
throw Exception("No fallback backend available");
}

var (_, recommendOtherBackend) = await fetchLoad(baseUrl);
useFallback = !recommendOtherBackend;
} catch (e, stacktrace) {
final hint = "Error while checking fallback backend: $e $stacktrace";
log.e(hint);
useFallback = false;
}
}

try {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
if (getIt<Feature>().canEnableInternalFeatures) {
// Don't switch the backend if the internal version is used. We want to keep the possibility
// to manually set the backend.
if (useFallback) {
ToastMessage.showError(
"Fallback müsste benutzt werden. Aufgrund der internen Version wird das Fallback jedoch nicht benutzt.");
}
useFallback = false;
}

final url = "https://$baseUrl/load-service/static/load_response.json";
notifyListeners();
}

/// Returns if a warning should be shown and if another backend should be used.
Future<(bool, bool)> fetchLoad(String baseUrl) async {
bool hasWarning = false;
bool recommendOtherBackend = false;
try {
final url = "https://$baseUrl/load-service/load.json";
final endpoint = Uri.parse(url);

final response = await Http.get(endpoint).timeout(const Duration(seconds: 4));

if (response.statusCode != 200) {
isLoading = false;
notifyListeners();
final err = "Error while fetching load status from $endpoint: ${response.statusCode}";
throw Exception(err);
}

final json = jsonDecode(response.body);
final backendStatus = BackendStatus.fromJson(json);

if (json["warning"]) {
// Load status is updated every minute.
// If the timestamp of the status is older than 5 minutes, we assume the backend is not usable.
if (DateTime.now().difference(backendStatus.timestamp).inMinutes > 5) {
hasWarning = true;
text = json["response_text"];
recommendOtherBackend = true;
log.w("Load status is older than 5 minutes");
} else {
hasWarning = false;
text = null;
hasWarning = backendStatus.warning;
recommendOtherBackend = backendStatus.recommendOtherBackend;
}

isLoading = false;
notifyListeners();
} catch (e, stacktrace) {
isLoading = false;
notifyListeners();
final hint = "Error while fetching load status: $e $stacktrace";
final hint = "Error while fetching load status for backend: $e $stacktrace";
log.e(hint);
hasWarning = true;
recommendOtherBackend = true;
}
}

/// Sends an app start notification to the load service in the backend.
Future<void> sendAppStartNotification() async {
try {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;

final url = "https://$baseUrl/load-service/app/start";
final endpoint = Uri.parse(url);

final response = await Http.post(endpoint).timeout(const Duration(seconds: 4));
if (response.statusCode != 200) {
final err = "Error while sending app start to load service $endpoint: ${response.statusCode}";
throw Exception(err);
}
} catch (e, stacktrace) {
final hint = "Error while sending app start to load service: $e $stacktrace";
log.e(hint);
}
}

/// Reset the status.
Future<void> reset() async {
hasWarning = false;
text = null;
isLoading = false;
notifyListeners();
return (hasWarning, recommendOtherBackend);
}
}
2 changes: 1 addition & 1 deletion lib/home/services/poi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ class POI with ChangeNotifier {
/// A method which is used to fetch the POI data from the backend.
Future<dynamic> _fetchData(String relativeUrl) async {
final settings = getIt<Settings>();
final baseUrl = settings.backend.path;
final baseUrl = settings.city.selectedBackend(true).path;
final dataUrl = "https://$baseUrl$relativeUrl";
final dataEndpoint = Uri.parse(dataUrl);

Expand Down
10 changes: 5 additions & 5 deletions lib/home/services/shortcuts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,9 @@ class Shortcuts with ChangeNotifier {
if (shortcuts == null) return;
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().backend;
final city = getIt<Settings>().city;
final jsonStr = jsonEncode(shortcuts!.map((e) => e.toJson()).toList());
storage.setString("priobike.home.shortcuts.${backend.regionName}", jsonStr);
storage.setString("priobike.home.shortcuts.${city.nameDE}", jsonStr);

// Activates the tutorial if more then 3 (+2 default shortcuts) shortcuts were stored.
if (shortcuts!.length >= 5) {
Expand All @@ -137,11 +137,11 @@ class Shortcuts with ChangeNotifier {
if (shortcuts != null) return;
final storage = await SharedPreferences.getInstance();

final backend = getIt<Settings>().backend;
final jsonStr = storage.getString("priobike.home.shortcuts.${backend.regionName}");
final city = getIt<Settings>().city;
final jsonStr = storage.getString("priobike.home.shortcuts.${city.nameDE}");

if (jsonStr == null) {
shortcuts = backend.defaultShortcuts;
shortcuts = city.defaultShortcuts;
await storeShortcuts();
} else {
// Init shortcuts.
Expand Down
6 changes: 3 additions & 3 deletions lib/home/views/load_status.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ class LoadStatusViewState extends State<LoadStatusView> {
context: context,
builder: (BuildContext context) {
return DialogLayout(
title: "Mehr Nutzende als normalerweise",
text: loadStatus.text ?? "",
title: "Starke Auslastung",
text: "Aktuell sind unsere Server außergewöhnlich stark ausgelastet.",
actions: [
BigButtonTertiary(
label: "Schließen",
Expand All @@ -58,7 +58,7 @@ class LoadStatusViewState extends State<LoadStatusView> {
children: [
Flexible(
child: Content(
text: "Mehr Nutzende als normalerweise",
text: "Starke Auslastung",
context: context,
),
),
Expand Down
Loading

0 comments on commit ac48e2d

Please sign in to comment.