Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for lazy values and changed common modules to use it. #1228

Merged
merged 1 commit into from
Dec 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions common/common/src/main/java/io/helidon/common/LazyValue.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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 io.helidon.common;

import java.util.function.Supplier;

/**
* A typed supplier that wraps another supplier and only retrieves the value on the first
* request to {@link #get()}, caching the value for all subsequent invocations.
* <p>
* <b>Helidon implementations obtained through {@link #create(java.util.function.Supplier)}
* and {@link #create(Object)} are guaranteed to be thread safe.</b>
*
* @param <T> type of the provided object
*/
@FunctionalInterface
public interface LazyValue<T> extends Supplier<T> {
/**
* Create a lazy value from a supplier.
* @param supplier supplier to get the value from
* @param <T> type of the value
* @return a lazy value that will obtain the value from supplier on first call to {@link #get()}
*/
static <T> LazyValue<T> create(Supplier<T> supplier) {
return new LazyValueImpl<>(supplier);
}

/**
* Create a lazy value from a value.
*
* @param value actual value to return
* @param <T> type of the value
* @return a lazy value that will always return the value provided
*/
static <T> LazyValue<T> create(T value) {
return () -> value;
}

}
57 changes: 57 additions & 0 deletions common/common/src/main/java/io/helidon/common/LazyValueImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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 io.helidon.common;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;

class LazyValueImpl<T> implements LazyValue<T> {
private final Lock theLock = new ReentrantLock();

private volatile T value;

private Supplier<T> delegate;
private volatile boolean loaded;

LazyValueImpl(Supplier<T> supplier) {
this.delegate = supplier;
}

@Override
public T get() {
if (loaded) {
return value;
}

// not loaded (probably)
theLock.lock();

try {
if (loaded) {
return value;
}
value = delegate.get();
loaded = true;
delegate = null;
} finally {
theLock.unlock();
}

return value;
}
}
106 changes: 106 additions & 0 deletions common/common/src/test/java/io/helidon/common/LazyValueTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* 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 io.helidon.common;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

import org.junit.jupiter.api.Test;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;

/**
* Unit test for {@link LazyValue}.
*/
class LazyValueTest {

@Test
void testValue() {
String text = "Helidon";
LazyValue<String> value = LazyValue.create(text);

String s = value.get();
assertThat(s, is(text));
}

@Test
void testSupplier() {
String text = "Helidon";
AtomicInteger called = new AtomicInteger();

LazyValue<String> value = LazyValue.create(() -> {
called.incrementAndGet();
return text;
});

String s = value.get();
assertThat(s, is(text));
assertThat(called.get(), is(1));

s = value.get();
assertThat(s, is(text));
assertThat(called.get(), is(1));
}

@Test
void testSupplierParallel() {
String text = "Helidon";
AtomicInteger called = new AtomicInteger();
AtomicInteger threadsStarted = new AtomicInteger();

LazyValue<String> value = LazyValue.create(() -> {
called.incrementAndGet();
return text;
});

Errors.Collector errors = Errors.collector();

int threadCount = 20;
CyclicBarrier barrier = new CyclicBarrier(threadCount);
Thread[] testingThreads = new Thread[threadCount];
for (int i = 0; i < testingThreads.length; i++) {
testingThreads[i] = new Thread(() -> {
threadsStarted.incrementAndGet();
try {
barrier.await();
} catch (Exception e) {
errors.fatal("Failed to start, barrier failed: " + e.getMessage());
}
String s = value.get();
if (!text.equals(s)) {
errors.fatal("Got wrong value. Expected " + text + ", but got: " + s);
}
});
}

for (Thread testingThread : testingThreads) {
testingThread.start();
}

for (Thread testingThread : testingThreads) {
try {
testingThread.join();
} catch (InterruptedException ignored) {
}
}

errors.collect().checkValid();
assertThat(called.get(), is(1));
assertThat(threadsStarted.get(), is(threadCount));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import io.helidon.common.LazyValue;
import io.helidon.common.context.Contexts;
import io.helidon.config.Config;

Expand All @@ -41,7 +42,7 @@ public final class ScheduledThreadPoolSupplier implements Supplier<ExecutorServi
private final boolean isDaemon;
private final String threadNamePrefix;
private final boolean prestart;
private volatile ScheduledExecutorService instance;
private final LazyValue<ScheduledExecutorService> lazyValue = LazyValue.create(() -> Contexts.wrap(getThreadPool()));

private ScheduledThreadPoolSupplier(Builder builder) {
this.corePoolSize = builder.corePoolSize;
Expand Down Expand Up @@ -82,30 +83,27 @@ public static ScheduledThreadPoolSupplier create() {
ScheduledThreadPoolExecutor getThreadPool() {
ScheduledThreadPoolExecutor result;
result = new ScheduledThreadPoolExecutor(corePoolSize,
new ThreadFactory() {
private final AtomicInteger value = new AtomicInteger();

@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(null,
r,
threadNamePrefix + value.incrementAndGet());
t.setDaemon(isDaemon);
return t;
}
});
new ThreadFactory() {
private final AtomicInteger value = new AtomicInteger();

@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(null,
r,
threadNamePrefix + value.incrementAndGet());
t.setDaemon(isDaemon);
return t;
}
});
if (prestart) {
result.prestartAllCoreThreads();
}
return result;
}

@Override
public synchronized ScheduledExecutorService get() {
if (null == instance) {
instance = Contexts.wrap(getThreadPool());
}
return instance;
public ScheduledExecutorService get() {
return lazyValue.get();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.function.Supplier;
import java.util.logging.Logger;

import io.helidon.common.LazyValue;
import io.helidon.common.context.Contexts;
import io.helidon.config.Config;

Expand Down Expand Up @@ -56,7 +57,7 @@ public final class ThreadPoolSupplier implements Supplier<ExecutorService> {
private final int growthThreshold;
private final int growthRate;
private final ThreadPool.RejectionHandler rejectionHandler;
private volatile ExecutorService instance;
private final LazyValue<ExecutorService> lazyValue = LazyValue.create(() -> Contexts.wrap(getThreadPool()));

private ThreadPoolSupplier(Builder builder) {
this.corePoolSize = builder.corePoolSize;
Expand Down Expand Up @@ -89,7 +90,7 @@ public static Builder builder() {
*/
public static ThreadPoolSupplier create(Config config) {
return builder().config(config)
.build();
.build();
}

/**
Expand Down Expand Up @@ -120,11 +121,8 @@ ThreadPool getThreadPool() {
}

@Override
public synchronized ExecutorService get() {
if (null == instance) {
instance = Contexts.wrap(getThreadPool());
}
return instance;
public ExecutorService get() {
return lazyValue.get();
}

/**
Expand Down Expand Up @@ -357,7 +355,8 @@ public Builder prestart(boolean prestart) {
* <li>there are no idle threads, and</li>
* <li>the number of tasks in the queue exceeds the {@code growthThreshold}</li>
* </ul>
* <p>For example, a rate of 20 means that while these conditions are met one thread will be added for every 5 submitted
* <p>For example, a rate of 20 means that while these conditions are met one thread will be added for every 5
* submitted
* tasks.
* <p>A rate of 0 selects the default {@link ThreadPoolExecutor} growth behavior: a thread is added only when a
* submitted task is rejected because the queue is full.</td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Supplier;

import io.helidon.common.LazyValue;

/**
* A {@link Context} implementation with deque registry.
*/
Expand Down Expand Up @@ -140,17 +142,15 @@ private void registerItem(RegisteredItem<?> item) {
}
}

@SuppressWarnings("unchecked")
<T> void register(T instance) {
Objects.requireNonNull(instance, "Parameter 'instance' is null!");
registerItem(new RegisteredInstance(instance));
registerItem(new RegisteredInstance<>(instance));
}

@SuppressWarnings("unchecked")
<T> void supply(Class<T> type, Supplier<T> supplier) {
Objects.requireNonNull(type, "Parameter 'type' is null!");
Objects.requireNonNull(supplier, "Parameter 'supplier' is null!");
registerItem(new RegisteredSupplier(type, supplier));
registerItem(new RegisteredSupplier<>(type, supplier));
}

<T> T get(Class<T> type) {
Expand All @@ -173,26 +173,16 @@ <T> T get(Class<T> type) {

private static class RegisteredSupplier<T> implements RegisteredItem<T> {
private final Class<T> type;
private final Supplier<T> supplier;
private volatile boolean missing = true;
private volatile T instance;
private final LazyValue<T> value;

RegisteredSupplier(Class<T> type, Supplier<T> supplier) {
this.type = type;
this.supplier = supplier;
this.value = LazyValue.create(supplier);
}

@Override
public T get() {
if (missing) {
synchronized (this) {
if (missing) {
missing = false;
instance = supplier.get();
}
}
}
return instance;
return value.get();
}

@Override
Expand Down