private SegmentContainerRegistry createSegmentContainerRegistry() { SegmentContainerFactory containerFactory = getSingleton(this.containerFactory, this::createSegmentContainerFactory); return new StreamSegmentContainerRegistry(containerFactory, this.coreExecutor); }
@Override public CompletableFuture<ContainerHandle> startContainer(int containerId, Duration timeout) { Exceptions.checkNotClosed(this.closed.get(), this); // Check if container exists ContainerWithHandle existingContainer = this.containers.get(containerId); if (existingContainer != null) { if (!Services.isTerminating(existingContainer.container.state())) { // Container is already registered and not in the process of shutting down. throw new IllegalArgumentException(String.format("Container %d is already registered.", containerId)); } // Wait for the container to shut down, and then start a new one. return existingContainer.shutdownNotifier .thenComposeAsync(v -> startContainerInternal(containerId), this.executor); } else { // Start the container right away. return startContainerInternal(containerId); } }
private void handleContainerFailure(ContainerWithHandle containerWithHandle, Throwable exception) { unregisterContainer(containerWithHandle); log.error("Critical failure for SegmentContainer {}. {}", containerWithHandle, exception); }
/** * Tests the ability to detect a container failure and unregister the container in case the container fails while running. */ @Test public void testContainerFailureWhileRunning() throws Exception { final int containerId = 123; TestContainerFactory factory = new TestContainerFactory(); @Cleanup StreamSegmentContainerRegistry registry = new StreamSegmentContainerRegistry(factory, executorService()); ContainerHandle handle = registry.startContainer(containerId, TIMEOUT).join(); // Register a Listener for the Container.Stop event. Make this a Future since these callbacks are invoked async // so they may finish executing after stop() finished. CompletableFuture<Integer> stopListenerCallback = new CompletableFuture<>(); handle.setContainerStoppedListener(stopListenerCallback::complete); TestContainer container = (TestContainer) registry.getContainer(handle.getContainerId()); // Fail the container and wait for it to properly terminate. container.fail(new IntentionalException()); ServiceListeners.awaitShutdown(container, false); Assert.assertEquals("Unexpected value passed to Handle.stopListenerCallback or callback was not invoked.", containerId, (int) stopListenerCallback.join()); AssertExtensions.assertThrows( "Container is still registered after failure.", () -> registry.getContainer(containerId), ex -> ex instanceof ContainerNotFoundException); }
/** * Tests the ability to stop the container via the stopContainer() method. */ @Test public void testStopContainer() throws Exception { final int containerId = 123; TestContainerFactory factory = new TestContainerFactory(); @Cleanup StreamSegmentContainerRegistry registry = new StreamSegmentContainerRegistry(factory, executorService()); ContainerHandle handle = registry.startContainer(containerId, TIMEOUT).join(); // Register a Listener for the Container.Stop event. Make this a Future since these callbacks are invoked async // so they may finish executing after stop() finished. CompletableFuture<Integer> stopListenerCallback = new CompletableFuture<>(); handle.setContainerStoppedListener(stopListenerCallback::complete); TestContainer container = (TestContainer) registry.getContainer(handle.getContainerId()); Assert.assertFalse("Container is closed before being shut down.", container.isClosed()); registry.stopContainer(handle, TIMEOUT).join(); Assert.assertEquals("Unexpected value passed to Handle.stopListenerCallback or callback was not invoked.", containerId, (int) stopListenerCallback.join()); Assert.assertTrue("Container is not closed after being shut down.", container.isClosed()); AssertExtensions.assertThrows( "Container is still registered after being shut down.", () -> registry.getContainer(handle.getContainerId()), ex -> ex instanceof ContainerNotFoundException); }
/** * Creates a new Container and attempts to register it. This method works in an optimistic manner: it creates the * Container first and then attempts to register it, which should prevent us from having to lock on this entire method. * Creating new containers is cheap (we don't start them yet), so this operation should not take any extra resources. * * @param containerId The Id of the Container to start. * @return A CompletableFuture which will be completed with a ContainerHandle once the container has been started. */ private CompletableFuture<ContainerHandle> startContainerInternal(int containerId) { ContainerWithHandle newContainer = new ContainerWithHandle(this.factory.createStreamSegmentContainer(containerId), new SegmentContainerHandle(containerId)); ContainerWithHandle existingContainer = this.containers.putIfAbsent(containerId, newContainer); if (existingContainer != null) { // We had multiple concurrent calls to start this Container and some other request beat us to it. newContainer.container.close(); throw new IllegalArgumentException(String.format("Container %d is already registered.", containerId)); } log.info("Registered SegmentContainer {}.", containerId); // Attempt to Start the container, but first, attach a shutdown listener so we know to unregister it when it's stopped. Services.onStop( newContainer.container, () -> unregisterContainer(newContainer), ex -> handleContainerFailure(newContainer, ex), this.executor); return Services.startAsync(newContainer.container, this.executor) .thenApply(v -> newContainer.handle); }
TestContainerFactory factory = new TestContainerFactory(); @Cleanup StreamSegmentContainerRegistry registry = new StreamSegmentContainerRegistry(factory, executorService()); registry.startContainer(containerId, TIMEOUT).join(); TestContainer container1 = (TestContainer) registry.getContainer(containerId); () -> registry.startContainer(containerId, TIMEOUT), ex -> ex instanceof IllegalArgumentException); val startContainer2 = registry.startContainer(containerId, TIMEOUT); Assert.assertFalse("startContainer() completed before previous container shut down (with failure).", startContainer2.isDone()); TestContainer container2 = (TestContainer) registry.getContainer(containerId); val startContainer3 = registry.startContainer(containerId, TIMEOUT); Assert.assertFalse("startContainer() completed before previous container shut down (normally).", startContainer3.isDone()); TestContainer container3 = (TestContainer) registry.getContainer(containerId);
/** * Tests the getContainer method for registered and unregistered containers. */ @Test public void testGetContainer() throws Exception { final int containerCount = 1000; TestContainerFactory factory = new TestContainerFactory(); @Cleanup StreamSegmentContainerRegistry registry = new StreamSegmentContainerRegistry(factory, executorService()); HashSet<Integer> expectedContainerIds = new HashSet<>(); List<CompletableFuture<ContainerHandle>> handleFutures = new ArrayList<>(); for (int containerId = 0; containerId < containerCount; containerId++) { handleFutures.add(registry.startContainer(containerId, TIMEOUT)); expectedContainerIds.add(containerId); } List<ContainerHandle> handles = Futures.allOfWithResults(handleFutures).join(); HashSet<Integer> actualHandleIds = new HashSet<>(); for (ContainerHandle handle : handles) { actualHandleIds.add(handle.getContainerId()); SegmentContainer container = registry.getContainer(handle.getContainerId()); Assert.assertTrue("Wrong container Java type.", container instanceof TestContainer); Assert.assertEquals("Unexpected container Id.", handle.getContainerId(), container.getId()); container.close(); } AssertExtensions.assertContainsSameElements("Unexpected container ids registered.", expectedContainerIds, actualHandleIds); AssertExtensions.assertThrows( "getContainer did not throw when passed an invalid container id.", () -> registry.getContainer(containerCount + 1), ex -> ex instanceof ContainerNotFoundException); }
/** * Tests the ability to detect a container failure and unregister the container in case the container fails on startup. */ @Test public void testContainerFailureOnStartup() throws Exception { final int containerId = 123; // We insert a ReusableLatch that will allow us to manually delay the TestContainer's shutdown/closing process // so that we have enough time to verify that calling getContainer() on a currently shutting down container will // throw the appropriate exception. ReusableLatch closeReleaseSignal = new ReusableLatch(); TestContainerFactory factory = new TestContainerFactory(new IntentionalException(), closeReleaseSignal); @Cleanup StreamSegmentContainerRegistry registry = new StreamSegmentContainerRegistry(factory, executorService()); AssertExtensions.assertThrows( "Unexpected exception thrown upon failed container startup.", registry.startContainer(containerId, TIMEOUT)::join, ex -> ex instanceof IntentionalException || (ex instanceof IllegalStateException && ex.getCause() instanceof IntentionalException)); AssertExtensions.assertThrows( "Container is registered even if it failed to start (and is currently shut down).", () -> registry.getContainer(containerId), ex -> ex instanceof ContainerNotFoundException); // Unblock container closing, which will, in turn, unblock its de-registration. closeReleaseSignal.release(); AssertExtensions.assertThrows( "Container is registered even if it failed to start (and has been unregistered).", () -> registry.getContainer(containerId), ex -> ex instanceof ContainerNotFoundException); }