diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 8e377f0..f05bb75 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,6 +9,7 @@ diff --git a/basic-concurrency/build.gradle b/basic-concurrency/build.gradle new file mode 100644 index 0000000..e35abb8 --- /dev/null +++ b/basic-concurrency/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java' +} + +getTasks().withType(JavaCompile.class).configureEach { + getOptions().getRelease().set(9) +} + +repositories { + mavenCentral() +} + +dependencies { + implementation libs.contracts + implementation libs.concurrency + + testImplementation libs.contracts.test + testImplementation libs.concurrency.test +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java new file mode 100644 index 0000000..ab4bcaa --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Command.java @@ -0,0 +1,12 @@ +package io.github.jonloucks.examples.concurrency.basic; + +@FunctionalInterface +public interface Command { + + String execute(); + + default boolean foreground() { + return false; + } + +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java new file mode 100644 index 0000000..f276258 --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Constants.java @@ -0,0 +1,58 @@ +package io.github.jonloucks.examples.concurrency.basic; + +import io.github.jonloucks.concurrency.api.Waitable; +import io.github.jonloucks.contracts.api.Contract; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Contracts can be defined anywhere, placing them here to demonstrate package private access + */ +final class Constants { + + /** + * Constant string, but open for uses cases like localization. + */ + static final Contract PROGRAM_NAME = Contract.create("Program name contract"); + + /** + * The main program implementation, not everything has to be in the class with main() + */ + static final Contract PROGRAM = Contract.create("Program contract"); + + /** + * Example of a configuration setting that is a singleton with lazy evaluation of value + */ + static final Contract RUNNER_THREAD_COUNT = Contract.create("Number of worker threads contract"); + + /** + * Example of a shared executor with auto resource management + */ + static final Contract RUNNER = Contract.create("Dispatcher contract"); + + /** + * Set to true when program is quitting + */ + static final Contract> IS_QUITTING = Contract.create("Quitting contract"); + + /** + * The health the program + */ + static final Contract> HEALTH = Contract.create("Health contract"); + + /** + * Command Output + */ + static final Contract> OUTPUT = Contract.create("Command output contract"); + + /** + * Command Input + */ + static final Contract> INPUT = Contract.create("Command input contract"); + + /** + * Command Output + */ + static final Contract> ERROR = Contract.create("Command error contract"); +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java new file mode 100644 index 0000000..0ab85f9 --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Dispatcher.java @@ -0,0 +1,6 @@ +package io.github.jonloucks.examples.concurrency.basic; + +public interface Dispatcher { + + void dispatch(Command command); +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java new file mode 100644 index 0000000..38787ec --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/DispatcherImpl.java @@ -0,0 +1,69 @@ +package io.github.jonloucks.examples.concurrency.basic; + +import io.github.jonloucks.concurrency.api.OnCompletion; +import io.github.jonloucks.concurrency.api.Waitable; +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.AutoOpen; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.*; +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.concurrency.basic.Constants.*; +import static java.util.Optional.ofNullable; + +final class DispatcherImpl implements Dispatcher, AutoOpen { + @Override + public void dispatch(Command command) { + incrementRunning(); + final OnCompletion onCompletion = c -> { + c.getValue().ifPresent(output); + c.getThrown().ifPresent(e ->error.accept(e.getMessage())); + decrementRunning(); + }; + if (!command.foreground() && ofNullable(delegate).isPresent()) { + completeLater(onCompletion, on -> delegate.execute(() -> completeNow(on, command::execute))); + } else { + completeNow(onCompletion, command::execute); + } + } + + @Override + public AutoClose open() { + delegate = Executors.newFixedThreadPool(claimContract(RUNNER_THREAD_COUNT)); + return this::privateClose; // only open caller can close + } + + private void privateClose() { + ofNullable(delegate).ifPresent(executor -> { + delegate = null; + executor.shutdown(); + try { + if (executor.awaitTermination(5, TimeUnit.MINUTES)) { + executor.shutdownNow(); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + }); + } + + private void incrementRunning() { + runningCommandCount.incrementAndGet(); + health.accept("busy"); + } + + private void decrementRunning() { + if (runningCommandCount.decrementAndGet() == 0) { + health.accept("idle"); + } + } + + private ExecutorService delegate; + private final AtomicInteger runningCommandCount = new AtomicInteger(); + private final Consumer output = claimContract(OUTPUT); + private final Consumer error = claimContract(ERROR); + private final Waitable health = claimContract(HEALTH); +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java new file mode 100644 index 0000000..1fc5c48 --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/HelpCommand.java @@ -0,0 +1,17 @@ +package io.github.jonloucks.examples.concurrency.basic; + +final class HelpCommand implements Command { + @Override + public String execute() { + return "This is an example..."; + } + + @Override + public boolean foreground() { + return true; + } + + HelpCommand() { + + } +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java new file mode 100644 index 0000000..38177c3 --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Main.java @@ -0,0 +1,95 @@ +package io.github.jonloucks.examples.concurrency.basic; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.BindStrategy; +import io.github.jonloucks.contracts.api.Repository; + +import java.util.Scanner; + +import static io.github.jonloucks.concurrency.api.GlobalConcurrency.createWaitable; +import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND; +import static io.github.jonloucks.contracts.api.GlobalContracts.*; +import static io.github.jonloucks.examples.concurrency.basic.Constants.*; +import static java.lang.Boolean.TRUE; + +@SuppressWarnings("unused") +public final class Main { + + /** + * Main entry point. + * Note. Entry points are where final decisions on dependency inversions are made. + * Not all decisions must be made in an entry point, but in this example it is exposed + * for visibility + * @param args the command line arguments + */ + public static void main(String[] args) { + final Repository repository = createMyRepository(); + try (AutoClose closeRepository = repository.open(); + AutoClose closeHealthNotify = openHealthNotify()) { + + final Program program = claimContract(PROGRAM); + + waitForQuit(); + + waitForIdle(); + } // all repository resources will be released when this try block exits + } + + private static void waitForQuit() { + claimContract(IS_QUITTING).getWhen(TRUE::equals); + } + + private static void waitForIdle() { + claimContract(HEALTH).getWhen("idle"::equals); + } + + private static AutoClose openHealthNotify() { + return claimContract(HEALTH).notifyIf("idle"::equals, health -> { + System.out.println( "All commands have completed."); + }); + } + + /** + * For complex projects with many modules and entry points there a many + * ways to extend and manage dependency inversions without + * have them all defined here. + * For example: + * each module could have its own Repository. + * MyModule.createRepository(contracts) + * each module could have a static method to install contracts to a given repository + * MyModule.install(repository) + * each module could use the ServiceLoader mechanism + * I am sure there are more like SpringBoot + */ + private static Repository createMyRepository() { + final Repository repository = claimContract(Repository.FACTORY).get(); + + final BindStrategy strategy = IF_NOT_BOUND; + + // Constant string, but could be changed to a localized value without changing uses + repository.keep(PROGRAM_NAME, () -> "Concurrency Example"); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(PROGRAM, lifeCycle(ProgramImpl::new)); + + repository.keep(OUTPUT, singleton(() -> text -> System.out.println(text)), strategy); + + repository.keep(ERROR, singleton(() -> text -> System.err.println(text)), strategy); + + repository.keep(INPUT, singleton(() -> new Scanner(System.in)::nextLine), strategy); + + // lazy evaluated singleton + repository.keep(RUNNER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8)); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(RUNNER, lifeCycle(DispatcherImpl::new)); + + // set to true when program is quiting + repository.keep(IS_QUITTING, singleton(() -> createWaitable(false))); + + // program health + repository.keep(HEALTH, singleton(() -> createWaitable("ready"))); + + return repository; + } +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java new file mode 100644 index 0000000..a99dbec --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/Program.java @@ -0,0 +1,4 @@ +package io.github.jonloucks.examples.concurrency.basic; + +interface Program { +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java new file mode 100644 index 0000000..37d97ca --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/ProgramImpl.java @@ -0,0 +1,77 @@ +package io.github.jonloucks.examples.concurrency.basic; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.AutoOpen; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.concurrency.basic.Constants.*; +import static java.util.Optional.ofNullable; + +final class ProgramImpl implements Program, AutoOpen { + + @Override + public AutoClose open() { + serviceStart = Instant.now(); + dispatcher = claimContract(Constants.RUNNER); + output.accept("Welcome to " + claimContract(PROGRAM_NAME)); + new Thread(this::commandLoop).start(); + return this::privateClose; + } + + ProgramImpl() { + } + + void commandLoop() { + while (!isQuitting()) { + promptForCommand().ifPresent(dispatcher::dispatch); + } + } + + private Duration getUptime() { + return Duration.between(serviceStart, ofNullable(serviceEnd).orElseGet(Instant::now)); + } + + private boolean isQuitting() { + return claimContract(IS_QUITTING).get(); + } + + private Optional promptForCommand() { + try { + output.accept("Enter command: "); + return Optional.of(parseCommand(input.get())); // Read the entire line until a newline character + } catch (Exception thrown) { + claimContract(IS_QUITTING).accept(true); + return Optional.empty(); + } + } + + private Command parseCommand(String commandLine) { + switch (ofNullable(commandLine).orElse("").toLowerCase()) { + case "": + case "help": + case "?": + return new HelpCommand(); + case "quit": + return new QuitCommand(); + default: + return () -> "Unrecognized command: " + commandLine; + } + } + + private void privateClose() { + serviceEnd = Instant.now(); + output.accept("Service Uptime: " + getUptime()); + } + + private final Supplier input = claimContract(INPUT); + private final Consumer output = claimContract(OUTPUT); + private Instant serviceStart; + private Instant serviceEnd; + private Dispatcher dispatcher; +} diff --git a/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java new file mode 100644 index 0000000..c663f65 --- /dev/null +++ b/basic-concurrency/src/main/java/io/github/jonloucks/examples/concurrency/basic/QuitCommand.java @@ -0,0 +1,21 @@ +package io.github.jonloucks.examples.concurrency.basic; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.concurrency.basic.Constants.IS_QUITTING; + +final class QuitCommand implements Command { + @Override + public String execute() { + claimContract(IS_QUITTING).accept(true); + return "Quit initiated"; + } + + @Override + public boolean foreground() { + return true; + } + + QuitCommand() { + + } +} diff --git a/basic-concurrency/src/main/java/module-info.java b/basic-concurrency/src/main/java/module-info.java new file mode 100644 index 0000000..97bd0ba --- /dev/null +++ b/basic-concurrency/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module io.github.jonloucks.examples.concurrency.basic { + requires transitive io.github.jonloucks.contracts; + requires transitive io.github.jonloucks.concurrency; + + exports io.github.jonloucks.examples.concurrency.basic; +} \ No newline at end of file diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Constants.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Constants.java deleted file mode 100644 index c4face6..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Constants.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.jonloucks.examples.basic.contracts; - -import io.github.jonloucks.contracts.api.Contract; - -import java.util.concurrent.Executor; - -/** - * Contracts can be defined anywhere, placing them here to demonstrate package private access - */ -final class Constants { - - /** - * Constant string, but open for uses cases like localization. - */ - static final Contract PROGRAM_NAME_CONTRACT = Contract.create("Program name"); - - /** - * The main program implementation, not everything has to be in the class with main() - */ - static final Contract PROGRAM_CONTRACT = Contract.create("Program contract"); - - /** - * Example of a configuration setting that is a singleton with lazy evaluation of value - */ - static final Contract WORKER_THREAD_COUNT = Contract.create("Number of worker threads contract"); - - /** - * Example of a shared executor with auto resource management - */ - static final Contract EXECUTOR_CONTRACT = Contract.create("Executor contract"); -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Main.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Main.java deleted file mode 100644 index c09a158..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/Main.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.jonloucks.examples.basic.contracts; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.Repository; - -import static io.github.jonloucks.contracts.api.GlobalContracts.*; -import static io.github.jonloucks.examples.basic.contracts.Constants.*; - -public final class Main { - - /** - * Main entry point - * @param args the command line arguments - */ - public static void main(String[] args) { - final Repository repository = createMyRepository(); - try (AutoClose closeRepository = repository.open()) { - System.out.println("Welcome to " + claimContract(PROGRAM_NAME_CONTRACT)); - final MyProgram myProgram = claimContract(PROGRAM_CONTRACT); - - myProgram.runCommand(args); - - System.out.println("Service Uptime: " + myProgram.getUptime()); - } // all resources will be released when this try block exits - } - - private static Repository createMyRepository() { - final Repository repository = claimContract(Repository.FACTORY).get(); - - // Constant string, but could be changed to a localized value without changing uses - repository.keep(PROGRAM_NAME_CONTRACT, () -> "Contracts Example"); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(PROGRAM_CONTRACT, lifeCycle(MyProgramImpl::new)); - - // lazy evaluated singleton - repository.keep(WORKER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8)); - - // lifeCycle will create a singleton and detect AutoOpen implementations - repository.keep(EXECUTOR_CONTRACT, lifeCycle(MyExecutor::new)); - - return repository; - } -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyExecutor.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyExecutor.java deleted file mode 100644 index c491256..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyExecutor.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.jonloucks.examples.basic.contracts; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.AutoOpen; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static java.util.Optional.ofNullable; - -final class MyExecutor implements Executor, AutoOpen { - @Override - public void execute(Runnable command) { - if (ofNullable(delegate).isPresent()) { - delegate.execute(command); - } else { - command.run(); - } - } - - @Override - public AutoClose open() { - delegate = Executors.newFixedThreadPool(claimContract(Constants.WORKER_THREAD_COUNT)); - return this::myClose; - } - - private void myClose() { - ofNullable(delegate).ifPresent(executor -> { - delegate = null; - executor.shutdown(); - try { - if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { - executor.shutdownNow(); - } - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - }); - } - - private ExecutorService delegate; -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgram.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgram.java deleted file mode 100644 index f3ed9d5..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgram.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.jonloucks.examples.basic.contracts; - -import java.time.Duration; - -interface MyProgram { - Duration getUptime(); - - void runCommand(String[] args); -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgramImpl.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgramImpl.java deleted file mode 100644 index 93adcbb..0000000 --- a/basic-contracts/src/main/java/io/github/jonloucks/examples/basic/contracts/MyProgramImpl.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.jonloucks.examples.basic.contracts; - -import io.github.jonloucks.contracts.api.AutoClose; -import io.github.jonloucks.contracts.api.AutoOpen; - -import java.time.Duration; -import java.time.Instant; -import java.util.Arrays; -import java.util.concurrent.Executor; - -import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; -import static java.util.Optional.ofNullable; - -final class MyProgramImpl implements MyProgram, AutoOpen { - - @Override - public AutoClose open() { - serviceStart = Instant.now(); - executor = claimContract(Constants.EXECUTOR_CONTRACT); - return this::close; - } - - @Override - public Duration getUptime() { - return Duration.between(serviceStart, ofNullable(serviceEnd).orElseGet(Instant::now)); - } - - @Override - public void runCommand(String[] args) { - if (args.length == 0) { - executor.execute(() -> System.out.println("Command not specified.")); - } else { - executor.execute(() -> System.out.println("Unrecognized command: " + Arrays.toString(args))); - } - } - - MyProgramImpl() { - } - - private void close() { - serviceEnd = Instant.now(); - } - - private Instant serviceStart; - private Instant serviceEnd; - private Executor executor; -} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java new file mode 100644 index 0000000..56e89c5 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Command.java @@ -0,0 +1,12 @@ +package io.github.jonloucks.examples.contracts.basic; + +@FunctionalInterface +public interface Command { + + String execute(); + + default boolean foreground() { + return false; + } + +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java new file mode 100644 index 0000000..9295785 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Constants.java @@ -0,0 +1,60 @@ +package io.github.jonloucks.examples.contracts.basic; + +import io.github.jonloucks.contracts.api.Contract; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Contracts can be defined anywhere, placing them here to demonstrate package private access + */ +final class Constants { + + /** + * Constant string, but open for uses cases like localization. + */ + static final Contract PROGRAM_NAME = Contract.create("Program name contract"); + + /** + * The main program implementation, not everything has to be in the class with main() + */ + static final Contract PROGRAM = Contract.create("Program contract"); + + /** + * Example of a configuration setting that is a singleton with lazy evaluation of value + */ + static final Contract RUNNER_THREAD_COUNT = Contract.create("Number of worker threads contract"); + + /** + * Example of a shared executor with auto resource management + */ + static final Contract DISPATCHER = Contract.create("Dispatcher contract"); + + /** + * Set to true when program is quitting + */ + static final Contract IS_QUITTING = Contract.create("Quitting contract"); + + /** + * The health the program + */ + static final Contract> HEALTH = Contract.create("Health contract"); + + /** + * Command Output + */ + static final Contract> OUTPUT = Contract.create("Command output contract"); + + /** + * Command Input + */ + static final Contract> INPUT = Contract.create("Command input contract"); + + /** + * Command Output + */ + static final Contract> ERROR = Contract.create("Command error contract"); + +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java new file mode 100644 index 0000000..042f966 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Dispatcher.java @@ -0,0 +1,6 @@ +package io.github.jonloucks.examples.contracts.basic; + +public interface Dispatcher { + + void dispatch(Command command); +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java new file mode 100644 index 0000000..26627eb --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/DispatcherImpl.java @@ -0,0 +1,76 @@ +package io.github.jonloucks.examples.contracts.basic; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.AutoOpen; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.contracts.basic.Constants.*; +import static java.util.Optional.ofNullable; + +final class DispatcherImpl implements Dispatcher, AutoOpen { + + @Override + public void dispatch(Command command) { + incrementRunning(); + + final Runnable job = () -> { + try { + output.accept(command.execute()); + } catch (Throwable thrown) { + error.accept(thrown.getMessage()); + } finally { + decrementRunning(); + } + }; + + if (!command.foreground() && ofNullable(delegate).isPresent()) { + delegate.execute(job); + } else { + job.run(); + } + } + + @Override + public AutoClose open() { + delegate = Executors.newFixedThreadPool(claimContract(RUNNER_THREAD_COUNT)); + return this::privateClose; // only open caller can close + } + + private void privateClose() { + ofNullable(delegate).ifPresent(executor -> { + delegate = null; + executor.shutdown(); + try { + if (!executor.awaitTermination(1, TimeUnit.MINUTES)) { + executor.shutdownNow(); + } + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + }); + } + + private void incrementRunning() { + runningCommandCount.incrementAndGet(); + health.set("busy"); + } + + private void decrementRunning() { + if (runningCommandCount.decrementAndGet() == 0) { + health.set("idle"); + } + } + + private ExecutorService delegate; + private final AtomicInteger runningCommandCount = new AtomicInteger(); + private final Consumer error = claimContract(ERROR); + private final Consumer output = claimContract(OUTPUT); + private final AtomicReference health = claimContract(HEALTH); +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java new file mode 100644 index 0000000..224ab08 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/HelpCommand.java @@ -0,0 +1,17 @@ +package io.github.jonloucks.examples.contracts.basic; + +final class HelpCommand implements Command { + @Override + public String execute() { + return "This is an example..."; + } + + @Override + public boolean foreground() { + return true; + } + + HelpCommand() { + + } +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java new file mode 100644 index 0000000..3f20fbf --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Main.java @@ -0,0 +1,86 @@ +package io.github.jonloucks.examples.contracts.basic; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.BindStrategy; +import io.github.jonloucks.contracts.api.Repository; + +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND; +import static io.github.jonloucks.contracts.api.GlobalContracts.*; +import static io.github.jonloucks.examples.contracts.basic.Constants.*; + +@SuppressWarnings("unused") +public final class Main { + + /** + * Main entry point. + * Note. Entry points are where final decisions on dependency inversions are made. + * Not all decisions must be made in an entry point, but in this example it is exposed + * for visibility + * @param args the command line arguments + */ + public static void main(String[] args) { + final Repository repository = createMyRepository(); + try (AutoClose closeRepository = repository.open()) { + + final Program program = claimContract(PROGRAM); + + waitForQuit(); + } // all repository resources will be released when this try block exits + } + + private static void waitForQuit() { + try { + claimContract(IS_QUITTING).await(); + } catch (InterruptedException ignored) { + throw new RuntimeException("Waiting for quit"); + } + } + + /** + * For complex projects with many modules and entry points there a many + * ways to extend and manage dependency inversions without + * have them all defined here. + * For example: + * each module could have its own Repository. + * MyModule.createRepository(contracts) + * each module could have a static method to install contracts to a given repository + * MyModule.install(repository) + * each module could use the ServiceLoader mechanism + * I am sure there are more like SpringBoot + */ + private static Repository createMyRepository() { + final Repository repository = claimContract(Repository.FACTORY).get(); + + final BindStrategy strategy = IF_NOT_BOUND; + + // Constant string, but could be changed to a localized value without changing uses + repository.keep(PROGRAM_NAME, () -> "Concurrency Example", strategy); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(PROGRAM, lifeCycle(ProgramImpl::new), strategy); + + repository.keep(OUTPUT, singleton(() -> text -> System.out.println(text)), strategy); + + repository.keep(ERROR, singleton(() -> text -> System.err.println(text)), strategy); + + repository.keep(INPUT, singleton(() -> new Scanner(System.in)::nextLine), strategy); + + // lazy evaluated singleton + repository.keep(RUNNER_THREAD_COUNT, singleton(() -> Runtime.getRuntime().availableProcessors() * 8), strategy); + + // lifeCycle will create a singleton and detect AutoOpen implementations + repository.keep(DISPATCHER, lifeCycle(DispatcherImpl::new), strategy); + + // set to true when program is quiting + repository.keep(IS_QUITTING, singleton(() -> new CountDownLatch(1)), strategy); + + // program health + repository.keep(HEALTH, singleton(() -> new AtomicReference<>("ready")), strategy); + + return repository; + } +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java new file mode 100644 index 0000000..2be3712 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/Program.java @@ -0,0 +1,5 @@ +package io.github.jonloucks.examples.contracts.basic; + +interface Program { + +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java new file mode 100644 index 0000000..85eeb9c --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/ProgramImpl.java @@ -0,0 +1,83 @@ +package io.github.jonloucks.examples.contracts.basic; + +import io.github.jonloucks.contracts.api.AutoClose; +import io.github.jonloucks.contracts.api.AutoOpen; + + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.contracts.basic.Constants.*; +import static java.util.Optional.ofNullable; + +final class ProgramImpl implements Program, AutoOpen { + + @Override + public AutoClose open() { + serviceStart = Instant.now(); + dispatcher = claimContract(DISPATCHER); + output.accept("Welcome to " + claimContract(PROGRAM_NAME)); + new Thread(this::commandLoop).start(); + return this::privateClose; + } + + ProgramImpl() { + } + + private void commandLoop() { + while (!isQuitting()) { + promptForCommand().ifPresent(dispatcher::dispatch); + } + } + + private Duration getUptime() { + return Duration.between(serviceStart, ofNullable(serviceEnd).orElseGet(Instant::now)); + } + + private boolean isQuitting() { + try { + return claimContract(IS_QUITTING).await(0, TimeUnit.MINUTES); + } catch (InterruptedException e) { + return true; + } + } + + private Optional promptForCommand() { + try { + output.accept("Enter command: "); + return Optional.of(parseCommand(input.get())); // Read the entire line until a newline character + } catch (Exception thrown) { + claimContract(IS_QUITTING).countDown(); + return Optional.empty(); + } + } + + private Command parseCommand(String commandLine) { + switch (ofNullable(commandLine).orElse("").toLowerCase()) { + case "": + case "help": + case "?": + return new HelpCommand(); + case "quit": + return new QuitCommand(); + default: + return () -> "Unrecognized command: " + commandLine; + } + } + + private void privateClose() { + serviceEnd = Instant.now(); + output.accept("Service Uptime: " + getUptime()); + } + + private final Supplier input = claimContract(INPUT); + private final Consumer output = claimContract(OUTPUT); + private Instant serviceStart; + private Instant serviceEnd; + private Dispatcher dispatcher; +} diff --git a/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java new file mode 100644 index 0000000..1361343 --- /dev/null +++ b/basic-contracts/src/main/java/io/github/jonloucks/examples/contracts/basic/QuitCommand.java @@ -0,0 +1,21 @@ +package io.github.jonloucks.examples.contracts.basic; + +import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract; +import static io.github.jonloucks.examples.contracts.basic.Constants.IS_QUITTING; + +final class QuitCommand implements Command { + @Override + public String execute() { + claimContract(IS_QUITTING).countDown(); + return "Quit initiated"; + } + + @Override + public boolean foreground() { + return true; + } + + QuitCommand() { + + } +} diff --git a/basic-contracts/src/main/java/module-info.java b/basic-contracts/src/main/java/module-info.java new file mode 100644 index 0000000..d88a09a --- /dev/null +++ b/basic-contracts/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module io.github.jonloucks.examples.contracts.basic { + requires transitive io.github.jonloucks.contracts; + + exports io.github.jonloucks.examples.contracts.basic; +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index de69431..e5a05d5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,3 @@ rootProject.name = 'examples' -include 'basic-contracts' \ No newline at end of file +include 'basic-contracts' +include 'basic-concurrency' \ No newline at end of file