Skip to content

Commit e8522b9

Browse files
authored
feat: add lazy initializer (#423)
* feat: add lazy initializer * chore: run linter
1 parent fd63673 commit e8522b9

File tree

3 files changed

+203
-0
lines changed

3 files changed

+203
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2020 Google LLC
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+
* http://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 or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner;
18+
19+
/**
20+
* Generic {@link AbstractLazyInitializer} for any heavy-weight object that might throw an exception
21+
* during initialization. The underlying object is initialized at most once.
22+
*/
23+
public abstract class AbstractLazyInitializer<T> {
24+
private final Object lock = new Object();
25+
private volatile boolean initialized;
26+
private volatile T object;
27+
private volatile Exception error;
28+
29+
/** Returns an initialized instance of T. */
30+
T get() throws Exception {
31+
// First check without a lock to improve performance.
32+
if (!initialized) {
33+
synchronized (lock) {
34+
if (!initialized) {
35+
try {
36+
object = initialize();
37+
} catch (Exception e) {
38+
error = e;
39+
}
40+
initialized = true;
41+
}
42+
}
43+
}
44+
if (error != null) {
45+
throw error;
46+
}
47+
return object;
48+
}
49+
50+
/**
51+
* Initializes the actual object that should be returned. Is called once the first time an
52+
* instance of T is required.
53+
*/
54+
public abstract T initialize() throws Exception;
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Copyright 2020 Google LLC
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+
* http://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 or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner;
18+
19+
/** Default implementation of {@link AbstractLazyInitializer} for a {@link Spanner} instance. */
20+
public class LazySpannerInitializer extends AbstractLazyInitializer<Spanner> {
21+
/**
22+
* Initializes a default {@link Spanner} instance. Override this method to create an instance with
23+
* custom configuration.
24+
*/
25+
@Override
26+
public Spanner initialize() throws Exception {
27+
return SpannerOptions.newBuilder().build().getService();
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2020 Google LLC
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+
* http://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 or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.fail;
21+
import static org.mockito.Mockito.mock;
22+
23+
import com.google.common.util.concurrent.Futures;
24+
import com.google.common.util.concurrent.ListenableFuture;
25+
import com.google.common.util.concurrent.ListeningExecutorService;
26+
import com.google.common.util.concurrent.MoreExecutors;
27+
import java.io.IOException;
28+
import java.util.ArrayList;
29+
import java.util.List;
30+
import java.util.concurrent.Callable;
31+
import java.util.concurrent.CountDownLatch;
32+
import java.util.concurrent.ExecutionException;
33+
import java.util.concurrent.Executors;
34+
import java.util.concurrent.TimeUnit;
35+
import java.util.concurrent.atomic.AtomicInteger;
36+
import org.junit.Test;
37+
import org.junit.runner.RunWith;
38+
import org.junit.runners.JUnit4;
39+
40+
@RunWith(JUnit4.class)
41+
public class LazySpannerInitializerTest {
42+
43+
@Test
44+
public void testGet_shouldReturnSameInstance() throws Throwable {
45+
final LazySpannerInitializer initializer =
46+
new LazySpannerInitializer() {
47+
@Override
48+
public Spanner initialize() {
49+
return mock(Spanner.class);
50+
}
51+
};
52+
Spanner s1 = initializer.get();
53+
Spanner s2 = initializer.get();
54+
assertThat(s1).isSameInstanceAs(s2);
55+
}
56+
57+
@Test
58+
public void testGet_shouldThrowErrorFromInitializeMethod() {
59+
final LazySpannerInitializer initializer =
60+
new LazySpannerInitializer() {
61+
@Override
62+
public Spanner initialize() throws IOException {
63+
throw new IOException("Could not find credentials file");
64+
}
65+
};
66+
Throwable t1 = null;
67+
try {
68+
initializer.get();
69+
fail("Missing expected exception");
70+
} catch (Throwable t) {
71+
t1 = t;
72+
}
73+
Throwable t2 = null;
74+
try {
75+
initializer.get();
76+
fail("Missing expected exception");
77+
} catch (Throwable t) {
78+
t2 = t;
79+
}
80+
assertThat(t1).isSameInstanceAs(t2);
81+
}
82+
83+
@Test
84+
public void testGet_shouldInvokeInitializeOnlyOnce()
85+
throws InterruptedException, ExecutionException {
86+
final AtomicInteger count = new AtomicInteger();
87+
final LazySpannerInitializer initializer =
88+
new LazySpannerInitializer() {
89+
@Override
90+
public Spanner initialize() {
91+
count.incrementAndGet();
92+
return mock(Spanner.class);
93+
}
94+
};
95+
final int threads = 16;
96+
final CountDownLatch latch = new CountDownLatch(threads);
97+
ListeningExecutorService executor =
98+
MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threads));
99+
List<ListenableFuture<Spanner>> futures = new ArrayList<>(threads);
100+
for (int i = 0; i < threads; i++) {
101+
futures.add(
102+
executor.submit(
103+
new Callable<Spanner>() {
104+
@Override
105+
public Spanner call() throws Exception {
106+
latch.countDown();
107+
latch.await(10L, TimeUnit.SECONDS);
108+
return initializer.get();
109+
}
110+
}));
111+
}
112+
assertThat(Futures.allAsList(futures).get()).hasSize(threads);
113+
for (int i = 0; i < threads - 1; i++) {
114+
assertThat(futures.get(i).get()).isSameInstanceAs(futures.get(i + 1).get());
115+
}
116+
assertThat(count.get()).isEqualTo(1);
117+
executor.shutdown();
118+
}
119+
}

0 commit comments

Comments
 (0)