diff options
author | Morten Tokle <mortent@yahooinc.com> | 2023-06-20 09:01:19 +0200 |
---|---|---|
committer | Morten Tokle <mortent@yahooinc.com> | 2023-06-20 09:08:06 +0200 |
commit | 828ca13b3490b6bc7561bb40149fa979ddd9f18e (patch) | |
tree | da8f0cdc87ad9e0802a12b5645fe5097ee7a5f25 /container-disc | |
parent | ae5b5deda1253e7ea75d06d09be063eb8b4419cd (diff) |
Verify proxy running + handle stop
Diffstat (limited to 'container-disc')
-rw-r--r-- | container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java | 76 | ||||
-rw-r--r-- | container-disc/src/test/java/com/yahoo/container/jdisc/DataplaneProxyServiceTest.java | 98 |
2 files changed, 132 insertions, 42 deletions
diff --git a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java index 78c89e3a7d4..e6af65c0bc8 100644 --- a/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java +++ b/container-disc/src/main/java/com/yahoo/container/jdisc/DataplaneProxyService.java @@ -35,8 +35,9 @@ public class DataplaneProxyService extends AbstractComponent { private final ScheduledThreadPoolExecutor executorService; private final Path root; - enum NginxState {INITIALIZING, RUNNING, RELOAD_REQUIRED}; + enum NginxState {INITIALIZING, RUNNING, RELOAD_REQUIRED, STOPPED}; private NginxState state; + private NginxState wantedState; private DataplaneProxyConfig cfg; private Path proxyCredentialsCert; @@ -52,14 +53,14 @@ public class DataplaneProxyService extends AbstractComponent { this.root = root; this.proxyCommands = proxyCommands; changeState(NginxState.INITIALIZING); + wantedState = NginxState.RUNNING; configTemplate = root.resolve("conf/nginx/nginx.conf.template"); serverCertificateFile = root.resolve("conf/nginx/server_cert.pem"); serverKeyFile = root.resolve("conf/nginx/server_key.pem"); nginxConf = root.resolve("conf/nginx/nginx.conf"); executorService = new ScheduledThreadPoolExecutor(1); - executorService.scheduleAtFixedRate(this::startOrReloadNginx, reloadPeriodMinutes, reloadPeriodMinutes, TimeUnit.MINUTES); - + executorService.scheduleAtFixedRate(this::converge, reloadPeriodMinutes, reloadPeriodMinutes, TimeUnit.MINUTES); } public void reconfigure(DataplaneProxyConfig config, DataplaneProxyCredentials credentialsProvider) { @@ -70,11 +71,11 @@ public class DataplaneProxyService extends AbstractComponent { } } - private synchronized void changeState(NginxState newState) { + private void changeState(NginxState newState) { state = newState; } - void startOrReloadNginx() { + void converge() { DataplaneProxyConfig config; Path proxyCredentialsCert; Path proxyCredentialsKey; @@ -86,14 +87,13 @@ public class DataplaneProxyService extends AbstractComponent { this.proxyCredentialsCert = null; this.proxyCredentialsKey = null; } - - boolean configChanged = false; if (config != null) { try { String serverCert = config.serverCertificate(); String serverKey = config.serverKey(); + boolean configChanged = false; configChanged |= writeFile(serverCertificateFile, serverCert); configChanged |= writeFile(serverKeyFile, serverKey); configChanged |= writeFile(nginxConf, @@ -113,38 +113,47 @@ public class DataplaneProxyService extends AbstractComponent { throw new RuntimeException("Error reconfiguring data plane proxy", e); } } - - if (state == NginxState.INITIALIZING) { - try { - proxyCommands.start(nginxConf); - changeState(NginxState.RUNNING); - } catch (Exception e) { - logger.log(Level.INFO, "Failed to start nginx, will retry"); + if (wantedState == NginxState.RUNNING) { + boolean nginxRunning = proxyCommands.isRunning(); + if (!nginxRunning) { + try { + proxyCommands.start(nginxConf); + changeState(wantedState); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to start nginx, will retry"); + } + } else if (nginxRunning && state == NginxState.RELOAD_REQUIRED) { + try { + proxyCommands.reload(); + changeState(wantedState); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to reconfigure nginx, will retry."); + } } - } else if (state == NginxState.RELOAD_REQUIRED){ - try { - proxyCommands.reload(); - changeState(NginxState.RUNNING); - } catch (Exception e) { - logger.log(Level.INFO, "Failed to reconfigure nginx, will retry."); + } else if (wantedState == NginxState.STOPPED) { + if (proxyCommands.isRunning()) { + try { + proxyCommands.stop(); + changeState(wantedState); + executorService.shutdownNow(); + } catch (Exception e) { + logger.log(Level.INFO, "Failed to stop nginx, will retry"); + } } + } else { + logger.warning("Unknown state " + wantedState); } } @Override public void deconstruct() { super.deconstruct(); + wantedState = NginxState.STOPPED; try { - executorService.shutdownNow(); - executorService.awaitTermination(1, TimeUnit.MINUTES); + executorService.awaitTermination(5, TimeUnit.MINUTES); } catch (InterruptedException e) { logger.log(Level.WARNING, "Error shutting down proxy reload thread"); } - try { - proxyCommands.stop(); - } catch (Exception e) { - logger.log(Level.WARNING, "Failed shutting down nginx"); - } } /* @@ -153,7 +162,6 @@ public class DataplaneProxyService extends AbstractComponent { */ private boolean writeFile(Path file, String contents) throws IOException { Path tempPath = file.getParent().resolve(file.getFileName().toString() + ".new"); -// Path tempPath = Paths.get(file.toFile().getAbsolutePath() + ".new"); Files.createDirectories(tempPath.getParent()); Files.writeString(tempPath, contents); @@ -199,10 +207,15 @@ public class DataplaneProxyService extends AbstractComponent { return state; } + NginxState wantedState() { + return wantedState; + } + public interface ProxyCommands { void start(Path configFile); void stop(); void reload(); + boolean isRunning(); } public static class NginxProxyCommands implements ProxyCommands { @@ -255,5 +268,12 @@ public class DataplaneProxyService extends AbstractComponent { throw new RuntimeException("Could not start nginx", e); } } + + @Override + public boolean isRunning() { + return ProcessHandle.allProcesses() + .map(ProcessHandle::info) + .anyMatch(info -> info.command().orElse("").endsWith("nginx")); + } } } diff --git a/container-disc/src/test/java/com/yahoo/container/jdisc/DataplaneProxyServiceTest.java b/container-disc/src/test/java/com/yahoo/container/jdisc/DataplaneProxyServiceTest.java index b71dfbba88a..947c99adf51 100644 --- a/container-disc/src/test/java/com/yahoo/container/jdisc/DataplaneProxyServiceTest.java +++ b/container-disc/src/test/java/com/yahoo/container/jdisc/DataplaneProxyServiceTest.java @@ -19,61 +19,107 @@ import java.time.Duration; import static com.yahoo.yolean.Exceptions.uncheck; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class DataplaneProxyServiceTest { private FileSystem fileSystem = Jimfs.newFileSystem(); - DataplaneProxyService.ProxyCommands proxyCommands = Mockito.mock(DataplaneProxyService.ProxyCommands.class); + DataplaneProxyService.ProxyCommands proxyCommandsMock = Mockito.mock(DataplaneProxyService.ProxyCommands.class); @Test public void starts_and_reloads_if_no_errors() throws IOException { - DataplaneProxyService service = dataplaneProxyService(proxyCommands); + DataplaneProxyService service = dataplaneProxyService(proxyCommandsMock); assertEquals(DataplaneProxyService.NginxState.INITIALIZING, service.state()); service.reconfigure(proxyConfig(), credentials(fileSystem)); // Simulate executor next tick - service.startOrReloadNginx(); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); // Trigger reload by recreating the proxy config (generates new server cert) service.reconfigure(proxyConfig(), credentials(fileSystem)); - service.startOrReloadNginx(); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); } @Test public void retries_startup_errors() throws IOException { - Mockito.doThrow(new RuntimeException("IO error")).doNothing().when(proxyCommands).start(any()); - DataplaneProxyService service = dataplaneProxyService(proxyCommands); + Mockito.doThrow(new RuntimeException("IO error")).doNothing().when(proxyCommandsMock).start(any()); + DataplaneProxyService service = dataplaneProxyService(proxyCommandsMock); assertEquals(DataplaneProxyService.NginxState.INITIALIZING, service.state()); service.reconfigure(proxyConfig(), credentials(fileSystem)); - // Start nginx, - service.startOrReloadNginx(); + // Start nginx, starting will fail, so the service should be in INITIALIZING state + service.converge(); assertEquals(DataplaneProxyService.NginxState.INITIALIZING, service.state()); - service.startOrReloadNginx(); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); } @Test public void retries_reload_errors() throws IOException { - Mockito.doThrow(new RuntimeException("IO error")).doNothing().when(proxyCommands).reload(); - DataplaneProxyService service = dataplaneProxyService(proxyCommands); + Mockito.doThrow(new RuntimeException("IO error")).doNothing().when(proxyCommandsMock).reload(); + when(proxyCommandsMock.isRunning()).thenReturn(false); + DataplaneProxyService service = dataplaneProxyService(proxyCommandsMock); // Make sure service in running state service.reconfigure(proxyConfig(), credentials(fileSystem)); - service.startOrReloadNginx(); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); + when(proxyCommandsMock.isRunning()).thenReturn(true); // Trigger reload, verifies 2nd attempt succeeds service.reconfigure(proxyConfig(), credentials(fileSystem)); - service.startOrReloadNginx(); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RELOAD_REQUIRED, service.state()); - service.startOrReloadNginx(); + service.converge(); + assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); + verify(proxyCommandsMock, times(2)).reload(); + } + + @Test + public void converges_to_wanted_state_when_nginx_not_running() throws IOException { + DataplaneProxyService.ProxyCommands proxyCommands = new TestProxyCommands(); + DataplaneProxyService service = dataplaneProxyService(proxyCommands); + + assertFalse(proxyCommands.isRunning()); + service.reconfigure(proxyConfig(), credentials(fileSystem)); + service.converge(); assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); + assertTrue(proxyCommands.isRunning()); + // Simulate nginx process dying + proxyCommands.stop(); + assertFalse(proxyCommands.isRunning()); + service.converge(); + assertTrue(proxyCommands.isRunning()); + } + + @Test + public void shuts_down() throws IOException { + DataplaneProxyService.ProxyCommands proxyCommands = new TestProxyCommands(); + DataplaneProxyService service = dataplaneProxyService(proxyCommands); + service.converge(); + assertTrue(proxyCommands.isRunning()); + assertEquals(DataplaneProxyService.NginxState.RUNNING, service.state()); + + new Thread(service::deconstruct).start(); // deconstruct will block until nginx is stopped + // Wait for above thread to set the wanted state to STOPPED + while (service.wantedState() != DataplaneProxyService.NginxState.STOPPED) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + } + service.converge(); + assertEquals(service.state(), DataplaneProxyService.NginxState.STOPPED); + assertFalse(proxyCommands.isRunning()); } private DataplaneProxyService dataplaneProxyService(DataplaneProxyService.ProxyCommands proxyCommands) throws IOException { @@ -101,4 +147,28 @@ public class DataplaneProxyServiceTest { uncheck(() -> Files.createDirectories(path)); return new DataplaneProxyCredentials(path.resolve("cert.pem"), path.resolve("key.pem")); } + + private static class TestProxyCommands implements DataplaneProxyService.ProxyCommands { + private boolean running = false; + + @Override + public void start(Path configFile) { + running = true; + } + + @Override + public void stop() { + running = false; + } + + @Override + public void reload() { + + } + + @Override + public boolean isRunning() { + return running; + } + } } |