diff --git a/spring-geode-samples/caching/multi-site/lombok.config b/spring-geode-samples/caching/multi-site/lombok.config new file mode 100644 index 00000000..6aa51d71 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/spring-geode-samples/caching/multi-site/spring-geode-samples-caching-multisite.gradle b/spring-geode-samples/caching/multi-site/spring-geode-samples-caching-multisite.gradle new file mode 100644 index 00000000..01e8128f --- /dev/null +++ b/spring-geode-samples/caching/multi-site/spring-geode-samples-caching-multisite.gradle @@ -0,0 +1,27 @@ +plugins { + id "io.freefair.lombok" version "5.0.0-rc6" +} + +apply plugin: 'io.spring.convention.spring-sample-boot' + +description = "Spring Geode Sample for Multi-Site Caching." + +dependencies { + + compile project(":spring-geode-starter") + + compile "org.assertj:assertj-core" + compile "org.projectlombok:lombok" + compile "org.springframework.boot:spring-boot-starter-web" + + runtime project(":spring-geode-starter-logging") + + testCompile project(":spring-geode-starter-test") + + testCompile "org.springframework.boot:spring-boot-starter-test" + +} + +bootJar { + mainClassName = 'example.app.caching.multisite.client.BootGeodeMultiSiteCachingClientApplication' +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/BootGeodeMultiSiteCachingClientApplication.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/BootGeodeMultiSiteCachingClientApplication.java new file mode 100644 index 00000000..9ec58d27 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/BootGeodeMultiSiteCachingClientApplication.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.client; + +import org.apache.geode.cache.client.ClientCache; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.config.annotation.EnableCachingDefinedRegions; + +/** + * The {@link BootGeodeMultiSiteCachingClientApplication} class is a Spring Boot, Apache Geode {@link ClientCache} + * Web application that can be configured to connect to 1 of many Apache Geode clusters. + * + * @author John Blum + * @see org.apache.geode.cache.client.ClientCache + * @see org.springframework.boot.SpringApplication + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @since 1.3.0 + */ +@SpringBootApplication +@SuppressWarnings("unused") +public class BootGeodeMultiSiteCachingClientApplication { + + public static void main(String[] args) { + SpringApplication.run(BootGeodeMultiSiteCachingClientApplication.class, args); + } + + @ClientCacheApplication + @EnableCachingDefinedRegions + static class GeodeClientConfiguration { } + +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/model/Customer.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/model/Customer.java new file mode 100644 index 00000000..74ab43d6 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/model/Customer.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.client.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.gemfire.mapping.annotation.Region; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.ToString; + +/** + * {@link Customer} is an Abstract Data Type (ADT) modeling a customer in a customer service application. + * + * @author John Blum + * @see org.springframework.data.annotation.Id + * @see org.springframework.data.gemfire.mapping.annotation.Region + * @since 1.3.0 + */ +@Region("Customers") +@EqualsAndHashCode +@ToString(of = "name") +@RequiredArgsConstructor(staticName = "newCustomer") +public class Customer { + + @Id @Getter @NonNull + private Long id; + + @Getter @NonNull + private String name; + +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/service/CustomerService.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/service/CustomerService.java new file mode 100644 index 00000000..708b2839 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/service/CustomerService.java @@ -0,0 +1,81 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.client.service; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import example.app.caching.multisite.client.model.Customer; +import example.app.caching.multisite.client.util.ThreadUtils; + +/** + * {@link CustomerService} is a Spring application {@link Service} component used to service interactions with + * {@link Customer customers}. + * + * This service class employs the {@literal Look-Aside Caching} Pattern to lookups of {@link Customer} + * by {@link String name} since it is not common for the {@link Customer Customer's} {@link String name} + * to change frequently. + * + * Additionally, the {@link #findBy(String)} {@link CustomerService} operations simulates an expensive, + * or resource-intensive operation by introducing a delay. + * + * @author John Blum + * @see org.springframework.cache.annotation.Cacheable + * @see org.springframework.stereotype.Service + * @see example.app.caching.multisite.client.model.Customer + * @since 1.3.0 + */ +@Service +public class CustomerService { + + private static final long SLEEP_IN_SECONDS = 5; + + private final AtomicBoolean cacheMiss = new AtomicBoolean(false); + + private final AtomicLong customerId = new AtomicLong(0L); + + private volatile Long sleepInSeconds; + + @Cacheable("CustomersByName") + public Customer findBy(String name) { + setCacheMiss(); + ThreadUtils.safeSleep(name, Duration.ofSeconds(getSleepInSeconds())); + return Customer.newCustomer(this.customerId.incrementAndGet(), name); + } + + public boolean isCacheMiss() { + return this.cacheMiss.compareAndSet(true, false); + } + + protected void setCacheMiss() { + this.cacheMiss.set(true); + } + + public Long getSleepInSeconds() { + + Long sleepInSeconds = this.sleepInSeconds; + + return sleepInSeconds != null ? sleepInSeconds : SLEEP_IN_SECONDS; + } + + public void setSleepInSeconds(Long seconds) { + this.sleepInSeconds = seconds; + } +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/util/ThreadUtils.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/util/ThreadUtils.java new file mode 100644 index 00000000..2ca6501d --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/util/ThreadUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.client.util; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +import org.springframework.lang.NonNull; + +/** + * Abstract utility class for {@link Thread Threading}. + * + * @author John Blum + * @see java.lang.Thread + * @see java.time.Duration + * @since 1.3.0 + */ +public abstract class ThreadUtils { + + @SuppressWarnings("all") + public static void safeSleep(@NonNull Object lock, @NonNull Duration duration) { + + boolean interrupted = false; + + long timeout = System.currentTimeMillis() + duration.toMillis(); + + while (System.currentTimeMillis() < timeout) { + synchronized (lock) { + try { + lock.wait(duration.toMillis(), 0); + } + catch (InterruptedException ignore) { + interrupted = true; + } + } + } + + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + public static boolean waitFor(@NonNull Duration duration, @NonNull Condition condition) { + return waitFor(duration, duration.toMillis() / 10L, condition); + } + + @SuppressWarnings("all") + public static boolean waitFor(Duration duration, long interval, Condition condition) { + + long timeout = System.currentTimeMillis() + duration.toMillis(); + + try { + while (!condition.evaluate() && System.currentTimeMillis() < timeout) { + synchronized (condition) { + TimeUnit.MILLISECONDS.timedWait(condition, interval); + } + } + } + catch (InterruptedException cause) { + Thread.currentThread().interrupt(); + } + + return condition.evaluate(); + } + + @FunctionalInterface + public interface Condition { + boolean evaluate(); + } +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/web/CustomerController.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/web/CustomerController.java new file mode 100644 index 00000000..86ee758c --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/client/web/CustomerController.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.client.web; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import example.app.caching.multisite.client.model.Customer; +import example.app.caching.multisite.client.service.CustomerService; + +/** + * {@link CustomerController} is a Spring Web application {@link RestController} component used to + * service user requests for {@link Customer customers} through a REST API (interface) over HTTP. + * + * @author John Blum + * @see org.springframework.core.env.Environment + * @see org.springframework.web.bind.annotation.GetMapping + * @see org.springframework.web.bind.annotation.RestController + * @see example.app.caching.multisite.client.model.Customer + * @see example.app.caching.multisite.client.service.CustomerService + * @since 1.3.0 + */ +@RestController +public class CustomerController { + + @Autowired + private CustomerService customerService; + + @Autowired + private Environment environment; + + @GetMapping("/customers/{name}") + public CustomerHolder searchBy(@PathVariable String name) { + + return CustomerHolder.from(this.customerService.findBy(name)) + .setCacheMiss(this.customerService.isCacheMiss()); + } + + @GetMapping("/ping") + public String pingPong() { + return "PONG"; + } + + @GetMapping("/") + public String home() { + + return String.format("%s is running!", + environment.getProperty("spring.application.name", "UNKNOWN")); + } + + public static class CustomerHolder { + + public static CustomerHolder from(Customer customer) { + return new CustomerHolder(customer); + } + + private boolean cacheMiss = true; + + private final Customer customer; + + protected CustomerHolder(Customer customer) { + + Assert.notNull(customer, "Customer must not be null"); + + this.customer = customer; + } + + public CustomerHolder setCacheMiss(boolean cacheMiss) { + this.cacheMiss = cacheMiss; + return this; + + } + + public boolean isCacheMiss() { + return this.cacheMiss; + } + + public Customer getCustomer() { + return customer; + } + } +} diff --git a/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/server/BootGeodeMultiSiteCachingServerApplication.java b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/server/BootGeodeMultiSiteCachingServerApplication.java new file mode 100644 index 00000000..5db3d2a4 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/java/example/app/caching/multisite/server/BootGeodeMultiSiteCachingServerApplication.java @@ -0,0 +1,199 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite.server; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.DataPolicy; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.server.CacheServer; +import org.apache.geode.cache.wan.GatewayReceiver; +import org.apache.geode.cache.wan.GatewaySender; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.core.env.Environment; +import org.springframework.data.gemfire.PeerRegionFactoryBean; +import org.springframework.data.gemfire.ReplicatedRegionFactoryBean; +import org.springframework.data.gemfire.config.annotation.CacheServerApplication; +import org.springframework.data.gemfire.config.annotation.EnableLocator; +import org.springframework.data.gemfire.config.annotation.EnableManager; +import org.springframework.data.gemfire.config.annotation.RegionConfigurer; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.data.gemfire.util.RegionUtils; +import org.springframework.data.gemfire.wan.GatewayReceiverFactoryBean; +import org.springframework.data.gemfire.wan.GatewaySenderFactoryBean; + +import example.app.caching.multisite.client.model.Customer; + +/** + * The {@link BootGeodeMultiSiteCachingServerApplication} class is a Spring Boot, Apache Geode {@literal peer} + * {@link CacheServer} application serving cache clients. + * + * This Apache Geode peer member server data node can also connect to a remote cluster over a WAN using Gateways. + * + * @author John Blum + * @see org.apache.geode.cache.Cache + * @see org.apache.geode.cache.Region + * @see org.apache.geode.cache.server.CacheServer + * @see org.apache.geode.cache.wan.GatewayReceiver + * @see org.apache.geode.cache.wan.GatewaySender + * @see org.springframework.boot.ApplicationRunner + * @see org.springframework.boot.autoconfigure.SpringBootApplication + * @see org.springframework.boot.builder.SpringApplicationBuilder + * @see org.springframework.context.annotation.Bean + * @see org.springframework.context.annotation.Configuration + * @see org.springframework.context.annotation.Profile + * @see org.springframework.data.gemfire.ReplicatedRegionFactoryBean + * @see org.springframework.data.gemfire.config.annotation.CacheServerApplication + * @see org.springframework.data.gemfire.config.annotation.EnableLocator + * @see org.springframework.data.gemfire.config.annotation.EnableManager + * @see org.springframework.data.gemfire.config.annotation.RegionConfigurer + * @see org.springframework.data.gemfire.wan.GatewayReceiverFactoryBean + * @see org.springframework.data.gemfire.wan.GatewaySenderFactoryBean + * @since 1.3.0 + */ +@SpringBootApplication +@SuppressWarnings("unused") +public class BootGeodeMultiSiteCachingServerApplication { + + private static final boolean PERSISTENT = false; + + private static final int GATEWAY_RECEIVER_END_PORT = 29779; + private static final int GATEWAY_RECEIVER_START_PORT = 13339; + + private static final String CUSTOMERS_BY_NAME_REGION = "CustomersByName"; + private static final String GATEWAY_RECEIVER_HOSTNAME_FOR_SENDERS = "localhost"; + + public static void main(String[] args) { + + new SpringApplicationBuilder(BootGeodeMultiSiteCachingServerApplication.class) + .web(WebApplicationType.NONE) + .build() + .run(args); + } + + @CacheServerApplication(name = "BootGeodeMultiSiteCachingServerApplication", port = 0) + static class GeodeServerConfiguration { + + @Bean(CUSTOMERS_BY_NAME_REGION) + ReplicatedRegionFactoryBean customersByNameRegion(Cache cache, + @Autowired(required = false) List regionConfigurers) { + + ReplicatedRegionFactoryBean customersByName = new ReplicatedRegionFactoryBean<>(); + + customersByName.setCache(cache); + customersByName.setPersistent(PERSISTENT); + customersByName.setRegionConfigurers(regionConfigurers); + + return customersByName; + } + + @Bean + ApplicationRunner geodeClusterObjectsBootstrappedAssertionRunner(Environment environment, Cache cache, + Region customersByName, GatewayReceiver gatewayReceiver, GatewaySender gatewaySender) { + + return args -> { + + assertThat(cache).isNotNull(); + assertThat(cache.getName()).startsWith(BootGeodeMultiSiteCachingServerApplication.class.getSimpleName()); + assertThat(customersByName).isNotNull(); + assertThat(customersByName.getAttributes()).isNotNull(); + assertThat(customersByName.getAttributes().getDataPolicy()).isEqualTo(DataPolicy.REPLICATE); + assertThat(customersByName.getAttributes().getGatewaySenderIds()).containsExactly(gatewaySender.getId()); + assertThat(customersByName.getName()).isEqualTo(CUSTOMERS_BY_NAME_REGION); + assertThat(customersByName.getRegionService()).isEqualTo(cache); + assertThat(cache.getRegion(RegionUtils.toRegionPath(CUSTOMERS_BY_NAME_REGION))).isEqualTo(customersByName); + assertThat(gatewayReceiver).isNotNull(); + assertThat(gatewayReceiver.isRunning()).isTrue(); + assertThat(cache.getGatewayReceivers()).containsExactly(gatewayReceiver); + assertThat(gatewaySender).isNotNull(); + assertThat(gatewaySender.isRunning()).isTrue(); + assertThat(cache.getGatewaySenders().stream().map(GatewaySender::getId).collect(Collectors.toSet())) + .containsExactly(gatewaySender.getId()); + + System.err.printf("Apache Geode Cluster [%s] configured and bootstrapped successfully!%n", + environment.getProperty("spring.application.name", "UNKNOWN")); + }; + } + } + + @Configuration + @EnableLocator + @EnableManager(start = true) + @Profile("locator-manager") + static class GeodeLocatorManagerConfiguration { } + + @Configuration + @Profile("gateway-receiver") + static class GeodeGatewayReceiverConfiguration { + + @Bean + GatewayReceiverFactoryBean gatewayReceiver(Cache cache) { + + GatewayReceiverFactoryBean gatewayReceiver = new GatewayReceiverFactoryBean(cache); + + gatewayReceiver.setHostnameForSenders(GATEWAY_RECEIVER_HOSTNAME_FOR_SENDERS); + gatewayReceiver.setStartPort(GATEWAY_RECEIVER_START_PORT); + gatewayReceiver.setEndPort(GATEWAY_RECEIVER_END_PORT); + + return gatewayReceiver; + } + } + + @Configuration + @Profile("gateway-sender") + static class GeodeGatewaySenderConfiguration { + + @Bean + GatewaySenderFactoryBean customersByNameGatewaySender(Cache cache, + @Value("${geode.distributed-system.remote.id:1}") int remoteDistributedSystemId) { + + GatewaySenderFactoryBean gatewaySender = new GatewaySenderFactoryBean(cache); + + gatewaySender.setPersistent(PERSISTENT); + gatewaySender.setRemoteDistributedSystemId(remoteDistributedSystemId); + + return gatewaySender; + } + + @Bean + RegionConfigurer customersByNameConfigurer(GatewaySender gatewaySender) { + + return new RegionConfigurer() { + + @Override + public void configure(String beanName, PeerRegionFactoryBean regionBean) { + + if (CUSTOMERS_BY_NAME_REGION.equals(beanName)) { + regionBean.setGatewaySenders(ArrayUtils.asArray(gatewaySender)); + } + } + }; + } + } +} diff --git a/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site1.properties b/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site1.properties new file mode 100644 index 00000000..44b98cf9 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site1.properties @@ -0,0 +1,3 @@ +server.port=8080 +spring.application.name=BootGeodeMultiSiteCachingClientApplication-Site1 +spring.data.gemfire.pool.locators=localhost[11235] diff --git a/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site2.properties b/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site2.properties new file mode 100644 index 00000000..7c82c4c0 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/resources/application-client-site2.properties @@ -0,0 +1,3 @@ +server.port=9090 +spring.application.name=BootGeodeMultiSiteCachingClientApplication-Site2 +spring.data.gemfire.pool.locators=localhost[12480] diff --git a/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site1.properties b/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site1.properties new file mode 100644 index 00000000..129d9a5c --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site1.properties @@ -0,0 +1,7 @@ +gemfire.distributed-system-id=10 +gemfire.remote-locators=localhost[12480] +geode.distributed-system.remote.id=20 +spring.application.name=BootGeodeMultiSiteCachingServerApplication-Site1 +spring.profiles.include=locator-manager,gateway-receiver,gateway-sender +spring.data.gemfire.locator.port=11235 +spring.data.gemfire.manager.port=1199 diff --git a/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site2.properties b/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site2.properties new file mode 100644 index 00000000..0a984bee --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/main/resources/application-server-site2.properties @@ -0,0 +1,7 @@ +gemfire.distributed-system-id=20 +gemfire.remote-locators=localhost[11235] +geode.distributed-system.remote.id=10 +spring.application.name=BootGeodeMultiSiteCachingServerApplication-Site2 +spring.profiles.include=locator-manager,gateway-receiver,gateway-sender +spring.data.gemfire.locator.port=12480 +spring.data.gemfire.manager.port=2299 diff --git a/spring-geode-samples/caching/multi-site/src/test/java/example/app/caching/multisite/MultiSiteCachingIntegrationTests.java b/spring-geode-samples/caching/multi-site/src/test/java/example/app/caching/multisite/MultiSiteCachingIntegrationTests.java new file mode 100644 index 00000000..7ba88ab6 --- /dev/null +++ b/spring-geode-samples/caching/multi-site/src/test/java/example/app/caching/multisite/MultiSiteCachingIntegrationTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 example.app.caching.multisite; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; +import java.util.Properties; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport; +import org.springframework.data.gemfire.tests.process.ProcessWrapper; + +import example.app.caching.multisite.client.BootGeodeMultiSiteCachingClientApplication; +import example.app.caching.multisite.client.model.Customer; +import example.app.caching.multisite.client.service.CustomerService; +import example.app.caching.multisite.client.util.ThreadUtils; +import example.app.caching.multisite.server.BootGeodeMultiSiteCachingServerApplication; + +/** + * Integration Tests testing the Multi-Site (WAN) Caching Example. + * + * @author John Blum + * @see org.junit.Test + * @see java.util.Properties + * @see org.springframework.boot.SpringApplication + * @see org.springframework.boot.builder.SpringApplicationBuilder + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport + * @see org.springframework.data.gemfire.tests.process.ProcessWrapper + * @see example.app.caching.multisite.client.BootGeodeMultiSiteCachingClientApplication + * @see example.app.caching.multisite.client.model.Customer + * @see example.app.caching.multisite.client.service.CustomerService + * @see example.app.caching.multisite.server.BootGeodeMultiSiteCachingServerApplication + * @since 1.3.0 + */ +public class MultiSiteCachingIntegrationTests extends ForkingClientServerIntegrationTestsSupport { + + private static int locatorPortClusterOne; + private static int locatorPortClusterTwo; + + private static ProcessWrapper geodeClusterOne; + private static ProcessWrapper geodeClusterTwo; + + private static final String HOSTNAME = "localhost"; + + @BeforeClass + public static void startGeodeClusters() throws IOException { + + locatorPortClusterOne = findAndReserveAvailablePort(); + locatorPortClusterTwo = findAndReserveAvailablePort(); + + geodeClusterOne = run(BootGeodeMultiSiteCachingServerApplication.class, + "-Dspring.profiles.active=server-site1", + String.format("-Dspring.data.gemfire.locator.port=%d", locatorPortClusterOne), + "-Dspring.data.gemfire.manager.start=false", + String.format("-Dgemfire.remote-locators=%s[%d]", HOSTNAME, locatorPortClusterTwo)); + + waitForServerToStart("localhost", locatorPortClusterOne); + + geodeClusterTwo = run(BootGeodeMultiSiteCachingServerApplication.class, + "-Dspring.profiles.active=server-site2", + String.format("-Dspring.data.gemfire.locator.port=%d", locatorPortClusterTwo), + "-Dspring.data.gemfire.manager.start=false", + String.format("-Dgemfire.remote-locators=%s[%d]", HOSTNAME, locatorPortClusterOne)); + + waitForServerToStart("localhost", locatorPortClusterTwo); + } + + @AfterClass + public static void stopGeodeClusters() { + stop(geodeClusterOne); + stop(geodeClusterTwo); + } + + private ConfigurableApplicationContext newApplicationContext(int locatorPort, String... springProfiles) { + + Properties configuration = new Properties(); + + configuration.setProperty("spring.data.gemfire.pool.locators", + String.format("%s[%d]", HOSTNAME, locatorPort)); + + System.setProperty("spring.data.gemfire.pool.locators", + configuration.getProperty("spring.data.gemfire.pool.locators")); + + SpringApplication springApplication = + new SpringApplicationBuilder(BootGeodeMultiSiteCachingClientApplication.class) + .headless(true) + .profiles(springProfiles) + //.properties(configuration) + .registerShutdownHook(true) + .build(); + + return springApplication.run(); + } + + private void close(ConfigurableApplicationContext applicationContext) { + + Optional.ofNullable(applicationContext) + .ifPresent(it -> { + it.close(); + ThreadUtils.waitFor(Duration.ofSeconds(5), () -> !(it.isActive() || it.isRunning())); + }); + } + + @Test + public void clientOfClusterTwoResultsInCacheHit() { + + ConfigurableApplicationContext applicationContext = null; + + try { + + applicationContext = newApplicationContext(locatorPortClusterOne, "client-site1"); + + assertThat(applicationContext).isNotNull(); + assertThat(applicationContext.isActive()).isTrue(); + + CustomerService customerService = applicationContext.getBean(CustomerService.class); + + customerService.setSleepInSeconds(2L); + + Customer jonDoe = customerService.findBy("Jon Doe"); + + assertThat(jonDoe).isNotNull(); + assertThat(jonDoe.getName()).isEqualTo("Jon Doe"); + assertThat(customerService.isCacheMiss()).isTrue(); + assertThat(customerService.findBy(jonDoe.getName())).isEqualTo(jonDoe); + assertThat(customerService.isCacheMiss()).isFalse(); + + close(applicationContext); + + applicationContext = newApplicationContext(locatorPortClusterTwo, "client-site2"); + + assertThat(applicationContext).isNotNull(); + assertThat(applicationContext.isActive()).isTrue(); + + customerService = applicationContext.getBean(CustomerService.class); + customerService.setSleepInSeconds(2L); + + Customer cachedJonDoe = customerService.findBy(jonDoe.getName()); + + assertThat(cachedJonDoe).isNotNull(); + assertThat(cachedJonDoe).isNotSameAs(jonDoe); + assertThat(cachedJonDoe).isEqualTo(jonDoe); + assertThat(customerService.isCacheMiss()).isFalse(); + } + finally { + close(applicationContext); + } + } +}