Skip to content

Commit

Permalink
ThreadFactoryBuilder replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
gthea committed Dec 11, 2023
1 parent afee180 commit 6fac773
Show file tree
Hide file tree
Showing 6 changed files with 340 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import androidx.annotation.NonNull;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.split.android.client.service.executor.ThreadFactoryBuilder;

import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import androidx.annotation.NonNull;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import io.split.android.engine.scheduler.PausableScheduledThreadPoolExecutor;
import io.split.android.engine.scheduler.PausableScheduledThreadPoolExecutorImpl;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import androidx.annotation.NonNull;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import io.split.android.engine.scheduler.PausableScheduledThreadPoolExecutor;
import io.split.android.engine.scheduler.PausableScheduledThreadPoolExecutorImpl;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package io.split.android.client.service.executor;

import static java.util.Objects.requireNonNull;

import static io.split.android.client.utils.Utils.checkArgument;
import static io.split.android.client.utils.Utils.checkNotNull;

import androidx.annotation.Nullable;

import java.util.Locale;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicLong;

public final class ThreadFactoryBuilder {

@Nullable
private String nameFormat = null;
@Nullable
private Boolean daemon = null;
@Nullable
private Integer priority = null;
@Nullable
private Thread.UncaughtExceptionHandler uncaughtExceptionHandler = null;
@Nullable
private ThreadFactory backingThreadFactory = null;

/**
* Creates a new {@link ThreadFactory} builder.
*/
public ThreadFactoryBuilder() {
}

/**
* Sets the naming format to use when naming threads ({@link Thread#setName}) which are created
* with this ThreadFactory.
*
* @param nameFormat a {@link String#format(String, Object...)}-compatible format String, to which
* a unique integer (0, 1, etc.) will be supplied as the single parameter. This integer will
* be unique to the built instance of the ThreadFactory and will be assigned sequentially. For
* example, {@code "rpc-pool-%d"} will generate thread names like {@code "rpc-pool-0"}, {@code
* "rpc-pool-1"}, {@code "rpc-pool-2"}, etc.
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setNameFormat(String nameFormat) {
String unused = format(nameFormat, 0); // fail fast if the format is bad or null
this.nameFormat = nameFormat;
return this;
}

/**
* Sets daemon or not for new threads created with this ThreadFactory.
*
* @param daemon whether or not new Threads created with this ThreadFactory will be daemon threads
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setDaemon(boolean daemon) {
this.daemon = daemon;
return this;
}

/**
* Sets the priority for new threads created with this ThreadFactory.
*
* <p><b>Warning:</b> relying on the thread scheduler is <a
* href="http://errorprone.info/bugpattern/ThreadPriorityCheck">discouraged</a>.
*
* @param priority the priority for new Threads created with this ThreadFactory
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setPriority(int priority) {
// Thread#setPriority() already checks for validity. These error messages
// are nicer though and will fail-fast.
checkArgument(priority >= Thread.MIN_PRIORITY);
checkArgument(priority <= Thread.MAX_PRIORITY);
this.priority = priority;
return this;
}

/**
* Sets the {@link Thread.UncaughtExceptionHandler} for new threads created with this ThreadFactory.
*
* @param uncaughtExceptionHandler the uncaught exception handler for new Threads created with
* this ThreadFactory
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setUncaughtExceptionHandler(
Thread.UncaughtExceptionHandler uncaughtExceptionHandler) {
this.uncaughtExceptionHandler = checkNotNull(uncaughtExceptionHandler);
return this;
}

/**
* Sets the backing {@link ThreadFactory} for new threads created with this ThreadFactory. Threads
* will be created by invoking #newThread(Runnable) on this backing {@link ThreadFactory}.
*
* @param backingThreadFactory the backing {@link ThreadFactory} which will be delegated to during
* thread creation.
* @return this for the builder pattern
*/
public ThreadFactoryBuilder setThreadFactory(ThreadFactory backingThreadFactory) {
this.backingThreadFactory = checkNotNull(backingThreadFactory);
return this;
}

/**
* Returns a new thread factory using the options supplied during the building process. After
* building, it is still possible to change the options used to build the ThreadFactory and/or
* build again. State is not shared amongst built instances.
*
* @return the fully constructed {@link ThreadFactory}
*/
public ThreadFactory build() {
return doBuild(this);
}

// Split out so that the anonymous ThreadFactory can't contain a reference back to the builder.
// At least, I assume that's why. TODO(cpovirk): Check, and maybe add a test for this.
private static ThreadFactory doBuild(ThreadFactoryBuilder builder) {
String nameFormat = builder.nameFormat;
Boolean daemon = builder.daemon;
Integer priority = builder.priority;
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = builder.uncaughtExceptionHandler;
ThreadFactory backingThreadFactory =
(builder.backingThreadFactory != null)
? builder.backingThreadFactory
: Executors.defaultThreadFactory();
AtomicLong count = (nameFormat != null) ? new AtomicLong(0) : null;
return new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = backingThreadFactory.newThread(runnable);
// TODO(b/139735208): Figure out what to do when the factory returns null.
requireNonNull(thread);
if (nameFormat != null) {
// requireNonNull is safe because we create `count` if (and only if) we have a nameFormat.
thread.setName(format(nameFormat, requireNonNull(count).getAndIncrement()));
}
if (daemon != null) {
thread.setDaemon(daemon);
}
if (priority != null) {
thread.setPriority(priority);
}
if (uncaughtExceptionHandler != null) {
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
}
return thread;
}
};
}

private static String format(String format, Object... args) {
return String.format(Locale.ROOT, format, args);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
package io.split.android.client.service.sseclient.sseclient;

import static com.google.common.base.Preconditions.checkNotNull;
import static io.split.android.client.utils.Utils.checkNotNull;
import static java.lang.Thread.sleep;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import io.split.android.client.service.executor.ThreadFactoryBuilder;

import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import io.split.android.client.service.ServiceConstants;
import io.split.android.client.service.executor.SplitSingleThreadTaskExecutor;
import io.split.android.client.service.executor.SplitTask;
import io.split.android.client.service.executor.SplitTaskExecutionInfo;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package io.split.android.client.service.executor;

import static org.junit.Assert.*;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import org.junit.Before;
import org.junit.Test;

public class ThreadFactoryBuilderTest {
private final Runnable monitoredRunnable = new Runnable() {
@Override public void run() {
completed = true;
}
};
private static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER =
(t, e) -> {
// No-op
};
private ThreadFactoryBuilder builder;
private volatile boolean completed = false;

@Before
public void setUp() {
builder = new ThreadFactoryBuilder();
}

@Test
public void testThreadFactoryBuilder_defaults() throws InterruptedException {
ThreadFactory threadFactory = builder.build();
Thread thread = threadFactory.newThread(monitoredRunnable);
checkThreadPoolName(thread, 1);
Thread defaultThread =
Executors.defaultThreadFactory().newThread(monitoredRunnable);
assertEquals(defaultThread.isDaemon(), thread.isDaemon());
assertEquals(defaultThread.getPriority(), thread.getPriority());
assertSame(defaultThread.getThreadGroup(), thread.getThreadGroup());
assertSame(defaultThread.getUncaughtExceptionHandler(),
thread.getUncaughtExceptionHandler());
assertFalse(completed);
thread.start();
thread.join();
assertTrue(completed);
// Creating a new thread from the same ThreadFactory will have the same
// pool ID but a thread ID of 2.
Thread thread2 = threadFactory.newThread(monitoredRunnable);
checkThreadPoolName(thread2, 2);
assertEquals(
thread.getName().substring(0, thread.getName().lastIndexOf('-')),
thread2.getName().substring(0, thread.getName().lastIndexOf('-')));
// Building again should give us a different pool ID.
ThreadFactory threadFactory2 = builder.build();
Thread thread3 = threadFactory2.newThread(monitoredRunnable);
checkThreadPoolName(thread3, 1);
assertNotEquals(thread2.getName().substring(0, thread.getName().lastIndexOf('-')), thread3.getName().substring(0, thread.getName().lastIndexOf('-')));
}
private static void checkThreadPoolName(Thread thread, int threadId) {
assertTrue(thread.getName().matches("^pool-\\d+-thread-" + threadId + "$"));
}

@Test
public void testNameFormatWithPercentS_custom() {
String format = "super-duper-thread-%s";
ThreadFactory factory = builder.setNameFormat(format).build();
for (int i = 0; i < 11; i++) {
assertEquals(String.format(format, i),
factory.newThread(monitoredRunnable).getName());
}
}

@Test
public void testNameFormatWithPercentD_custom() {
String format = "super-duper-thread-%d";
ThreadFactory factory = builder.setNameFormat(format).build();
for (int i = 0; i < 11; i++) {
assertEquals(String.format(format, i),
factory.newThread(monitoredRunnable).getName());
}
}

@Test
public void testDaemon_false() {
ThreadFactory factory = builder.setDaemon(false).build();
Thread thread = factory.newThread(monitoredRunnable);
assertFalse(thread.isDaemon());
}

@Test
public void testDaemon_true() {
ThreadFactory factory = builder.setDaemon(true).build();
Thread thread = factory.newThread(monitoredRunnable);
assertTrue(thread.isDaemon());
}

@Test
public void testPriority_custom() {
for (int i = Thread.MIN_PRIORITY; i <= Thread.MAX_PRIORITY; i++) {
ThreadFactory factory = builder.setPriority(i).build();
Thread thread = factory.newThread(monitoredRunnable);
assertEquals(i, thread.getPriority());
}
}

@Test
public void testPriority_tooLow() {
try {
builder.setPriority(Thread.MIN_PRIORITY - 1);
fail();
} catch (IllegalArgumentException expected) {
}
}

@Test
public void testPriority_tooHigh() {
try {
builder.setPriority(Thread.MAX_PRIORITY + 1);
fail();
} catch (IllegalArgumentException expected) {
}
}

@Test
public void testUncaughtExceptionHandler_custom() {
assertEquals(UNCAUGHT_EXCEPTION_HANDLER,
builder.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER).build()
.newThread(monitoredRunnable).getUncaughtExceptionHandler());
}

@Test
public void testBuildMutateBuild() {
ThreadFactory factory1 = builder.setPriority(1).build();
assertEquals(1, factory1.newThread(monitoredRunnable).getPriority());
ThreadFactory factory2 = builder.setPriority(2).build();
assertEquals(1, factory1.newThread(monitoredRunnable).getPriority());
assertEquals(2, factory2.newThread(monitoredRunnable).getPriority());
}

@Test
public void testBuildTwice() {
builder.build(); // this is allowed
builder.build(); // this is *also* allowed
}

@Test
public void testBuildMutate() {
ThreadFactory factory1 = builder.setPriority(1).build();
assertEquals(1, factory1.newThread(monitoredRunnable).getPriority());
builder.setPriority(2); // change the state of the builder
assertEquals(1, factory1.newThread(monitoredRunnable).getPriority());
}

@Test
public void testThreadFactory() throws InterruptedException {
final String THREAD_NAME = "ludicrous speed";
final int THREAD_PRIORITY = 1;
final boolean THREAD_DAEMON = false;
ThreadFactory backingThreadFactory = new ThreadFactory() {
@Override public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName(THREAD_NAME);
thread.setPriority(THREAD_PRIORITY);
thread.setDaemon(THREAD_DAEMON);
thread.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
return thread;
}
};
Thread thread = builder.setThreadFactory(backingThreadFactory).build()
.newThread(monitoredRunnable);
assertEquals(THREAD_NAME, thread.getName());
assertEquals(THREAD_PRIORITY, thread.getPriority());
assertEquals(THREAD_DAEMON, thread.isDaemon());
assertSame(UNCAUGHT_EXCEPTION_HANDLER,
thread.getUncaughtExceptionHandler());
assertSame(Thread.State.NEW, thread.getState());
assertFalse(completed);
thread.start();
thread.join();
assertTrue(completed);
}
}

0 comments on commit 6fac773

Please sign in to comment.