-
Notifications
You must be signed in to change notification settings - Fork 1
LineWriter, NonBlockingLineWriter & various fixes & clean-up #18
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
Changes from all commits
43c638f
c7c8d0f
518598d
cd27c33
02628df
3194202
f313a28
7a3acd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| package dev.enola.be.io; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public interface LineWriter { | ||
|
|
||
| void println(Object line) throws IOException; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| package dev.enola.be.io; | ||
|
|
||
| import java.io.IOException; | ||
|
|
||
| public final class LineWriters { | ||
|
|
||
| public static final LineWriter NOOP = _ -> {}; | ||
| public static final LineWriter SYSTEM_OUT = from(System.out); | ||
| public static final LineWriter SYSTEM_ERR = from(System.err); | ||
|
|
||
| public static LineWriter from(Appendable appendable) { | ||
| return new LineWriter() { | ||
| @Override | ||
| public void println(Object line) throws IOException { | ||
| appendable.append(line.toString()).append(System.lineSeparator()); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| private LineWriters() {} | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,52 @@ | ||||||
| package dev.enola.be.io; | ||||||
|
|
||||||
| import dev.enola.be.task.Status; | ||||||
| import dev.enola.be.task.TaskWithoutInputOutput; | ||||||
|
|
||||||
| import java.io.IOException; | ||||||
| import java.util.concurrent.ArrayBlockingQueue; | ||||||
| import java.util.concurrent.BlockingQueue; | ||||||
|
|
||||||
| public class NonBlockingLineWriter extends TaskWithoutInputOutput implements LineWriter { | ||||||
|
|
||||||
| private final LineWriter delegate; | ||||||
| private final BlockingQueue<Object> queue; | ||||||
| private volatile boolean overflow; | ||||||
|
|
||||||
| public NonBlockingLineWriter(int queueCapacity, LineWriter delegate) { | ||||||
| this.delegate = delegate; | ||||||
| this.queue = new ArrayBlockingQueue<>(queueCapacity, false); | ||||||
| } | ||||||
|
|
||||||
| @Override | ||||||
| public void println(Object line) throws IOException { | ||||||
| if (status() != Status.IN_PROGRESS) throw new IllegalStateException(); | ||||||
| if (!queue.offer(line)) overflow = true; | ||||||
| } | ||||||
|
|
||||||
| @Override | ||||||
| protected void executeIt() throws Exception { | ||||||
| try { | ||||||
| while (!Thread.currentThread().isInterrupted()) { | ||||||
| if (overflow) { | ||||||
| // https://en.wikipedia.org/wiki/Ellipsis | ||||||
| delegate.println("[… output ... ※ ... truncated …]"); | ||||||
|
||||||
| delegate.println("[… output ... ※ ... truncated …]"); | |
| delegate.println("[… output … ※ … truncated …]"); |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,19 +2,23 @@ | |||||||||||||
|
|
||||||||||||||
| import static java.util.concurrent.TimeUnit.MILLISECONDS; | ||||||||||||||
|
|
||||||||||||||
| import java.util.Collection; | ||||||||||||||
| import java.util.List; | ||||||||||||||
| import java.time.Instant; | ||||||||||||||
| import java.util.Map; | ||||||||||||||
| import java.util.Set; | ||||||||||||||
| import java.util.UUID; | ||||||||||||||
| import java.util.concurrent.Callable; | ||||||||||||||
| import java.util.concurrent.CompletableFuture; | ||||||||||||||
| import java.util.concurrent.ConcurrentHashMap; | ||||||||||||||
| import java.util.concurrent.ExecutorService; | ||||||||||||||
| import java.util.concurrent.Executors; | ||||||||||||||
| import java.util.concurrent.Future; | ||||||||||||||
| import java.util.concurrent.FutureTask; | ||||||||||||||
| import java.util.concurrent.ScheduledExecutorService; | ||||||||||||||
| import java.util.logging.Logger; | ||||||||||||||
|
|
||||||||||||||
| public class TaskExecutor implements AutoCloseable { | ||||||||||||||
|
|
||||||||||||||
| private static final Logger LOG = Logger.getLogger(TaskExecutor.class.getName()); | ||||||||||||||
|
|
||||||||||||||
| // TODO Synthetic "root" task, to which all running tasks are children? | ||||||||||||||
| // This could be useful for managing task hierarchies and dependencies. | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -26,6 +30,28 @@ public class TaskExecutor implements AutoCloseable { | |||||||||||||
|
|
||||||||||||||
| private final ExecutorService executor = TaskExecutorServices.newVirtualThreadPerTaskExecutor(); | ||||||||||||||
|
|
||||||||||||||
| // TODO Can the timeoutScheduler also use virtual threads? | ||||||||||||||
| // (Would need to check if ScheduledExecutorService supports that.) | ||||||||||||||
| // | ||||||||||||||
| // TODO Either way, use LoggingScheduledExecutorService from Enola Commons? | ||||||||||||||
| private final ScheduledExecutorService timeoutScheduler = | ||||||||||||||
| Executors.newSingleThreadScheduledExecutor(); | ||||||||||||||
|
|
||||||||||||||
| private static class LoggingFutureTask<V> extends FutureTask<V> { | ||||||||||||||
| private final Task<?, V> task; | ||||||||||||||
|
|
||||||||||||||
| public LoggingFutureTask(Callable<V> callable, Task<?, V> task) { | ||||||||||||||
| super(callable); | ||||||||||||||
| this.task = task; | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| @Override | ||||||||||||||
| protected void done() { | ||||||||||||||
| task.endedAt(Instant.now()); | ||||||||||||||
| LOG.fine(() -> task.toString()); | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private <O> Future<O> future(Task<?, O> task) throws IllegalStateException { | ||||||||||||||
| if (tasks.putIfAbsent(task.id(), task) != null) | ||||||||||||||
| throw new IllegalStateException("Task already submitted: " + task.id()); | ||||||||||||||
|
|
@@ -38,27 +64,19 @@ private <O> Future<O> future(Task<?, O> task) throws IllegalStateException { | |||||||||||||
| throw new IllegalStateException( | ||||||||||||||
| "Task " + task.id() + " not PENDING: " + task.status()); | ||||||||||||||
|
|
||||||||||||||
| Future<O> future; | ||||||||||||||
| var timeout = task.timeout(); | ||||||||||||||
| Callable<O> callable = new TaskCallable<>(task); | ||||||||||||||
| if (timeout.isZero() || timeout.isNegative()) { | ||||||||||||||
| future = executor.submit(callable); | ||||||||||||||
| } else { | ||||||||||||||
| Collection<? extends Callable<O>> callables = List.of(callable); | ||||||||||||||
| try { | ||||||||||||||
| // Nota bene: The risk of toMillis() throwing an ArithmeticException is | ||||||||||||||
| // unrealistically low, as that would require a timeout of more than ~292 | ||||||||||||||
| // million years... :-) But if that ever happens, we want to know about it, | ||||||||||||||
| // hence no try/catch here. | ||||||||||||||
| var futures = executor.invokeAll(callables, timeout.toMillis(), MILLISECONDS); | ||||||||||||||
| future = futures.iterator().next(); | ||||||||||||||
| } catch (InterruptedException e) { | ||||||||||||||
| future = CompletableFuture.failedFuture(e); | ||||||||||||||
| Thread.currentThread().interrupt(); | ||||||||||||||
| } | ||||||||||||||
| var futureTask = new LoggingFutureTask<>(callable, task); | ||||||||||||||
|
|
||||||||||||||
| executor.execute(futureTask); | ||||||||||||||
|
|
||||||||||||||
| var timeout = task.timeout(); | ||||||||||||||
| if (!timeout.isZero() && !timeout.isNegative()) { | ||||||||||||||
| timeoutScheduler.schedule( | ||||||||||||||
| () -> futureTask.cancel(true), timeout.toMillis(), MILLISECONDS); | ||||||||||||||
|
||||||||||||||
| () -> futureTask.cancel(true), timeout.toMillis(), MILLISECONDS); | |
| () -> { | |
| if (!futureTask.isDone()) { | |
| futureTask.cancel(true); | |
| } | |
| }, timeout.toMillis(), MILLISECONDS); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package dev.enola.be.task; | ||
|
|
||
| public abstract class TaskWithoutInputOutput extends Task<Empty, Empty> { | ||
|
|
||
| protected TaskWithoutInputOutput() { | ||
| super(Empty.INSTANCE); | ||
| } | ||
|
|
||
| @Override | ||
| protected final Empty execute() throws Exception { | ||
| executeIt(); | ||
| return Empty.INSTANCE; | ||
| } | ||
|
|
||
| protected abstract void executeIt() throws Exception; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,23 +1,28 @@ | ||
| package dev.enola.be.task.demo; | ||
|
|
||
| import dev.enola.be.io.LineWriters; | ||
| import dev.enola.be.io.NonBlockingLineWriter; | ||
| import dev.enola.be.task.Task; | ||
| import dev.enola.be.task.TaskExecutor; | ||
| import dev.enola.be.task.demo.LongIncrementingTask.Input; | ||
| import dev.enola.be.task.demo.LongIncrementingTask.Output; | ||
| import dev.enola.common.concurrent.Threads; | ||
| import dev.enola.common.function.CheckedConsumer; | ||
| import dev.enola.common.log.JulConfigurer; | ||
|
|
||
| import java.io.IOException; | ||
| import java.time.Duration; | ||
| import java.time.Instant; | ||
| import java.util.function.Consumer; | ||
|
|
||
| public class LongIncrementingTask extends Task<Input, Output> { | ||
|
|
||
| record Input(long max, Duration sleep) {} | ||
|
|
||
| record Output(long result) {} | ||
|
|
||
| private final Consumer<Long> progressConsumer; | ||
| private final CheckedConsumer<Long, IOException> progressConsumer; | ||
|
|
||
| public LongIncrementingTask(Input input, Consumer<Long> progressConsumer) { | ||
| public LongIncrementingTask(Input input, CheckedConsumer<Long, IOException> progressConsumer) { | ||
| super(input); | ||
| this.progressConsumer = progressConsumer; | ||
| } | ||
|
|
@@ -26,7 +31,7 @@ public LongIncrementingTask(Input input, Consumer<Long> progressConsumer) { | |
| protected Output execute() throws Exception { | ||
| for (long i = 0; i < input.max; i++) { | ||
| progressConsumer.accept(i); | ||
| Thread.yield(); | ||
| // Do *NOT* Thread.yield(); that makes it really horribly slow, by like a factor x100! | ||
| if (Thread.currentThread().isInterrupted()) | ||
| throw new InterruptedException("Task was interrupted"); | ||
| Threads.sleep(input.sleep); | ||
|
|
@@ -39,7 +44,10 @@ protected Output execute() throws Exception { | |
|
|
||
| private static void simpleLoop(long max, Duration sleep) throws InterruptedException { | ||
| var start = Instant.now(); | ||
| for (long i = 0; i < max; i++) Threads.sleep(sleep); | ||
| for (long i = 0; i < max; i++) { | ||
| // Do *NOT* Thread.yield(); that makes it really horribly slow, by like a factor x100! | ||
| Threads.sleep(sleep); | ||
| } | ||
| var duration = Duration.between(start, Instant.now()); | ||
| System.out.println( | ||
| "Looped to " | ||
|
|
@@ -51,17 +59,25 @@ private static void simpleLoop(long max, Duration sleep) throws InterruptedExcep | |
| } | ||
|
|
||
| public static void main(String[] args) throws InterruptedException { | ||
| // Count to max, with 1ms pause between each increment | ||
| var max = 10000; | ||
| JulConfigurer.configureRootLogger(); | ||
|
|
||
| // Count to max, with sleep pause between each increment | ||
| var max = 1000000L; | ||
|
||
| var sleep = Duration.ofMillis(0); | ||
|
|
||
| var input = new Input(max, sleep); | ||
| var task = new LongIncrementingTask(input, System.out::println); | ||
| var pumperTask = new NonBlockingLineWriter(13, LineWriters.SYSTEM_OUT); | ||
|
||
| var printingTask = new LongIncrementingTask(input, pumperTask::println); | ||
| var silentTask = new LongIncrementingTask(input, LineWriters.NOOP::println); | ||
|
|
||
| try (var executor = new TaskExecutor()) { | ||
| executor.await(task); | ||
| System.out.println(task); | ||
| } | ||
| executor.async(pumperTask); | ||
| executor.async(silentTask); | ||
|
|
||
| executor.await(printingTask); | ||
| silentTask.await(); | ||
|
|
||
| simpleLoop(max, sleep); | ||
| simpleLoop(max, sleep); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.