From 6f4801fd6f45102fb5e18e35d071e255f404efc0 Mon Sep 17 00:00:00 2001 From: Jon Chambers Date: Tue, 25 Jul 2023 17:43:00 -0400 Subject: [PATCH] Add a manager class for checking "liveness" of client versions --- .../storage/ClientReleaseManager.java | 80 +++++++++++++++++++ .../storage/ClientReleaseManagerTest.java | 57 +++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java new file mode 100644 index 000000000..3ce0dfe39 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManager.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import com.vdurmont.semver4j.Semver; +import java.time.Clock; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import io.dropwizard.lifecycle.Managed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; +import javax.annotation.Nullable; + +public class ClientReleaseManager implements Managed { + + private final ClientReleases clientReleases; + private final ScheduledExecutorService scheduledExecutorService; + private final Duration refreshInterval; + private final Clock clock; + + @Nullable + private ScheduledFuture refreshClientReleasesFuture; + + private volatile Map> clientReleasesByPlatform = Collections.emptyMap(); + + private static final Logger logger = LoggerFactory.getLogger(ClientReleaseManager.class); + + public ClientReleaseManager(final ClientReleases clientReleases, + final ScheduledExecutorService scheduledExecutorService, + final Duration refreshInterval, + final Clock clock) { + + this.clientReleases = clientReleases; + this.scheduledExecutorService = scheduledExecutorService; + this.refreshInterval = refreshInterval; + this.clock = clock; + } + + public boolean isVersionActive(final ClientPlatform platform, final Semver version) { + final Map releasesByVersion = clientReleasesByPlatform.get(platform); + + return releasesByVersion != null && + releasesByVersion.containsKey(version) && + releasesByVersion.get(version).expiration().isAfter(clock.instant()); + } + + @Override + public void start() throws Exception { + refreshClientVersions(); + + refreshClientReleasesFuture = + scheduledExecutorService.scheduleWithFixedDelay(this::refreshClientVersions, + refreshInterval.toMillis(), + refreshInterval.toMillis(), + TimeUnit.MILLISECONDS); + } + + @Override + public void stop() throws Exception { + if (refreshClientReleasesFuture != null) { + refreshClientReleasesFuture.cancel(true); + } + } + + void refreshClientVersions() { + try { + clientReleasesByPlatform = clientReleases.getClientReleases(); + } catch (final Exception e) { + logger.warn("Failed to refresh client releases", e); + } + } +} diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java new file mode 100644 index 000000000..8cb17ebff --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/storage/ClientReleaseManagerTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.storage; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.vdurmont.semver4j.Semver; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.whispersystems.textsecuregcm.util.ua.ClientPlatform; + +class ClientReleaseManagerTest { + + private ClientReleases clientReleases; + private Clock clock; + + private ClientReleaseManager clientReleaseManager; + + @BeforeEach + void setUp() { + clientReleases = mock(ClientReleases.class); + clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()); + + clientReleaseManager = + new ClientReleaseManager(clientReleases, mock(ScheduledExecutorService.class), Duration.ofHours(4), clock); + } + + @Test + void isVersionActive() { + final Semver iosVersion = new Semver("1.2.3"); + final Semver desktopVersion = new Semver("4.5.6"); + + when(clientReleases.getClientReleases()).thenReturn(Map.of( + ClientPlatform.DESKTOP, Map.of(desktopVersion, new ClientRelease(ClientPlatform.DESKTOP, desktopVersion, clock.instant(), clock.instant().plus(Duration.ofDays(90)))), + ClientPlatform.IOS, Map.of(iosVersion, new ClientRelease(ClientPlatform.IOS, iosVersion, clock.instant().minus(Duration.ofDays(91)), clock.instant().minus(Duration.ofDays(1)))) + )); + + clientReleaseManager.refreshClientVersions(); + + assertTrue(clientReleaseManager.isVersionActive(ClientPlatform.DESKTOP, desktopVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.DESKTOP, iosVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.IOS, iosVersion)); + assertFalse(clientReleaseManager.isVersionActive(ClientPlatform.ANDROID, new Semver("7.8.9"))); + } +}