diff --git a/build.gradle b/build.gradle index 47fce33aab..0b6d16cb2d 100644 --- a/build.gradle +++ b/build.gradle @@ -294,7 +294,7 @@ dependencies { implementation("io.projectreactor.addons:reactor-extra") implementation("io.projectreactor:reactor-tools") - def commonsVersion = "deb598df95" + def commonsVersion = "0ff63ee928" implementation("com.github.FAForever.faf-java-commons:faf-commons-data:${commonsVersion}") { exclude module: 'guava' diff --git a/src/main/java/com/faforever/client/config/ClientProperties.java b/src/main/java/com/faforever/client/config/ClientProperties.java index e66af420eb..28531accbc 100644 --- a/src/main/java/com/faforever/client/config/ClientProperties.java +++ b/src/main/java/com/faforever/client/config/ClientProperties.java @@ -82,8 +82,8 @@ public static class Irc { @Data public static class Server { private String url; - private int retryDelaySeconds = 30; - private int retryAttempts = 10; + private int retryDelaySeconds = 5; + private int retryAttempts = 60; } @Data diff --git a/src/main/java/com/faforever/client/remote/FafServerAccessor.java b/src/main/java/com/faforever/client/remote/FafServerAccessor.java index 4a939f1de4..75e9c50cc7 100644 --- a/src/main/java/com/faforever/client/remote/FafServerAccessor.java +++ b/src/main/java/com/faforever/client/remote/FafServerAccessor.java @@ -2,6 +2,7 @@ import com.faforever.client.api.TokenRetriever; import com.faforever.client.config.ClientProperties; +import com.faforever.client.config.ClientProperties.Server; import com.faforever.client.domain.MatchmakerQueueBean; import com.faforever.client.domain.PlayerBean; import com.faforever.client.exception.UIDException; @@ -22,6 +23,7 @@ import com.faforever.commons.lobby.GameLaunchResponse; import com.faforever.commons.lobby.GameVisibility; import com.faforever.commons.lobby.GpgGameOutboundMessage; +import com.faforever.commons.lobby.LoginException; import com.faforever.commons.lobby.MatchmakerState; import com.faforever.commons.lobby.MessageTarget; import com.faforever.commons.lobby.NoticeInfo; @@ -44,6 +46,8 @@ import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.function.TupleUtils; +import reactor.util.retry.Retry; import java.io.IOException; import java.net.URL; @@ -62,7 +66,8 @@ @RequiredArgsConstructor public class FafServerAccessor implements InitializingBean, DisposableBean { - private final ReadOnlyObjectWrapper connectionState = new ReadOnlyObjectWrapper<>(ConnectionState.DISCONNECTED); + private final ReadOnlyObjectWrapper connectionState = new ReadOnlyObjectWrapper<>( + ConnectionState.DISCONNECTED); private final NotificationService notificationService; private final I18n i18n; @@ -75,6 +80,8 @@ public class FafServerAccessor implements InitializingBean, DisposableBean { @Qualifier("userWebClient") private final ObjectFactory userWebClientFactory; + private boolean autoReconnect = false; + @Override public void afterPropertiesSet() throws Exception { eventBus.register(this); @@ -89,6 +96,12 @@ public void afterPropertiesSet() throws Exception { case CONNECTING -> ConnectionState.CONNECTING; case CONNECTED -> ConnectionState.CONNECTED; }).subscribe(connectionState::set, throwable -> log.error("Error processing connection status", throwable)); + + connectionState.subscribe((oldValue, newValue) -> { + if (autoReconnect && oldValue == ConnectionState.CONNECTED && newValue == ConnectionState.DISCONNECTED) { + connectAndLogIn().subscribe(); + } + }); } public Flux getEvents(Class type) { @@ -101,12 +114,12 @@ public Flux getEvents(Class type) { @Deprecated public void addEventListener(Class type, Consumer listener) { lobbyClient.getEvents() - .ofType(type) - .flatMap(message -> Mono.fromRunnable(() -> listener.accept(message)).onErrorResume(throwable -> { - log.error("Could not process listener for `{}`", message, throwable); - return Mono.empty(); - })) - .subscribe(); + .ofType(type) + .flatMap(message -> Mono.fromRunnable(() -> listener.accept(message)).onErrorResume(throwable -> { + log.error("Could not process listener for `{}`", message, throwable); + return Mono.empty(); + })) + .subscribe(); } public ConnectionState getConnectionState() { @@ -118,16 +131,34 @@ public ReadOnlyObjectProperty connectionStateProperty() { } public Mono connectAndLogIn() { + autoReconnect = true; return userWebClientFactory.getObject() - .get() - .uri("/lobby/access") - .retrieve() - .bodyToMono(LobbyAccess.class) - .map(lobbyAccess -> new Config(tokenRetriever.getRefreshedTokenValue(), Version.getCurrentVersion(), clientProperties.getUserAgent(), lobbyAccess.accessUrl(), this::tryGenerateUid, 1024 * 1024, false, clientProperties.getServer() - .getRetryAttempts(), clientProperties.getServer().getRetryDelaySeconds())) - .flatMap(lobbyClient::connectAndLogin); + .get() + .uri("/lobby/access") + .retrieve() + .bodyToMono(LobbyAccess.class) + .map(LobbyAccess::accessUrl) + .zipWith(tokenRetriever.getRefreshedTokenValue()) + .map(TupleUtils.function( + (lobbyUrl, token) -> new Config(token, Version.getCurrentVersion(), + clientProperties.getUserAgent(), lobbyUrl, + this::tryGenerateUid, 1024 * 1024, false))) + .flatMap(lobbyClient::connectAndLogin) + .timeout(Duration.ofSeconds(30)) + .retryWhen(createRetrySpec(clientProperties.getServer())); + } + + private Retry createRetrySpec(Server server) { + return Retry.fixedDelay(server.getRetryAttempts(), Duration.ofSeconds(server.getRetryDelaySeconds())) + .filter(exception -> !(exception instanceof LoginException)) + .doBeforeRetry( + retry -> log.warn("Could not reach server retrying: Attempt #{} of {}", retry.totalRetries(), + server.getRetryAttempts(), retry.failure())) + .onRetryExhaustedThrow((spec, retrySignal) -> new LoginException( + "Could not reach server after %d attempts".formatted(spec.maxAttempts), retrySignal.failure())); } + private String tryGenerateUid(Long sessionId) { try { return uidService.generate(String.valueOf(sessionId)); @@ -137,10 +168,11 @@ private String tryGenerateUid(Long sessionId) { } public CompletableFuture requestHostGame(NewGameInfo newGameInfo) { - return lobbyClient.requestHostGame(newGameInfo.getTitle(), newGameInfo.getMap(), newGameInfo.getFeaturedMod() - .getTechnicalName(), GameVisibility.valueOf(newGameInfo.getGameVisibility() - .name()), newGameInfo.getPassword(), newGameInfo.getRatingMin(), newGameInfo.getRatingMax(), newGameInfo.getEnforceRatingRange()) - .toFuture(); + return lobbyClient.requestHostGame(newGameInfo.getTitle(), newGameInfo.getMap(), + newGameInfo.getFeaturedMod().getTechnicalName(), + GameVisibility.valueOf(newGameInfo.getGameVisibility().name()), + newGameInfo.getPassword(), newGameInfo.getRatingMin(), + newGameInfo.getRatingMax(), newGameInfo.getEnforceRatingRange()).toFuture(); } public CompletableFuture requestJoinGame(int gameId, String password) { @@ -148,17 +180,18 @@ public CompletableFuture requestJoinGame(int gameId, String } public void disconnect() { + autoReconnect = false; log.info("Closing lobby server connection"); lobbyClient.disconnect(); } public Mono reconnect() { return lobbyClient.getConnectionStatus() - .filter(ConnectionStatus.DISCONNECTED::equals) - .next() - .take(Duration.ofSeconds(5)) - .then(connectAndLogIn()) - .doOnSubscribe(ignored -> disconnect()); + .filter(ConnectionStatus.DISCONNECTED::equals) + .next() + .take(Duration.ofSeconds(5)) + .then(connectAndLogIn()) + .doOnSubscribe(ignored -> disconnect()); } public void addFriend(int playerId) { @@ -175,14 +208,15 @@ public void requestMatchmakerInfo() { public CompletableFuture startSearchMatchmaker() { return lobbyClient.getEvents() - .filter(event -> event instanceof GameLaunchResponse) - .next() - .cast(GameLaunchResponse.class) - .toFuture(); + .filter(event -> event instanceof GameLaunchResponse) + .next() + .cast(GameLaunchResponse.class) + .toFuture(); } public void sendGpgMessage(GpgGameOutboundMessage message) { - lobbyClient.sendGpgGameMessage(new GpgGameOutboundMessage(message.getCommand(), message.getArgs(), MessageTarget.GAME)); + lobbyClient.sendGpgGameMessage( + new GpgGameOutboundMessage(message.getCommand(), message.getArgs(), MessageTarget.GAME)); } public void removeFriend(int playerId) { @@ -223,8 +257,12 @@ private void onNotice(NoticeInfo noticeMessage) { if (Objects.equals(noticeMessage.getStyle(), "kick")) { log.info("Kicked from lobby, client closing after delay"); - notificationService.addNotification(new ImmediateNotification(i18n.get("server.kicked.title"), i18n.get("server.kicked.message", clientProperties.getLinks() - .get("linksRules")), Severity.WARN, Collections.singletonList(new DismissAction(i18n)))); + notificationService.addNotification(new ImmediateNotification(i18n.get("server.kicked.title"), + i18n.get("server.kicked.message", + clientProperties.getLinks() + .get("linksRules")), + Severity.WARN, Collections.singletonList( + new DismissAction(i18n)))); taskScheduler.scheduleWithFixedDelay(Platform::exit, Duration.ofSeconds(10)); } @@ -243,7 +281,9 @@ private void onNotice(NoticeInfo noticeMessage) { default -> Severity.INFO; }; } - notificationService.addServerNotification(new ImmediateNotification(i18n.get("messageFromServer"), noticeMessage.getText(), severity, Collections.singletonList(new DismissAction(i18n)))); + notificationService.addServerNotification( + new ImmediateNotification(i18n.get("messageFromServer"), noticeMessage.getText(), severity, + Collections.singletonList(new DismissAction(i18n)))); } public void restoreGameSession(int id) {