/* * 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.integration.tests; import static com.avanza.astrix.beans.core.AstrixSettings.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.avanza.astrix.beans.core.AstrixBeanKey; import com.avanza.astrix.beans.core.AstrixSettings; import com.avanza.astrix.beans.registry.AstrixServiceRegistryEntry; import com.avanza.astrix.beans.registry.InMemoryServiceRegistry; import com.avanza.astrix.beans.registry.ServiceRegistryClient; import com.avanza.astrix.beans.service.ServiceProperties; import com.avanza.astrix.config.ConfigSource; import com.avanza.astrix.config.DynamicConfig; import com.avanza.astrix.config.GlobalConfigSourceRegistry; import com.avanza.astrix.config.MapConfigSource; import com.avanza.astrix.context.AstrixConfigurer; import com.avanza.astrix.context.AstrixContext; import com.avanza.astrix.provider.component.AstrixServiceComponentNames; import com.avanza.astrix.provider.core.AstrixApiProvider; import com.avanza.astrix.provider.core.AstrixApplication; import com.avanza.astrix.provider.core.AstrixServiceExport; import com.avanza.astrix.provider.core.Service; import com.avanza.astrix.serviceunit.ServiceAdministrator; import com.avanza.astrix.spring.AstrixFrameworkBean; import com.avanza.astrix.test.util.AutoCloseableRule; import com.avanza.astrix.test.util.Poller; import com.avanza.astrix.test.util.Probe; import com.avanza.astrix.versioning.core.AstrixObjectSerializerConfig; import com.avanza.astrix.versioning.core.AstrixObjectSerializerConfigurer; import com.avanza.astrix.versioning.core.Versioned; import com.avanza.astrix.versioning.jackson2.AstrixJsonApiMigration; import com.avanza.astrix.versioning.jackson2.Jackson2ObjectSerializerConfigurer; import com.avanza.astrix.versioning.jackson2.JacksonObjectMapperBuilder; import org.hamcrest.Description; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; /** * * @author Elias Lindholm (elilin) * */ public class AstrixServiceBlueGreenDeployTest { private InMemoryServiceRegistry serviceRegistry = new InMemoryServiceRegistry(); static String ACCOUNT_PERFORMANCE_SUBSYSTEM = "account-performance-subsystem"; private MapConfigSource accountPerformanceClientConfig = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(SUBSYSTEM_NAME, "client-subsystem"); }}; private MapConfigSource feeder1clientConfig = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(SUBSYSTEM_NAME, ACCOUNT_PERFORMANCE_SUBSYSTEM); set(APPLICATION_TAG, "1"); }}; private MapConfigSource feeder2clientConfig = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(SUBSYSTEM_NAME, ACCOUNT_PERFORMANCE_SUBSYSTEM); set(APPLICATION_TAG, "2"); }}; private MapConfigSource server1Config = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(APPLICATION_INSTANCE_ID, "server-1"); set(SERVICE_ADMINISTRATOR_COMPONENT, AstrixServiceComponentNames.DIRECT); set(APPLICATION_TAG, "1"); }}; private MapConfigSource server2Config = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(APPLICATION_INSTANCE_ID, "server-2"); set(SERVICE_ADMINISTRATOR_COMPONENT, AstrixServiceComponentNames.DIRECT); set(APPLICATION_TAG, "2"); }}; private MapConfigSource feeder1Config = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(APPLICATION_TAG, "1"); set(SERVICE_ADMINISTRATOR_COMPONENT, AstrixServiceComponentNames.DIRECT); set(APPLICATION_INSTANCE_ID, "feeder-server-1"); }}; private MapConfigSource feeder2Config = new MapConfigSource() {{ set(AstrixSettings.ENABLE_FAULT_TOLERANCE, false); set(SERVICE_REGISTRY_URI, serviceRegistry.getServiceUri()); set(SERVICE_LEASE_RENEW_INTERVAL, 100); set(BEAN_BIND_ATTEMPT_INTERVAL, 100); set(APPLICATION_TAG, "2"); set(SERVICE_ADMINISTRATOR_COMPONENT, AstrixServiceComponentNames.DIRECT); set(APPLICATION_INSTANCE_ID, "feeder-server-2"); }}; @Rule public AutoCloseableRule autoClosables = new AutoCloseableRule(); private AstrixContext feeder1clientContext; private AstrixContext feeder2clientContext; private final AstrixSpringApp server1 = autoClosables.add(new AstrixSpringApp(server1Config, AccountPerformanceAppConfig.class)); private final AstrixSpringApp server2 = autoClosables.add(new AstrixSpringApp(server2Config, AccountPerformanceAppConfig.class)); private final AstrixSpringApp feeder1 = autoClosables.add(new AstrixSpringApp(feeder1Config, FeederAppConfig.class)); private final AstrixSpringApp feeder2 = autoClosables.add(new AstrixSpringApp(feeder2Config, FeederAppConfig.class)); private AstrixContext accountPerformanceClientContext; private AccountPerformance accountPerformance; static class AstrixSpringApp implements AutoCloseable { private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); public AstrixSpringApp(MapConfigSource configSource, Class<?> configuration) { this.context.register(configuration); Map<String, Object> settings = new HashMap<>(); settings.put("configSourceId", GlobalConfigSourceRegistry.register(configSource)); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource("astrixSettings", settings)); } @Override public void close() throws Exception { this.context.close(); } public void start() { this.context.refresh(); } } @Before public void setup() throws Exception { this.feeder1clientContext = autoClosables.add(new AstrixConfigurer().setConfig(DynamicConfig.create(feeder1clientConfig)).configure()); this.feeder2clientContext = autoClosables.add(new AstrixConfigurer().setConfig(DynamicConfig.create(feeder2clientConfig)).configure()); this.accountPerformanceClientContext = autoClosables.add(new AstrixConfigurer().setConfig(DynamicConfig.create(accountPerformanceClientConfig)).configure()); } @Test public void registersServicesInZoneUsingSubsystemNameAndTag() throws Exception { feeder1.start(); feeder1clientContext.waitForBean(FeederService.class, 1000); List<AstrixServiceRegistryEntry> providers = this.serviceRegistry.listServices(FeederService.class.getName(), null); assertEquals(1, providers.size()); AstrixServiceRegistryEntry providerProperties = providers.get(0); assertEquals(ACCOUNT_PERFORMANCE_SUBSYSTEM + "#1", providerProperties.getServiceProperties().get(ServiceProperties.SERVICE_ZONE)); } @Test public void blueGreenDeployTest() throws Exception { server1Config.set(AstrixSettings.PUBLISH_SERVICES, true); server1.start(); server2Config.set(AstrixSettings.PUBLISH_SERVICES, false); server2.start(); this.accountPerformance = accountPerformanceClientContext.waitForBean(AccountPerformance.class, 1000); String respondingAppIntance = accountPerformance.getAppInstanceId(); assertEquals("server-1", respondingAppIntance); // Activate service-2 ServiceAdministrator serviceInstance2Administrator = feeder1clientContext.waitForBean(ServiceAdministrator.class, "server-2", 1000L); serviceInstance2Administrator.setPublishServices(true); ServiceAdministrator serviceInstance1Administrator = feeder1clientContext.waitForBean(ServiceAdministrator.class, "server-1", 1000L); serviceInstance1Administrator.setPublishServices(false); ServiceRegistryClient serviceRegistryClient = feeder1clientContext.getBean(ServiceRegistryClient.class); List<ServiceProperties> serviceProperties = serviceRegistryClient.list(AstrixBeanKey.create(AccountPerformance.class)); assertEquals(2, serviceProperties.size()); // // Verify traffic eventually moves to instance-2 assertEventually(new Probe() { private String lastReply; private Exception lastException; @Override public boolean isSatisfied() { return lastReply.startsWith("server-2"); } @Override public void sample() { try { lastReply = accountPerformance.getAppInstanceId(); } catch (Exception e) { lastException = e; } } @Override public void describeFailureTo(Description description) { description.appendText("Expected reply from instance 1, but was reply=" + lastReply + " lastException:\n" + lastException); } }); } @Test public void serviceGoesToInactiveStateWhenServerIsDeactivated() throws Exception { server1Config.set(AstrixSettings.PUBLISH_SERVICES, true); server1.start(); this.accountPerformance = accountPerformanceClientContext.waitForBean(AccountPerformance.class, 1000); assertNotNull(accountPerformance.getAppInstanceId()); ServiceAdministrator serviceInstance1Administrator = feeder1clientContext.waitForBean(ServiceAdministrator.class, "server-1", 1000); serviceInstance1Administrator.setPublishServices(false); assertEventually(new Probe() { private boolean currentServiceState; @Override public void sample() { ServiceRegistryClient serviceRegistryClient = feeder1clientContext.getBean(ServiceRegistryClient.class); List<ServiceProperties> servicePropertyList = serviceRegistryClient.list(AstrixBeanKey.create(AccountPerformance.class)); assertEquals("registered service count" + servicePropertyList, 1, servicePropertyList.size()); ServiceProperties serviceProperties = servicePropertyList.get(0); currentServiceState = Boolean.valueOf(serviceProperties.getProperties().get(ServiceProperties.PUBLISHED)); } @Override public boolean isSatisfied() { return currentServiceState == false; } @Override public void describeFailureTo(Description description) { description.appendText("Expected service to be non published, but it was published=" + currentServiceState); } }); } @Test public void blueGreenDeployTest_TwoApplicationsInSameSubsystem() throws Exception { server1Config.set(AstrixSettings.PUBLISH_SERVICES, true); server1.start(); feeder1.start(); // Start isolated version of subsystem in state INACTIVE server2Config.set(AstrixSettings.PUBLISH_SERVICES, false); server2.start(); feeder2.start(); FeederService feeder1 = feeder1clientContext.waitForBean(FeederService.class, 2000); FeederService feeder2 = feeder2clientContext.waitForBean(FeederService.class, 2000); this.accountPerformance = accountPerformanceClientContext.waitForBean(AccountPerformance.class, 2000); assertEquals("feeder-server-1", feeder1.getAppInstanceId()); // Verify feeder1 talks to correct instance assertEquals("feeder-server-2", feeder2.getAppInstanceId()); feeder1.setPerformance("21", 100); feeder2.setPerformance("21", 200); assertEquals("server-1", accountPerformance.getAppInstanceId()); // Verify server-1 is published assertEquals(Integer.valueOf(100), accountPerformance.getPerformance("21")); feeder1clientContext.waitForBean(ServiceAdministrator.class, "server-2", 2000).setPublishServices(true); feeder1clientContext.waitForBean(ServiceAdministrator.class, "server-1", 2000).setPublishServices(false); // Verify traffic eventually moves to instance-2 assertEventually(new Probe() { Integer lastReply; @Override public boolean isSatisfied() { return Integer.valueOf(200).equals(lastReply); } @Override public void sample() { lastReply = accountPerformance.getPerformance("21"); } @Override public void describeFailureTo(Description description) { description.appendText("Expected performance from server-2, but was : " + lastReply); } }); } @Configuration public static class AccountPerformanceAppConfig { @Bean public static AstrixFrameworkBean astrix() { AstrixFrameworkBean astrix = new AstrixFrameworkBean(); astrix.setSubsystem(ACCOUNT_PERFORMANCE_SUBSYSTEM); astrix.setApplicationDescriptor(AccountPerformanceApplicationDescriptor.class); return astrix; } @Bean public DynamicConfig config(Environment env) { String configSourceId = env.getProperty("configSourceId"); ConfigSource configSource = GlobalConfigSourceRegistry.getConfigSource(configSourceId); return DynamicConfig.create(configSource); } @Bean public AccountPerformance accountPerformance(DynamicConfig config) { return new AccountPerformanceImpl(AstrixSettings.APPLICATION_INSTANCE_ID.getFrom(config).get()); } } public static class DummyVersioningConfig implements AstrixObjectSerializerConfigurer { } @Configuration public static class FeederAppConfig { @Bean public static AstrixFrameworkBean astrix() { AstrixFrameworkBean astrix = new AstrixFrameworkBean(); astrix.setSubsystem(ACCOUNT_PERFORMANCE_SUBSYSTEM); astrix.setApplicationDescriptor(FeederApplicationDescriptor.class); return astrix; } @Bean public DynamicConfig config(Environment env) { String configSourceId = env.getProperty("configSourceId"); ConfigSource configSource = GlobalConfigSourceRegistry.getConfigSource(configSourceId); return DynamicConfig.create(configSource); } @Bean public FeederServiceImpl ping(AstrixContext astrix, DynamicConfig config) { return new FeederServiceImpl(astrix.getBean(AccountPerformanceInternal.class), AstrixSettings.APPLICATION_INSTANCE_ID.getFrom(config).get()); } } @AstrixApplication( exportsRemoteServicesFor = AccountPerformaneApi.class, defaultServiceComponent = AstrixServiceComponentNames.DIRECT ) public static class AccountPerformanceApplicationDescriptor { } @AstrixApplication( exportsRemoteServicesFor = FeederApi.class, defaultServiceComponent = AstrixServiceComponentNames.DIRECT ) public static class FeederApplicationDescriptor { } @AstrixObjectSerializerConfig( objectSerializerConfigurer = AccountPerformanceVersioningConfig.class, version = 1 ) @AstrixApiProvider public static interface AccountPerformaneApi { @Versioned @Service AccountPerformance accountPerformance(); @Service AccountPerformanceInternal accountPerformanceInternal(); } public static class AccountPerformanceVersioningConfig implements Jackson2ObjectSerializerConfigurer { @Override public List<? extends AstrixJsonApiMigration> apiMigrations() { return Collections.emptyList(); } @Override public void configure(JacksonObjectMapperBuilder objectMapperBuilder) { } } @AstrixApiProvider public interface FeederApi { @Service FeederService feederService(); } public interface AccountPerformance { String getAppInstanceId(); Integer getPerformance(String accountId); } public interface AccountPerformanceInternal { void setPerformance(String accountId, Integer performance); } public interface FeederService { String getAppInstanceId(); void setPerformance(String accountId, Integer performance); } @AstrixServiceExport(FeederService.class) public static class FeederServiceImpl implements FeederService { private AccountPerformanceInternal accountPerformance; private String applicationInstanceId; public FeederServiceImpl(AccountPerformanceInternal updater, String applicationInstanceId) { this.accountPerformance = updater; this.applicationInstanceId = applicationInstanceId; } @Override public void setPerformance(String accountId, Integer performance) { this.accountPerformance.setPerformance(accountId, performance); } @Override public String getAppInstanceId() { return this.applicationInstanceId; } } @AstrixServiceExport({AccountPerformance.class, AccountPerformanceInternal.class}) public static class AccountPerformanceImpl implements AccountPerformance, AccountPerformanceInternal { private String applicationInstanceId; private final ConcurrentMap<String, Integer> performanceByAccountId = new ConcurrentHashMap<>(); public AccountPerformanceImpl(String applicationInstanceId) { this.applicationInstanceId = applicationInstanceId; } @Override public String getAppInstanceId() { return applicationInstanceId; } @Override public Integer getPerformance(String accountId) { return performanceByAccountId.get(accountId); } @Override public void setPerformance(String accountId, Integer performance) { this.performanceByAccountId.put(accountId, performance); } } private void assertEventually(Probe probe) throws InterruptedException { new Poller(10_000, 10).check(probe); } }