Skip to content

Commit 288b6bb

Browse files
committed
Fix bug in cache data export caused by Apache Geode's JVM Shutdown Hook.
Resolves gh-88.
1 parent b07ca31 commit 288b6bb

File tree

4 files changed

+301
-5
lines changed

4 files changed

+301
-5
lines changed

spring-geode-autoconfigure/src/main/java/org/springframework/geode/boot/autoconfigure/DataImportExportAutoConfiguration.java

+64-5
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,32 @@
1616
package org.springframework.geode.boot.autoconfigure;
1717

1818
import java.util.Optional;
19+
import java.util.function.Predicate;
1920

2021
import org.apache.geode.cache.GemFireCache;
2122

23+
import org.springframework.boot.SpringApplication;
2224
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2325
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
2426
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2527
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2628
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
29+
import org.springframework.boot.env.EnvironmentPostProcessor;
2730
import org.springframework.context.annotation.Bean;
2831
import org.springframework.context.annotation.Condition;
2932
import org.springframework.context.annotation.ConditionContext;
3033
import org.springframework.context.annotation.Conditional;
3134
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.core.env.ConfigurableEnvironment;
3236
import org.springframework.core.env.Environment;
3337
import org.springframework.core.type.AnnotatedTypeMetadata;
3438
import org.springframework.data.gemfire.CacheFactoryBean;
3539
import org.springframework.geode.boot.autoconfigure.support.PdxInstanceWrapperRegionAspect;
3640
import org.springframework.geode.cache.SimpleCacheResolver;
41+
import org.springframework.geode.data.AbstractCacheDataImporterExporter;
3742
import org.springframework.geode.data.json.JsonCacheDataImporterExporter;
3843
import org.springframework.lang.NonNull;
44+
import org.springframework.lang.Nullable;
3945

4046
/**
4147
* Spring Boot {@link EnableAutoConfiguration auto-configuration} for cache data import/export.
@@ -49,12 +55,13 @@
4955
* @see org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
5056
* @see org.springframework.context.annotation.Bean
5157
* @see org.springframework.context.annotation.Condition
58+
* @see org.springframework.context.annotation.ConditionContext
5259
* @see org.springframework.context.annotation.Conditional
5360
* @see org.springframework.context.annotation.Configuration
5461
* @see org.springframework.core.env.Environment
5562
* @see org.springframework.core.type.AnnotatedTypeMetadata
5663
* @see org.springframework.data.gemfire.CacheFactoryBean
57-
* @see PdxInstanceWrapperRegionAspect
64+
* @see org.springframework.geode.boot.autoconfigure.support.PdxInstanceWrapperRegionAspect
5865
* @see org.springframework.geode.cache.SimpleCacheResolver
5966
* @see org.springframework.geode.data.json.JsonCacheDataImporterExporter
6067
* @since 1.3.0
@@ -65,6 +72,7 @@
6572
@SuppressWarnings("unused")
6673
public class DataImportExportAutoConfiguration {
6774

75+
protected static final String GEMFIRE_DISABLE_SHUTDOWN_HOOK = "gemfire.disableShutdownHook";
6876
protected static final String PDX_READ_SERIALIZED_PROPERTY = "spring.data.gemfire.pdx.read-serialized";
6977
protected static final String REGION_ADVICE_ENABLED_PROPERTY =
7078
"spring.boot.data.gemfire.cache.region.advice.enabled";
@@ -90,29 +98,80 @@ static class RegionAdviceConditions extends AnyNestedCondition {
9098
static class AdviseRegionOnRegionAdviceEnabledProperty { }
9199

92100
@Conditional(PdxReadSerializedCondition.class)
93-
static class AdviceRegionOnPdxReadSerializedCondition { }
101+
static class AdviseRegionOnPdxReadSerializedCondition { }
94102

95103
}
96104

97105
static class PdxReadSerializedCondition implements Condition {
98106

99107
@Override
100108
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
101-
return isPdxReadSerializedTrue(context.getEnvironment()) || isCachePdxReadSerializedTrue();
109+
return isPdxReadSerializedEnabled(context.getEnvironment()) || isCachePdxReadSerializedEnabled();
102110
}
103111

104-
private boolean isCachePdxReadSerializedTrue() {
112+
private boolean isCachePdxReadSerializedEnabled() {
105113

106114
return SimpleCacheResolver.getInstance().resolve()
107115
.filter(GemFireCache::getPdxReadSerialized)
108116
.isPresent();
109117
}
110118

111-
private boolean isPdxReadSerializedTrue(@NonNull Environment environment) {
119+
private boolean isPdxReadSerializedEnabled(@NonNull Environment environment) {
112120

113121
return Optional.ofNullable(environment)
114122
.filter(env -> env.getProperty(PDX_READ_SERIALIZED_PROPERTY, Boolean.class, false))
115123
.isPresent();
116124
}
117125
}
126+
127+
private static final boolean DEFAULT_EXPORT_ENABLED = false;
128+
129+
private static final Predicate<Environment> disableGemFireShutdownHookPredicate = environment ->
130+
Optional.ofNullable(environment)
131+
.filter(env -> env.getProperty(CacheDataImporterExporterReference.EXPORT_ENABLED_PROPERTY_NAME,
132+
Boolean.class, DEFAULT_EXPORT_ENABLED))
133+
.isPresent();
134+
135+
static abstract class AbstractDisableGemFireShutdownHookSupport {
136+
137+
boolean shouldDisableGemFireShutdownHook(@Nullable Environment environment) {
138+
return disableGemFireShutdownHookPredicate.test(environment);
139+
}
140+
141+
/**
142+
* If we do not disable GemFire/Geode's {@link org.apache.geode.distributed.DistributedSystem} JRE/JVM runtime
143+
* shutdown hook then the {@link org.apache.geode.cache.Region} is prematurely closed by the JRE/JVM shutdown hook
144+
* before Spring's {@link org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor}s can do their
145+
* work of exporting data from the {@link org.apache.geode.cache.Region} as JSON.
146+
*/
147+
void disableGemFireShutdownHook(@Nullable Environment environment) {
148+
System.setProperty(GEMFIRE_DISABLE_SHUTDOWN_HOOK, Boolean.TRUE.toString());
149+
}
150+
}
151+
152+
static abstract class CacheDataImporterExporterReference extends AbstractCacheDataImporterExporter {
153+
static final String EXPORT_ENABLED_PROPERTY_NAME =
154+
AbstractCacheDataImporterExporter.CACHE_DATA_EXPORT_ENABLED_PROPERTY_NAME;
155+
}
156+
157+
static class DisableGemFireShutdownHookCondition extends AbstractDisableGemFireShutdownHookSupport
158+
implements Condition {
159+
160+
@Override
161+
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
162+
return shouldDisableGemFireShutdownHook(context.getEnvironment());
163+
}
164+
}
165+
166+
public static class DisableGemFireShutdownHookEnvironmentPostProcessor
167+
extends AbstractDisableGemFireShutdownHookSupport implements EnvironmentPostProcessor {
168+
169+
@Override
170+
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
171+
172+
if (shouldDisableGemFireShutdownHook(environment)) {
173+
disableGemFireShutdownHook(environment);
174+
}
175+
}
176+
}
118177
}

spring-geode-autoconfigure/src/main/resources/META-INF/spring.factories

+1
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ org.springframework.geode.boot.autoconfigure.SslAutoConfiguration
2121
# Environment Post Processing
2222
org.springframework.boot.env.EnvironmentPostProcessor=\
2323
org.springframework.geode.boot.autoconfigure.ClientSecurityAutoConfiguration.AutoConfiguredCloudSecurityEnvironmentPostProcessor,\
24+
org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration.DisableGemFireShutdownHookEnvironmentPostProcessor,\
2425
org.springframework.geode.boot.autoconfigure.SpringSessionAutoConfiguration.SpringSessionPropertiesEnvironmentPostProcessor,\
2526
org.springframework.geode.boot.autoconfigure.SslAutoConfiguration.SslEnvironmentPostProcessor
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13+
* or implied. See the License for the specific language governing
14+
* permissions and limitations under the License.
15+
*/
16+
package example.app.golf.model;
17+
18+
import org.springframework.data.annotation.Id;
19+
import org.springframework.data.gemfire.mapping.annotation.Region;
20+
21+
import lombok.AccessLevel;
22+
import lombok.EqualsAndHashCode;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
import lombok.NonNull;
26+
import lombok.RequiredArgsConstructor;
27+
28+
/**
29+
* An Abstract Data Type (ADT) that models a person who golfs.
30+
*
31+
* @author John Blum
32+
* @since 1.3.0
33+
*/
34+
@Region("Golfers")
35+
@Getter
36+
@EqualsAndHashCode(of = "name")
37+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
38+
@RequiredArgsConstructor(staticName = "newGolfer")
39+
public class Golfer {
40+
41+
@Id @NonNull
42+
private Long id;
43+
44+
@NonNull
45+
private String name;
46+
47+
private Integer handicap;
48+
49+
public Golfer withHandicap(int handicap) {
50+
this.handicap = handicap;
51+
return this;
52+
}
53+
54+
@Override
55+
public String toString() {
56+
return String.format("%s:%d", getName(), getHandicap());
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
13+
* or implied. See the License for the specific language governing
14+
* permissions and limitations under the License.
15+
*/
16+
package org.springframework.geode.boot.autoconfigure.data;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.File;
21+
import java.io.IOException;
22+
import java.time.Duration;
23+
import java.util.HashSet;
24+
import java.util.Set;
25+
import java.util.stream.StreamSupport;
26+
27+
import com.fasterxml.jackson.databind.DeserializationFeature;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
30+
import org.junit.AfterClass;
31+
import org.junit.BeforeClass;
32+
import org.junit.Test;
33+
34+
import org.apache.geode.cache.Region;
35+
36+
import org.springframework.boot.ApplicationRunner;
37+
import org.springframework.boot.WebApplicationType;
38+
import org.springframework.boot.autoconfigure.SpringBootApplication;
39+
import org.springframework.boot.builder.SpringApplicationBuilder;
40+
import org.springframework.context.annotation.Bean;
41+
import org.springframework.context.annotation.Profile;
42+
import org.springframework.data.gemfire.GemfireTemplate;
43+
import org.springframework.data.gemfire.config.annotation.EnableEntityDefinedRegions;
44+
import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport;
45+
import org.springframework.data.gemfire.tests.process.ProcessWrapper;
46+
import org.springframework.data.gemfire.tests.util.FileSystemUtils;
47+
import org.springframework.data.gemfire.tests.util.FileUtils;
48+
import org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration;
49+
import org.springframework.geode.config.annotation.EnableClusterAware;
50+
51+
import example.app.golf.model.Golfer;
52+
53+
/**
54+
* Integration Tests for {@link DataImportExportAutoConfiguration}, which specifically tests the export of
55+
* {@link Region} values (data) to JSON on Spring Boot application (JVM) shutdown.
56+
*
57+
* @author John Blum
58+
* @see org.junit.Test
59+
* @see org.apache.geode.cache.Region
60+
* @see org.springframework.boot.SpringApplication
61+
* @see org.springframework.boot.autoconfigure.SpringBootApplication
62+
* @see org.springframework.context.annotation.Profile
63+
* @see org.springframework.geode.boot.autoconfigure.DataImportExportAutoConfiguration
64+
* @since 1.3.0
65+
*/
66+
public class CacheDataExportAutoConfigurationIntegrationTests extends ForkingClientServerIntegrationTestsSupport {
67+
68+
private static final File GEODE_WORKING_DIRECTORY =
69+
new File(String.format("cache-data-export-%d", System.currentTimeMillis()));
70+
71+
private static ProcessWrapper process;
72+
73+
private static final String DATA_GOLFERS_JSON = "data-golfers.json";
74+
75+
@BeforeClass
76+
public static void runGeodeProcess() throws IOException {
77+
78+
System.setProperty(DIRECTORY_DELETE_ON_EXIT_PROPERTY, Boolean.FALSE.toString());
79+
80+
process = run(GEODE_WORKING_DIRECTORY, TestGeodeConfiguration.class,
81+
"-Dspring.profiles.active=EXPORT", "-Dspring.boot.data.gemfire.cache.data.export.enabled=true");
82+
83+
assertThat(process).isNotNull();
84+
85+
waitOn(() -> !process.isRunning(), Duration.ofSeconds(20).toMillis(), Duration.ofSeconds(2).toMillis());
86+
}
87+
88+
@AfterClass
89+
public static void cleanup() {
90+
System.clearProperty(DIRECTORY_DELETE_ON_EXIT_PROPERTY);
91+
FileSystemUtils.deleteRecursive(GEODE_WORKING_DIRECTORY);
92+
stop(process);
93+
}
94+
95+
@Test
96+
public void exportedJsonIsCorrect() throws Exception {
97+
98+
File dataGolferJson = new File(GEODE_WORKING_DIRECTORY, DATA_GOLFERS_JSON);
99+
100+
assertThat(dataGolferJson).isFile();
101+
102+
String actualJson = FileUtils.read(dataGolferJson);
103+
104+
/*
105+
String expectedJson = "["
106+
+ "{\"@type\":\"example.app.golf.model.Golfer\",\"handicap\":9,\"id\":1,\"name\":\"John Blum\"},"
107+
+ "{\"@type\":\"example.app.golf.model.Golfer\",\"handicap\":10,\"id\":2,\"name\":\"Moe Haroon\"}"
108+
+ "]";
109+
110+
assertThat(actualJson).isEqualTo(expectedJson);
111+
*/
112+
113+
Set<Golfer> expectedGolfers = mapFromJsonToGolfers(actualJson);
114+
115+
assertThat(expectedGolfers).isNotNull();
116+
assertThat(expectedGolfers).hasSize(2);
117+
assertContains(expectedGolfers, Golfer.newGolfer(1L, "John Blum").withHandicap(9));
118+
assertContains(expectedGolfers, Golfer.newGolfer(2L, "Moe Haroon").withHandicap(10));
119+
}
120+
121+
private void assertContains(Iterable<Golfer> golfers, Golfer golfer) {
122+
123+
assertThat(StreamSupport.stream(golfers.spliterator(), false)
124+
.anyMatch(it -> it.getId().equals(golfer.getId())
125+
&& it.getName().equals(golfer.getName())
126+
&& it.getHandicap().equals(golfer.getHandicap())))
127+
.isTrue();
128+
}
129+
130+
private Set<Golfer> mapFromJsonToGolfers(String json) throws Exception {
131+
return new HashSet<>(newObjectMapper().readerForListOf(Golfer.class).readValue(json));
132+
}
133+
134+
private ObjectMapper newObjectMapper() {
135+
136+
return new ObjectMapper()
137+
.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false)
138+
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
139+
}
140+
141+
@Profile("EXPORT")
142+
@SpringBootApplication
143+
@EnableClusterAware
144+
@EnableEntityDefinedRegions(basePackageClasses = Golfer.class)
145+
@SuppressWarnings("unused")
146+
static class TestGeodeConfiguration {
147+
148+
public static void main(String[] args) {
149+
150+
new SpringApplicationBuilder(TestGeodeConfiguration.class)
151+
.web(WebApplicationType.NONE)
152+
.build()
153+
.run(args);
154+
}
155+
156+
private static void log(String message, Object... args) {
157+
System.out.printf(String.format("%s%n", message), args);
158+
System.out.flush();
159+
}
160+
161+
@Bean
162+
ApplicationRunner runner(GemfireTemplate golfersTemplate) {
163+
164+
return args -> {
165+
166+
save(golfersTemplate, Golfer.newGolfer(1L, "John Blum").withHandicap(9));
167+
save(golfersTemplate, Golfer.newGolfer(2L, "Moe Haroon").withHandicap(10));
168+
169+
log("FORE!");
170+
};
171+
}
172+
173+
private Golfer save(GemfireTemplate golfersTemplate, Golfer golfer) {
174+
golfersTemplate.put(golfer.getId(), golfer);
175+
return golfer;
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)