@VisibleForTesting
  static void reloadSslFactoryWhenFileStoreChanges(SSLFactory baseSslFactory,
      String keyStoreType, String keyStorePath, String keyStorePassword,
      String trustStoreType, String trustStorePath, String trustStorePassword,
      String sslContextProtocol, SecureRandom secureRandom, Supplier<Boolean> insecureModeSupplier)
      throws IOException, URISyntaxException, InterruptedException {
    LOGGER.info("Enable auto renewal of SSLFactory {} when key store {} or trust store {} changes",
        baseSslFactory, keyStorePath, trustStorePath);
    WatchService watchService = FileSystems.getDefault().newWatchService();
    Map<WatchKey, Set<Path>> watchKeyPathMap = new HashMap<>();
    registerFile(watchService, watchKeyPathMap, keyStorePath);
    registerFile(watchService, watchKeyPathMap, trustStorePath);
    int maxSslFactoryReloadingAttempts = 3;
    int sslFactoryReloadingRetryDelayMs = 1000;
    WatchKey key;
    while ((key = watchService.take()) != null) {
      for (WatchEvent<?> event : key.pollEvents()) {
        Path changedFile = (Path) event.context();
        if (watchKeyPathMap.get(key).contains(changedFile)) {
          LOGGER.info("Detected change in file: {}, try to renew SSLFactory {} "
                  + "(built from key store {} and truststore {})",
              changedFile, baseSslFactory, keyStorePath, trustStorePath);
          try {
            // Need to retry a few times because when one file (key store or trust store) is updated, the other file
            // (trust store or key store) may not have been fully written yet, so we need to wait a bit and retry.
            RetryPolicies.fixedDelayRetryPolicy(maxSslFactoryReloadingAttempts, sslFactoryReloadingRetryDelayMs)
                .attempt(() -> {
                  try {
                    SSLFactory updatedSslFactory =
                        createSSLFactory(keyStoreType, keyStorePath, keyStorePassword, trustStoreType, trustStorePath,
                            trustStorePassword, sslContextProtocol, secureRandom, false, insecureModeSupplier.get());
                    SSLFactoryUtils.reload(baseSslFactory, updatedSslFactory);
                    LOGGER.info("Successfully renewed SSLFactory {} (built from key store {} and truststore {}) on file"
                        + " {} changes", baseSslFactory, keyStorePath, trustStorePath, changedFile);
                    return true;
                  } catch (Exception e) {
                    LOGGER.info(
                        "Encountered issues when renewing SSLFactory {} (built from key store {} and truststore {}) on "
                            + "file {} changes", baseSslFactory, keyStorePath, trustStorePath, changedFile, e);
                    return false;
                  }
                });
          } catch (Exception e) {
            LOGGER.error(
                "Failed to renew SSLFactory {} (built from key store {} and truststore {}) on file {} changes after {} "
                    + "retries", baseSslFactory, keyStorePath, trustStorePath, changedFile,
                maxSslFactoryReloadingAttempts, e);
          }
        }
      }
      key.reset();
    }
  }
@Test
  public void reloadSslFactoryWhenFileStoreChanges()
      throws IOException, URISyntaxException, InterruptedException {
    SecureRandom secureRandom = new SecureRandom();
    SSLFactory sslFactory = RenewableTlsUtils.createSSLFactory(KEYSTORE_TYPE, TLS_KEYSTORE_FILE_PATH, PASSWORD,
        TRUSTSTORE_TYPE, TLS_TRUSTSTORE_FILE_PATH, PASSWORD, "TLS", secureRandom, true, false);
    X509ExtendedKeyManager x509ExtendedKeyManager = sslFactory.getKeyManager().get();
    X509ExtendedTrustManager x509ExtendedTrustManager = sslFactory.getTrustManager().get();
    SSLContext sslContext = sslFactory.getSslContext();

    PrivateKey privateKey = x509ExtendedKeyManager.getPrivateKey(KEY_NAME_ALIAS);
    Certificate certForPrivateKey = x509ExtendedKeyManager.getCertificateChain(KEY_NAME_ALIAS)[0];
    X509Certificate acceptedIssuerForCert = x509ExtendedTrustManager.getAcceptedIssuers()[0];

    // Start a new thread to reload the ssl factory when the tls files change
    // Avoid early finalization by not using Executors.newSingleThreadExecutor (java <= 20, JDK-8145304)
    ExecutorService executorService = Executors.newFixedThreadPool(1);
    executorService.execute(
        () -> {
          try {
            RenewableTlsUtils.reloadSslFactoryWhenFileStoreChanges(sslFactory, KEYSTORE_TYPE, TLS_KEYSTORE_FILE_PATH,
                PASSWORD, TRUSTSTORE_TYPE, TLS_TRUSTSTORE_FILE_PATH, PASSWORD, "TLS", secureRandom, () -> false);
          } catch (Exception e) {
            throw new RuntimeException(e);
          }
        });
    updateTlsFilesAndWaitForSslFactoryToBeRenewed();
    executorService.shutdown();

    // after tls file update, the returned values should be the same, since the wrapper is the same
    X509ExtendedKeyManager udpatedX509ExtendedKeyManager = sslFactory.getKeyManager().get();
    X509ExtendedTrustManager updatedX509ExtendedTrustManager = sslFactory.getTrustManager().get();
    SSLContext updatedSslContext = sslFactory.getSslContext();
    assertEquals(x509ExtendedKeyManager, udpatedX509ExtendedKeyManager);
    assertEquals(x509ExtendedTrustManager, updatedX509ExtendedTrustManager);
    assertEquals(sslContext, updatedSslContext);

    // after tls file update, the underlying values should be different
    assertNotEquals(privateKey, udpatedX509ExtendedKeyManager.getPrivateKey(KEY_NAME_ALIAS));
    assertNotEquals(certForPrivateKey, udpatedX509ExtendedKeyManager.getCertificateChain(KEY_NAME_ALIAS)[0]);
    assertNotEquals(acceptedIssuerForCert, updatedX509ExtendedTrustManager.getAcceptedIssuers()[0]);
  }