/* * Copyright 2014 Avanza Bank AB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.avanza.astrix.beans.service; import static org.hamcrest.CoreMatchers.any; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.junit.After; import org.junit.Test; import com.avanza.astrix.beans.core.AstrixBeanKey; import com.avanza.astrix.beans.core.AstrixBeanSettings; import com.avanza.astrix.beans.core.AstrixSettings; import com.avanza.astrix.beans.core.BeanProxy; import com.avanza.astrix.beans.core.BeanProxyFilter; import com.avanza.astrix.beans.core.BeanProxyNames; import com.avanza.astrix.beans.core.ReactiveTypeConverterImpl; import com.avanza.astrix.beans.ft.BeanFaultTolerance; import com.avanza.astrix.beans.ft.BeanFaultToleranceFactorySpi; import com.avanza.astrix.beans.registry.InMemoryServiceRegistry; import com.avanza.astrix.context.AstrixApplicationContext; import com.avanza.astrix.context.AstrixContext; import com.avanza.astrix.context.TestAstrixConfigurer; import com.avanza.astrix.core.IllegalServiceMetadataException; import com.avanza.astrix.core.ServiceUnavailableException; import com.avanza.astrix.core.function.CheckedCommand; import com.avanza.astrix.provider.component.AstrixServiceComponentNames; import com.avanza.astrix.provider.core.AstrixApiProvider; import com.avanza.astrix.provider.core.AstrixConfigDiscovery; import com.avanza.astrix.provider.core.Service; import com.avanza.astrix.test.util.AstrixTestUtil; import com.avanza.astrix.test.util.Poller; import com.avanza.astrix.versioning.core.AstrixObjectSerializer; import com.avanza.astrix.versioning.core.ObjectSerializerDefinition; import com.avanza.astrix.versioning.core.ObjectSerializerFactory; import rx.Observable; public class ServiceBeanInstanceTest { private AstrixContext astrixContext; @After public void destroy() { if (astrixContext != null) { astrixContext.destroy(); } } @Test public void addsFtProxyToServiceBean() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, new PingImpl()); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.enableFaultTolerance(true); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); final AtomicBoolean ftApplied = new AtomicBoolean(false); BeanFaultTolerance beanFt = new BeanFaultTolerance() { @Override public <T> Observable<T> observe(Supplier<Observable<T>> observable) { ftApplied.set(true); return observable.get(); } @Override public <T> T execute(CheckedCommand<T> command) throws Throwable { ftApplied.set(true); return command.call(); } }; astrixConfigurer.registerStrategy(BeanFaultToleranceFactorySpi.class, (beanKey) -> beanFt); astrixContext = astrixConfigurer.configure(); Ping ping = astrixContext.getBean(Ping.class); ftApplied.set(false); assertEquals("foo", ping.ping("foo")); assertTrue("Fault tolerance proxy should be applied to service beans", ftApplied.get()); } @Test public void ftProxyCanBeDisabledByServiceComponent() throws Exception { DisabledFtComponent component = new DisabledFtComponent(); InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, component.registerAndGetServiceProperties(Ping.class, new PingImpl())); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.enableFaultTolerance(true); astrixConfigurer.registerPlugin(ServiceComponent.class, component); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); final AtomicBoolean ftApplied = new AtomicBoolean(false); BeanFaultTolerance beanFt = new BeanFaultTolerance() { @Override public <T> Observable<T> observe(Supplier<Observable<T>> observable) { ftApplied.set(true); return observable.get(); } @Override public <T> T execute(CheckedCommand<T> command) throws Throwable { ftApplied.set(true); return command.call(); } }; astrixConfigurer.registerStrategy(BeanFaultToleranceFactorySpi.class, beanKey -> beanFt); astrixContext = astrixConfigurer.configure(); Ping ping = astrixContext.getBean(Ping.class); ftApplied.set(false); assertEquals("foo", ping.ping("foo")); assertFalse("Fault tolerance can be disabled by ServiceComponent", ftApplied.get()); } @Test public void itsPossibleToSetBeanInInactiveStateUsingBeanSettings() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, new PingImpl()); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); astrixContext = astrixConfigurer.configure(); final Ping ping = astrixContext.getBean(Ping.class); assertEquals("foo", ping.ping("foo")); astrixConfigurer.set(AstrixBeanSettings.AVAILABLE, AstrixBeanKey.create(Ping.class), false); AstrixTestUtil.assertThrows(() -> ping.ping("foo"), ServiceUnavailableException.class); } @Test public void waitForBeanReturnsWhenServiceIsBound() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.set(AstrixSettings.BEAN_BIND_ATTEMPT_INTERVAL, 1); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); astrixContext = astrixConfigurer.configure(); // Get bean in unbound state astrixContext.getBean(Ping.class); // Register provider serviceRegistry.registerProvider(Ping.class, new PingImpl()); // Bean should be bound astrixContext.waitForBean(Ping.class, 5000); } @Test public void whenServiceNotAvailableOnFirstBindAttemptTheServiceBeanShouldReattemptToBindLater() throws Exception { String serviceId = DirectComponent.register(Ping.class, new PingImpl()); TestAstrixConfigurer config = new TestAstrixConfigurer(); config.set(AstrixSettings.BEAN_BIND_ATTEMPT_INTERVAL, 10); config.set("pingUri", DirectComponent.getServiceUri(serviceId)); config.registerApiProvider(PingApiProviderUsingConfigLookup.class); AstrixContext context = config.configure(); // Unregister to simulate service that is available in config, but provider not available. DirectComponent.unregister(serviceId); final Ping ping = context.getBean(Ping.class); try { ping.ping("foo"); fail("Bean should not be bound"); } catch (ServiceUnavailableException e) { // expected } DirectComponent.register(Ping.class, new PingImpl(), serviceId); assertEventually(() -> ping.ping("foo"), equalTo("foo")); } @Test public void boundServiceInstancesShouldBeReleasedWhenContextIsDestroyed() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, new PingImpl()); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); AstrixApplicationContext astrixContext = (AstrixApplicationContext) astrixConfigurer.configure(); Ping ping = astrixContext.getBean(Ping.class); assertEquals("foo", ping.ping("foo")); DirectComponent directComponent = (DirectComponent) astrixContext.getInstance(ServiceComponentRegistry.class).getComponent(AstrixServiceComponentNames.DIRECT); assertEquals(2, directComponent.getBoundServices().size()); assertThat("Expected at least one service to be bound after pingBean is bound", directComponent.getBoundServices().size(), greaterThanOrEqualTo(1)); astrixContext.destroy(); assertEquals("All bound beans should be release when context is destroyed", 0, directComponent.getBoundServices().size()); } @Test public void boundServiceInstancesShouldBeReleasedWhenMovingToUnboundState() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, new PingImpl()); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.set(AstrixSettings.SERVICE_LEASE_RENEW_INTERVAL, 5); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); AstrixApplicationContext astrixContext = (AstrixApplicationContext) astrixConfigurer.configure(); DirectComponent directComponent = (DirectComponent) astrixContext.getInstance(ServiceComponentRegistry.class).getComponent(AstrixServiceComponentNames.DIRECT); final Ping ping = astrixContext.getBean(Ping.class); ping.ping("foo"); assertEquals(2, directComponent.getBoundServices().size()); serviceRegistry.clear(); assertEventuallyThrows(() -> ping.ping("foo"), any(ServiceUnavailableException.class)); assertEquals(1, directComponent.getBoundServices().size()); } @Test public void serviceBeanInstanceUsesDefaultSubsystemNameWhenNoSubsystemIsSetInServiceProperties() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); String serviceUri = DirectComponent.registerAndGetUri(Ping.class, new PingImpl()); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProviderUsingConfigLookup.class); astrixConfigurer.setSubsystem("default"); astrixConfigurer.set("pingUri", serviceUri); astrixConfigurer.set(AstrixSettings.ENFORCE_SUBSYSTEM_BOUNDARIES, true); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); AstrixContext astrixContext = astrixConfigurer.configure(); assertEquals("foo", astrixContext.getBean(Ping.class).ping("foo")); } @Test(expected = MyException.class) public void exceptionsThrownArePropagatedForServices() throws Exception { InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); serviceRegistry.registerProvider(Ping.class, new Ping() { @Override public String ping(String msg) { throw new MyException(); } }); TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.set(AstrixSettings.SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); astrixContext = astrixConfigurer.configure(); astrixContext.getBean(Ping.class).ping("foo"); } @Test public void throwsServiceUnavailableExceptionIfBeanNotBoundBeforeTimeout() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixContext = astrixConfigurer.configure(); try { astrixContext.waitForBean(Ping.class, 1); fail("Expected exception when bean can't be bound before timeout"); } catch (ServiceUnavailableException e) { assertThat("Root cause for waitForBean", e.getCause(), CoreMatchers.instanceOf(NoServiceProviderFound.class)); } } @Test public void throwsIllegalMetadataExceptionIfServiceIsNotWellFormed() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProviderUsingConfigLookup.class); astrixConfigurer.registerPlugin(ServiceComponent.class, new FakeComponent()); astrixConfigurer.set("pingUri", "test:"); astrixContext = astrixConfigurer.configure(); try { astrixContext.waitForBean(Ping.class, 1).ping("foo"); } catch (IllegalServiceMetadataException e) { assertThat("Expected IllegalServiceMetadataException with message: ", e.getMessage().toLowerCase(), CoreMatchers.containsString("illegal metadata")); } } @Test public void beanIsConsideredBoundWhenMovedToIllegalMetadataState() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.set(AstrixSettings.BEAN_BIND_ATTEMPT_INTERVAL, 1); astrixConfigurer.registerApiProvider(PingApiProviderUsingConfigLookup.class); astrixConfigurer.registerPlugin(ServiceComponent.class, new FakeComponent()); AstrixContext astrixContext = astrixConfigurer.configure(); // Get bean, will be unbound astrixContext.getBean(Ping.class); // Register provider with illegal metadata astrixConfigurer.set("pingUri", "test:"); astrixContext.waitForBean(Ping.class, 100); } @Test public void throwsServiceDiscoveryErrorWhenServiceDiscoveryThrowsAnException() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProvider.class); astrixConfigurer.registerPlugin(ServiceComponent.class, new FakeComponent()); astrixContext = astrixConfigurer.configure(); try { astrixContext.waitForBean(Ping.class, 1).ping("foo"); fail("Expected ServiceDiscoveryError"); } catch (ServiceDiscoveryError e) { assertThat("Root cause for ServiceDiscoveryError", e.getCause(), CoreMatchers.instanceOf(NoServiceProviderFound.class)); } } @Test public void throwsNoServiceProviderFoundWhenNoServiceProviderFound() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProviderUsingConfigLookup.class); astrixConfigurer.registerPlugin(ServiceComponent.class, new FakeComponent()); // No serviceUri for Ping in configuration => NoProviderFound astrixContext = astrixConfigurer.configure(); try { astrixContext.waitForBean(Ping.class, 1).ping("foo"); fail("Expected NoServiceProviderFound"); } catch (NoServiceProviderFound e) { } } @Test public void throwsServiceBindErrorWhenBindingADiscoveredServiceFails() throws Exception { TestAstrixConfigurer astrixConfigurer = new TestAstrixConfigurer(); astrixConfigurer.registerApiProvider(PingApiProviderUsingConfigLookup.class); astrixConfigurer.registerPlugin(ServiceComponent.class, new FakeComponent()); astrixConfigurer.set("pingUri", "direct:-21"); // DirectComponent will not find the associated provider (since it does not exist) astrixContext = astrixConfigurer.configure(); try { astrixContext.waitForBean(Ping.class, 1).ping("foo"); fail("Expected ServiceBindError"); } catch (ServiceBindError e) { } } private <T extends Exception> void assertEventuallyThrows(Supplier<?> invocation, Matcher<T> returnValueMatcher) throws InterruptedException { new Poller(1000, 1).check(AstrixTestUtil.serviceInvocationException(invocation, returnValueMatcher)); } private <T> void assertEventually(Supplier<T> invocation, Matcher<T> returnValueMatcher) throws InterruptedException { new Poller(1000, 1).check(AstrixTestUtil.serviceInvocationResult(invocation, returnValueMatcher)); } static class FakeComponent implements ServiceComponent { @Override public <T> BoundServiceBeanInstance<T> bind(ServiceDefinition<T> versioningContext, ServiceProperties serviceProperties) { throw new IllegalServiceMetadataException("Illegal metadata"); } @Override public String getName() { return "test"; } @Override public ServiceProperties parseServiceProviderUri(String serviceProviderUri) { return new ServiceProperties(); } @Override public <T> ServiceProperties createServiceProperties(ServiceDefinition<T> exportedServiceDefinition) { throw new UnsupportedOperationException(); } @Override public boolean canBindType(Class<?> type) { return true; } @Override public <T> void exportService(Class<T> providedApi, T provider, ServiceDefinition<T> serviceDefinition) { } @Override public boolean requiresProviderInstance() { return false; } } static class MyException extends RuntimeException { private static final long serialVersionUID = 1L; } @AstrixApiProvider public interface PingApiProvider { @Service Ping ping(); } @AstrixApiProvider public interface PingApiProviderUsingConfigLookup { @AstrixConfigDiscovery("pingUri") @Service Ping ping(); } public interface Ping { String ping(String msg); } public static class DisabledFtComponent implements ServiceComponent, BeanProxyFilter { private final DirectComponent directComponent = new DirectComponent(new ObjectSerializerFactory() { @Override public AstrixObjectSerializer create(ObjectSerializerDefinition serializerDefinition) { return new AstrixObjectSerializer.NoVersioningSupport(); } }, new ReactiveTypeConverterImpl(Collections.emptyList())); @Override public boolean applyBeanProxy(BeanProxy beanProxy) { return !BeanProxyNames.FAULT_TOLERANCE.equals(beanProxy.name()); } public <T> ServiceProperties registerAndGetServiceProperties(Class<T> bean, T provider) { ServiceProperties result = DirectComponent.registerAndGetProperties(bean, provider); result.setComponent(getName()); return result; } @Override public String getName() { return "NO_FT_DIRECT_COMPONENT"; } @Override public <T> BoundServiceBeanInstance<T> bind(ServiceDefinition<T> serviceDefinition, ServiceProperties serviceProperties) { return directComponent.bind(serviceDefinition, serviceProperties); } @Override public ServiceProperties parseServiceProviderUri(String serviceProviderUri) { ServiceProperties serviceProperties = directComponent.parseServiceProviderUri(serviceProviderUri); serviceProperties.setComponent(getName()); return serviceProperties; } @Override public <T> ServiceProperties createServiceProperties(ServiceDefinition<T> exportedServiceDefinition) { return null; } @Override public boolean canBindType(Class<?> type) { return true; } @Override public <T> void exportService(Class<T> providedApi, T provider, ServiceDefinition<T> serviceDefinition) { } @Override public boolean requiresProviderInstance() { return false; } } public static class PingImpl implements Ping { public String ping(String msg) { return msg; } } }