-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
340 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
156 changes: 156 additions & 0 deletions
156
src/main/java/io/split/android/client/service/executor/ThreadFactoryBuilder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
5 changes: 2 additions & 3 deletions
5
...ain/java/io/split/android/client/service/sseclient/sseclient/PushNotificationManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
181 changes: 181 additions & 0 deletions
181
src/test/java/io/split/android/client/service/executor/ThreadFactoryBuilderTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |