From e8522b9955c4a19fa7d6297fd463e9d2521dff92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 15 Sep 2020 16:07:15 +0200 Subject: [PATCH] feat: add lazy initializer (#423) * feat: add lazy initializer * chore: run linter --- .../spanner/AbstractLazyInitializer.java | 55 ++++++++ .../cloud/spanner/LazySpannerInitializer.java | 29 +++++ .../spanner/LazySpannerInitializerTest.java | 119 ++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java create mode 100644 google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java create mode 100644 google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java new file mode 100644 index 0000000000..c78a994c16 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.cloud.spanner; + +/** + * Generic {@link AbstractLazyInitializer} for any heavy-weight object that might throw an exception + * during initialization. The underlying object is initialized at most once. + */ +public abstract class AbstractLazyInitializer { + private final Object lock = new Object(); + private volatile boolean initialized; + private volatile T object; + private volatile Exception error; + + /** Returns an initialized instance of T. */ + T get() throws Exception { + // First check without a lock to improve performance. + if (!initialized) { + synchronized (lock) { + if (!initialized) { + try { + object = initialize(); + } catch (Exception e) { + error = e; + } + initialized = true; + } + } + } + if (error != null) { + throw error; + } + return object; + } + + /** + * Initializes the actual object that should be returned. Is called once the first time an + * instance of T is required. + */ + public abstract T initialize() throws Exception; +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java new file mode 100644 index 0000000000..a157a5ac3f --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LazySpannerInitializer.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.cloud.spanner; + +/** Default implementation of {@link AbstractLazyInitializer} for a {@link Spanner} instance. */ +public class LazySpannerInitializer extends AbstractLazyInitializer { + /** + * Initializes a default {@link Spanner} instance. Override this method to create an instance with + * custom configuration. + */ + @Override + public Spanner initialize() throws Exception { + return SpannerOptions.newBuilder().build().getService(); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java new file mode 100644 index 0000000000..907b465085 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Google LLC + * + * 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.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class LazySpannerInitializerTest { + + @Test + public void testGet_shouldReturnSameInstance() throws Throwable { + final LazySpannerInitializer initializer = + new LazySpannerInitializer() { + @Override + public Spanner initialize() { + return mock(Spanner.class); + } + }; + Spanner s1 = initializer.get(); + Spanner s2 = initializer.get(); + assertThat(s1).isSameInstanceAs(s2); + } + + @Test + public void testGet_shouldThrowErrorFromInitializeMethod() { + final LazySpannerInitializer initializer = + new LazySpannerInitializer() { + @Override + public Spanner initialize() throws IOException { + throw new IOException("Could not find credentials file"); + } + }; + Throwable t1 = null; + try { + initializer.get(); + fail("Missing expected exception"); + } catch (Throwable t) { + t1 = t; + } + Throwable t2 = null; + try { + initializer.get(); + fail("Missing expected exception"); + } catch (Throwable t) { + t2 = t; + } + assertThat(t1).isSameInstanceAs(t2); + } + + @Test + public void testGet_shouldInvokeInitializeOnlyOnce() + throws InterruptedException, ExecutionException { + final AtomicInteger count = new AtomicInteger(); + final LazySpannerInitializer initializer = + new LazySpannerInitializer() { + @Override + public Spanner initialize() { + count.incrementAndGet(); + return mock(Spanner.class); + } + }; + final int threads = 16; + final CountDownLatch latch = new CountDownLatch(threads); + ListeningExecutorService executor = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threads)); + List> futures = new ArrayList<>(threads); + for (int i = 0; i < threads; i++) { + futures.add( + executor.submit( + new Callable() { + @Override + public Spanner call() throws Exception { + latch.countDown(); + latch.await(10L, TimeUnit.SECONDS); + return initializer.get(); + } + })); + } + assertThat(Futures.allAsList(futures).get()).hasSize(threads); + for (int i = 0; i < threads - 1; i++) { + assertThat(futures.get(i).get()).isSameInstanceAs(futures.get(i + 1).get()); + } + assertThat(count.get()).isEqualTo(1); + executor.shutdown(); + } +}