CONTRACT = Contract.create(ClientFactory.class);
+
+ /**
+ * Create a new instance of Client
+ *
+ * Note: caller is responsible for calling {@link AutoOpen#open()} and calling
+ * the {@link io.github.jonloucks.contracts.api.AutoClose#close() when done}
+ *
+ * @param config the Client configuration for the new instance
+ * @return the new Client instance
+ */
+ Client create(Client.Config config);
+
+ /**
+ * Create a new instance of Client
+ *
+ * @param builderConsumer the config builder consumer callback
+ * @return the new Client instance
+ * @throws IllegalArgumentException if builderConsumer is null or when configuration is invalid
+ */
+ Client create(Consumer builderConsumer);
+
+ /**
+ * Install all the requirements and promises to the given Client Repository.
+ * Include Client#CONTRACT which will private a unique
+ *
+ * @param config the Client config
+ * @param repository the repository to add requirements and promises to
+ * @throws IllegalArgumentException if config is null, config is invalid, or repository is null
+ */
+ void install(Client.Config config, Repository repository);
+}
diff --git a/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryFinder.java b/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryFinder.java
new file mode 100644
index 0000000..67e3de1
--- /dev/null
+++ b/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryFinder.java
@@ -0,0 +1,71 @@
+package io.github.jonloucks.example.client;
+
+import java.util.Optional;
+import java.util.ServiceLoader;
+
+import static io.github.jonloucks.contracts.api.Checks.configCheck;
+import static io.github.jonloucks.contracts.api.Checks.nullCheck;
+import static java.util.Optional.ofNullable;
+
+/**
+ * Responsible for locating and creating the ClientFactory for a deployment.
+ */
+public final class ClientFactoryFinder {
+ public ClientFactoryFinder(Client.Config config) {
+ this.config = configCheck(config);
+ }
+
+ public ClientFactory get() {
+ return find().orElseThrow(this::newNotFoundException);
+ }
+
+ public Optional find() {
+ final Optional byReflection = createByReflection();
+ if (byReflection.isPresent()) {
+ return byReflection;
+ }
+ return createByServiceLoader();
+ }
+
+ private Optional createByServiceLoader() {
+ if (config.useServiceLoader()) {
+ try {
+ for (ClientFactory factory : ServiceLoader.load(getServiceFactoryClass())) {
+ return Optional.of(factory);
+ }
+ } catch (Throwable ignored) {
+ return Optional.empty();
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Class extends ClientFactory> getServiceFactoryClass() {
+ return nullCheck(config.serviceLoaderClass(), "Client Service Loader class must be present.");
+ }
+
+ private Optional createByReflection() {
+ if (config.useReflection()) {
+ return getReflectionClassName().map(this::createNewInstance);
+ }
+ return Optional.empty();
+ }
+
+ private ClientFactory createNewInstance(String className) {
+ try {
+ return (ClientFactory)Class.forName(className).getConstructor().newInstance();
+ } catch (Throwable thrown) {
+ return null;
+ }
+ }
+
+ private Optional getReflectionClassName() {
+ return ofNullable(config.reflectionClassName()).filter(x -> !x.isEmpty());
+ }
+
+ private ClientException newNotFoundException() {
+ return new ClientException("Unable to find Client factory.");
+ }
+
+ private final Client.Config config;
+}
diff --git a/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryImpl.java b/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryImpl.java
new file mode 100644
index 0000000..de545bc
--- /dev/null
+++ b/client/src/main/java/io/github/jonloucks/example/client/ClientFactoryImpl.java
@@ -0,0 +1,107 @@
+package io.github.jonloucks.example.client;
+
+import io.github.jonloucks.concurrency.api.Concurrency;
+import io.github.jonloucks.concurrency.api.ConcurrencyFactory;
+import io.github.jonloucks.concurrency.api.StateMachineFactory;
+import io.github.jonloucks.concurrency.api.WaitableFactory;
+import io.github.jonloucks.contracts.api.Contracts;
+import io.github.jonloucks.contracts.api.Promisor;
+import io.github.jonloucks.contracts.api.Repository;
+import io.github.jonloucks.example.client.Client.Config;
+import io.github.jonloucks.metalog.api.Metalog;
+import io.github.jonloucks.metalog.api.MetalogFactory;
+
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import static io.github.jonloucks.concurrency.api.GlobalConcurrency.findConcurrencyFactory;
+import static io.github.jonloucks.contracts.api.BindStrategy.IF_NOT_BOUND;
+import static io.github.jonloucks.contracts.api.Checks.*;
+import static io.github.jonloucks.contracts.api.GlobalContracts.lifeCycle;
+import static io.github.jonloucks.examples.common.Constants.WEATHER;
+import static io.github.jonloucks.metalog.api.GlobalMetalog.findMetalogFactory;
+
+public class ClientFactoryImpl implements ClientFactory {
+ @Override
+ public Client create(Config config) {
+ final Config validConfig = enhancedConfigCheck(config);
+ final Repository repository = validConfig.contracts().claim(Repository.FACTORY).get();
+
+ installConcurrency(validConfig, repository);
+ installMetalog(validConfig, repository);
+ installCore(validConfig, repository);
+
+ final Client server = new ClientImpl(validConfig, repository, true);
+ repository.keep(Client.CONTRACT, () -> server);
+ return server;
+ }
+
+ @Override
+ public Client create(Consumer builderConsumer) {
+ final Consumer validBuilderConsumer = builderConsumerCheck(builderConsumer);
+ final ConfigBuilderImpl configBuilder = new ConfigBuilderImpl();
+ validBuilderConsumer.accept(configBuilder);
+ return create(configBuilder);
+ }
+
+ @Override
+ public void install(Config config, Repository repository) {
+ final Config validConfig = enhancedConfigCheck(config);
+ final Repository validRepository = nullCheck(repository, "Repository must be present.");
+
+ installConcurrency(validConfig, validRepository);
+ installMetalog(validConfig, validRepository);
+ installCore(validConfig, validRepository);
+
+ final Promisor serverPromisor = lifeCycle(() -> new ClientImpl(validConfig, validRepository, false));
+
+ validRepository.keep(Client.CONTRACT, serverPromisor, IF_NOT_BOUND);
+ }
+
+ private void installConcurrency(Config config, Repository repository) {
+ final Concurrency.Config concurrencyConfig = new Concurrency.Config() {
+ @Override
+ public Contracts contracts() {
+ return config.contracts();
+ }
+ };
+ //noinspection ResultOfMethodCallIgnored
+ contractsCheck(concurrencyConfig.contracts());
+ final Optional optionalFactory = findConcurrencyFactory(concurrencyConfig);
+
+ optionalFactory.ifPresent(f -> f.install(concurrencyConfig, repository));
+ }
+
+ private void installMetalog(Config config, Repository repository) {
+ final Metalog.Config metalogConfig = new Metalog.Config() {
+ @Override
+ public Contracts contracts() {
+ return config.contracts();
+ }
+ };
+ //noinspection ResultOfMethodCallIgnored
+ contractsCheck(metalogConfig.contracts());
+ final Optional optionalFactory = findMetalogFactory(metalogConfig);
+
+ optionalFactory.ifPresent(f -> f.install(metalogConfig, repository));
+ }
+
+ private Config enhancedConfigCheck(Config config) {
+ final Config candidateConfig = configCheck(config);
+ final Contracts contracts = contractsCheck(candidateConfig.contracts());
+
+ if (contracts.isBound(Client.CONTRACT)) {
+ throw new ClientException("Client is already bound.");
+ }
+
+ return candidateConfig;
+ }
+
+ private void installCore(Config config, Repository repository) {
+ repository.require(Repository.FACTORY);
+ repository.require(WaitableFactory.CONTRACT);
+ repository.require(StateMachineFactory.CONTRACT);
+
+ repository.keep(WEATHER, () -> config.contracts().claim(Client.CONTRACT).getWeatherReport());
+ }
+}
diff --git a/client/src/main/java/io/github/jonloucks/example/client/ClientImpl.java b/client/src/main/java/io/github/jonloucks/example/client/ClientImpl.java
new file mode 100644
index 0000000..431917f
--- /dev/null
+++ b/client/src/main/java/io/github/jonloucks/example/client/ClientImpl.java
@@ -0,0 +1,90 @@
+package io.github.jonloucks.example.client;
+
+import io.github.jonloucks.concurrency.api.Idempotent;
+import io.github.jonloucks.concurrency.api.StateMachine;
+import io.github.jonloucks.concurrency.api.WaitableNotify;
+import io.github.jonloucks.contracts.api.AutoClose;
+import io.github.jonloucks.contracts.api.Repository;
+import io.github.jonloucks.examples.messages.weather.WeatherGrpc;
+import io.github.jonloucks.examples.messages.weather.WeatherOuterClass.WeatherReply;
+import io.github.jonloucks.examples.messages.weather.WeatherOuterClass.WeatherRequest;
+import io.grpc.*;
+import io.grpc.protobuf.services.HealthStatusManager;
+
+import java.util.concurrent.TimeUnit;
+
+import static io.github.jonloucks.concurrency.api.Idempotent.withClose;
+import static io.github.jonloucks.concurrency.api.Idempotent.withOpen;
+import static io.github.jonloucks.contracts.api.Checks.configCheck;
+import static io.github.jonloucks.contracts.api.Checks.nullCheck;
+import static io.github.jonloucks.metalog.api.GlobalMetalog.publish;
+import static java.util.Optional.ofNullable;
+
+final class ClientImpl implements Client {
+
+ @Override
+ public AutoClose open() {
+ return withOpen(stateMachine, this::realOpen);
+ }
+
+ @Override
+ public WaitableNotify lifeCycleNotify() {
+ return stateMachine;
+ }
+
+ @Override
+ public String getWeatherReport() {
+ try {
+ final WeatherRequest weatherRequest = WeatherRequest.newBuilder()
+ .addLocation("current")
+ .build();
+ final WeatherReply reply = blockingStub.sayWeather(weatherRequest);
+ return reply.getReport();
+ } catch (StatusRuntimeException thrown) {
+ publish(() -> "RPC failed: " + thrown.getStatus(), b -> b.thrown(thrown));
+ }
+ return null;
+ }
+
+ ClientImpl(Config config, Repository repository, boolean openRepository) {
+ this.config = configCheck(config);
+ final Repository validRepository = nullCheck(repository, "Repository must be present.");
+ this.closeRepository = openRepository ? validRepository.open() : AutoClose.NONE;
+ this.stateMachine = Idempotent.createStateMachine(config.contracts());
+ }
+
+ private AutoClose realOpen() {
+ final String target = config.hostname() + ":" + config.port();
+ channel = Grpc.newChannelBuilder(target, InsecureChannelCredentials.create())
+ .build();
+
+ blockingStub = WeatherGrpc.newBlockingStub(channel);
+
+ return this::exposedClose;
+ }
+
+ private void exposedClose() {
+ withClose(stateMachine, this::realClose);
+ }
+
+ private void realClose() {
+ ofNullable(channel).ifPresent(c -> {
+ c.shutdown();
+ try {
+ c.awaitTermination(config.shutdownTimeout().toMillis(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ });
+ closeRepository.close();
+ }
+
+ private final Config config;
+ private final AutoClose closeRepository;
+ private final StateMachine stateMachine;
+
+ private final HealthStatusManager health = new HealthStatusManager();
+
+ private ManagedChannel channel;
+ private WeatherGrpc.WeatherBlockingStub blockingStub;
+}
diff --git a/client/src/main/java/io/github/jonloucks/example/client/ConfigBuilderImpl.java b/client/src/main/java/io/github/jonloucks/example/client/ConfigBuilderImpl.java
new file mode 100644
index 0000000..f013359
--- /dev/null
+++ b/client/src/main/java/io/github/jonloucks/example/client/ConfigBuilderImpl.java
@@ -0,0 +1,63 @@
+package io.github.jonloucks.example.client;
+
+import io.github.jonloucks.contracts.api.Contracts;
+
+import java.time.Duration;
+
+import static io.github.jonloucks.contracts.api.Checks.contractsCheck;
+import static io.github.jonloucks.contracts.api.Checks.nullCheck;
+
+final class ConfigBuilderImpl implements Client.Config.Builder {
+
+ @Override
+ public Contracts contracts() {
+ return contracts;
+ }
+
+ @Override
+ public String hostname() {
+ return hostname;
+ }
+
+ @Override
+ public int port() {
+ return port;
+ }
+
+ @Override
+ public Duration shutdownTimeout() {
+ return shutdownTimeout;
+ }
+
+ @Override
+ public Builder contracts(Contracts contracts) {
+ this.contracts = contractsCheck(contracts);
+ return this;
+ }
+
+ @Override
+ public Builder port(int port) {
+ this.port = port;
+ return this;
+ }
+
+ @Override
+ public Builder hostname(String hostname) {
+ this.hostname = nullCheck(hostname, "Hostname must be present.");
+ return this;
+ }
+
+ @Override
+ public Builder shutdownTimeout(Duration timeout) {
+ this.shutdownTimeout = nullCheck(timeout, "Timeout must be present.");
+ return this;
+ }
+
+ ConfigBuilderImpl() {
+ }
+
+ private Contracts contracts;
+ private String hostname = Client.Config.DEFAULT.hostname();
+ private int port = Client.Config.DEFAULT.port();
+ private Duration shutdownTimeout = Client.Config.DEFAULT.shutdownTimeout();
+}
diff --git a/client/src/main/java/module-info.java b/client/src/main/java/module-info.java
new file mode 100644
index 0000000..85bf584
--- /dev/null
+++ b/client/src/main/java/module-info.java
@@ -0,0 +1,18 @@
+module io.github.jonloucks.example.client {
+ requires transitive io.github.jonloucks.contracts;
+ requires transitive io.github.jonloucks.concurrency;
+ requires transitive io.github.jonloucks.metalog;
+ requires transitive io.github.jonloucks.examples.messages;
+ requires transitive io.github.jonloucks.examples.common;
+ requires io.grpc.services;
+ requires io.grpc.stub;
+ requires io.grpc;
+
+ uses io.github.jonloucks.contracts.api.ContractsFactory;
+ uses io.github.jonloucks.concurrency.api.ConcurrencyFactory;
+ uses io.github.jonloucks.metalog.api.MetalogFactory;
+
+ provides io.github.jonloucks.example.client.ClientFactory with io.github.jonloucks.example.client.ClientFactoryImpl;
+
+ exports io.github.jonloucks.example.client;
+}
\ No newline at end of file
diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Common.java b/common/src/main/java/io/github/jonloucks/examples/common/Common.java
index d61c7d5..b2d8090 100644
--- a/common/src/main/java/io/github/jonloucks/examples/common/Common.java
+++ b/common/src/main/java/io/github/jonloucks/examples/common/Common.java
@@ -32,6 +32,8 @@ public static void install(Repository repository) {
final BindStrategy strategy = IF_ALLOWED;
+ repository.keep(WEATHER, () -> "Weather report unavailable.");
+
// Constant string, but could be changed to a localized value without changing uses
repository.keep(PROGRAM_NAME, () -> "Unnamed", strategy);
diff --git a/common/src/main/java/io/github/jonloucks/examples/common/Constants.java b/common/src/main/java/io/github/jonloucks/examples/common/Constants.java
index 2cfa672..0ba7fed 100644
--- a/common/src/main/java/io/github/jonloucks/examples/common/Constants.java
+++ b/common/src/main/java/io/github/jonloucks/examples/common/Constants.java
@@ -11,6 +11,11 @@
*/
public final class Constants {
+ /**
+ * Example of a simple contract
+ */
+ public static Contract WEATHER = Contract.create("Current Weather");
+
/**
* Constant string, but open for uses cases like localization.
*/
@@ -21,7 +26,6 @@ public final class Constants {
*/
public static final Contract PROGRAM = Contract.create("Program contract");
-
/**
* Main arguments
*/
diff --git a/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java b/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java
index f0929ad..b19cf48 100644
--- a/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java
+++ b/common/src/main/java/io/github/jonloucks/examples/common/ProgramImpl.java
@@ -35,7 +35,9 @@ public void runCommandLine(OnCompletion onCompletion) {
if (arguments.isEmpty()) {
runCommand("help", emptyList(), onCompletion);
} else {
- runCommand(arguments.get(0), arguments.subList(1, arguments.size()), onCompletion);
+ for (String argument : arguments) {
+ runCommand(argument, emptyList(), onCompletion);
+ }
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7df5a26..1b17775 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,7 +1,10 @@
[versions]
-contracts-version = "[2.5.1,3.0.0)"
-concurrency-version = "[1.3.0,2.0.0)"
-metalog-version = "[1.2.2,2.0.0)"
+contracts-version = "[2.5.2,3.0.0)"
+concurrency-version = "[1.3.1,2.0.0)"
+metalog-version = "[1.2.3,2.0.0)"
+protobuf-plugin-version = "0.9.5"
+grpc-version = "1.76.0"
+protobuf-version = "3.25.8"
[libraries]
contracts-api = { module = "io.github.jonloucks.contracts:contracts-api", version.ref = "contracts-version" }
@@ -13,3 +16,11 @@ concurrency = { module = "io.github.jonloucks.concurrency:concurrency", version.
metalog-api = { module = "io.github.jonloucks.metalog:metalog-api", version.ref = "metalog-version" }
metalog-test = { module = "io.github.jonloucks.metalog:metalog-test", version.ref = "metalog-version" }
metalog = { module = "io.github.jonloucks.metalog:metalog", version.ref = "metalog-version" }
+protoc = { module = "com.google.protobuf:protoc", version.ref = "protobuf-version" }
+grpc-protoc = { module = "io.grpc:protoc-gen-grpc-java", version.ref = "grpc-version"}
+grpc-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc-version"}
+grpc-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc-version"}
+grpc-services = { module = "io.grpc:grpc-services", version.ref = "grpc-version"}
+grpc-netty-shaded = { module = "io.grpc:grpc-netty-shaded", version.ref = "grpc-version"}
+grpc-testing = { module = "io.grpc:grpc-testing", version.ref = "grpc-version"}
+grpc-inprocess = { module = "io.grpc:grpc-inprocess", version.ref = "grpc-version"}
diff --git a/messages/build.gradle b/messages/build.gradle
new file mode 100644
index 0000000..1aca1e9
--- /dev/null
+++ b/messages/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'java-library'
+ id "com.google.protobuf" version libs.versions.protobuf.plugin.version
+}
+
+getTasks().withType(JavaCompile.class).configureEach {
+ getOptions().getRelease().set(9)
+}
+
+protobuf {
+ protoc {
+ artifact = libs.protoc.get()
+ }
+ plugins {
+ grpc {
+ artifact = libs.grpc.protoc.get()
+ }
+ }
+ generateProtoTasks {
+ all()*.plugins {
+ grpc {}
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+ mavenLocal()
+}
+
+dependencies {
+ implementation libs.grpc.protobuf
+ implementation libs.grpc.stub
+ implementation libs.grpc.services
+ runtimeOnly libs.grpc.netty.shaded
+
+ testImplementation libs.grpc.testing
+ testImplementation libs.grpc.inprocess
+}
+
+test {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/messages/src/main/java/module-info.java b/messages/src/main/java/module-info.java
new file mode 100644
index 0000000..410db59
--- /dev/null
+++ b/messages/src/main/java/module-info.java
@@ -0,0 +1,9 @@
+module io.github.jonloucks.examples.messages {
+ requires com.google.protobuf;
+ requires io.grpc;
+ requires io.grpc.stub;
+ requires io.grpc.protobuf;
+ requires com.google.common;
+
+ exports io.github.jonloucks.examples.messages.weather;
+}
\ No newline at end of file
diff --git a/messages/src/main/proto/examples/weather.proto b/messages/src/main/proto/examples/weather.proto
new file mode 100644
index 0000000..08f969b
--- /dev/null
+++ b/messages/src/main/proto/examples/weather.proto
@@ -0,0 +1,20 @@
+syntax = "proto3";
+
+package io.github.jonloucks.examples.messages.weather;
+
+// The weather service definition.
+service Weather {
+ // Sends a weather
+ rpc SayWeather (WeatherRequest) returns (WeatherReply);
+}
+
+message WeatherRequest {
+ string id = 1;
+ repeated string location = 2;
+}
+
+message WeatherReply {
+ string id = 1;
+ string location = 2;
+ string report = 4;
+}
\ No newline at end of file
diff --git a/server/build.gradle b/server/build.gradle
index d4d57b7..3e8b10c 100644
--- a/server/build.gradle
+++ b/server/build.gradle
@@ -15,10 +15,17 @@ dependencies {
api libs.contracts.api
api libs.concurrency.api
api libs.metalog.api
+ api project(":common")
implementation libs.contracts
implementation libs.concurrency
implementation libs.metalog
+ implementation project(":messages")
+
+ implementation libs.grpc.protobuf
+ implementation libs.grpc.stub
+ implementation libs.grpc.services
+ runtimeOnly libs.grpc.netty.shaded
testImplementation libs.contracts.test
testImplementation libs.concurrency.test
diff --git a/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java b/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java
index 06deb1d..dee5f48 100644
--- a/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java
+++ b/server/src/main/java/io/github/jonloucks/example/server/ConfigBuilderImpl.java
@@ -2,7 +2,10 @@
import io.github.jonloucks.contracts.api.Contracts;
+import java.time.Duration;
+
import static io.github.jonloucks.contracts.api.Checks.contractsCheck;
+import static io.github.jonloucks.contracts.api.Checks.nullCheck;
final class ConfigBuilderImpl implements Server.Config.Builder {
@@ -11,15 +14,39 @@ public Contracts contracts() {
return contracts;
}
+ @Override
+ public int port() {
+ return port;
+ }
+
+ @Override
+ public Duration shutdownTimeout() {
+ return shutdownTimeout;
+ }
+
@Override
public Builder contracts(Contracts contracts) {
this.contracts = contractsCheck(contracts);
return this;
}
+ @Override
+ public Builder port(int port) {
+ this.port = port;
+ return this;
+ }
+
+ @Override
+ public Builder shutdownTimeout(Duration timeout) {
+ this.shutdownTimeout = nullCheck(timeout, "Timeout must be present.");
+ return this;
+ }
+
ConfigBuilderImpl() {
}
private Contracts contracts;
+ private int port = Server.Config.DEFAULT.port();
+ private Duration shutdownTimeout = Server.Config.DEFAULT.shutdownTimeout();
}
diff --git a/server/src/main/java/io/github/jonloucks/example/server/Server.java b/server/src/main/java/io/github/jonloucks/example/server/Server.java
index 8211b4c..dd203fe 100644
--- a/server/src/main/java/io/github/jonloucks/example/server/Server.java
+++ b/server/src/main/java/io/github/jonloucks/example/server/Server.java
@@ -7,6 +7,7 @@
import io.github.jonloucks.contracts.api.Contracts;
import io.github.jonloucks.contracts.api.GlobalContracts;
+import java.time.Duration;
import java.util.function.Supplier;
public interface Server extends AutoOpen {
@@ -14,6 +15,10 @@ public interface Server extends AutoOpen {
WaitableNotify lifeCycleNotify();
+ Idempotent getLifeCycleState();
+
+ String getWeatherReport();
+
interface Config {
/**
* The default configuration used when creating a new Server instance
@@ -55,10 +60,22 @@ default Class extends ServerFactory> serviceLoaderClass() {
return ServerFactory.class;
}
+ default int port() {
+ return 50052;
+ }
+
+ default Duration shutdownTimeout() {
+ return Duration.ofSeconds(60);
+ }
+
interface Builder extends Config {
Contract> FACTORY = Contract.create("Server Config Builder Factory");
Builder contracts(Contracts contracts);
+
+ Builder port(int port);
+
+ Builder shutdownTimeout(Duration shutdownTimeout);
}
}
}
diff --git a/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java b/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java
index 47b497a..62b2d1b 100644
--- a/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java
+++ b/server/src/main/java/io/github/jonloucks/example/server/ServerImpl.java
@@ -5,11 +5,20 @@
import io.github.jonloucks.concurrency.api.WaitableNotify;
import io.github.jonloucks.contracts.api.AutoClose;
import io.github.jonloucks.contracts.api.Repository;
+import io.grpc.Grpc;
+import io.grpc.InsecureServerCredentials;
+import io.grpc.health.v1.HealthCheckResponse;
+import io.grpc.protobuf.services.HealthStatusManager;
+import io.grpc.protobuf.services.ProtoReflectionServiceV1;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
import static io.github.jonloucks.concurrency.api.Idempotent.withClose;
import static io.github.jonloucks.concurrency.api.Idempotent.withOpen;
import static io.github.jonloucks.contracts.api.Checks.configCheck;
import static io.github.jonloucks.contracts.api.Checks.nullCheck;
+import static java.util.Optional.ofNullable;
final class ServerImpl implements Server {
@@ -23,6 +32,16 @@ public WaitableNotify lifeCycleNotify() {
return stateMachine;
}
+ @Override
+ public Idempotent getLifeCycleState() {
+ return stateMachine.getState();
+ }
+
+ @Override
+ public String getWeatherReport() {
+ return "";
+ }
+
ServerImpl(Server.Config config, Repository repository, boolean openRepository) {
this.config = configCheck(config);
final Repository validRepository = nullCheck(repository, "Repository must be present.");
@@ -31,6 +50,18 @@ public WaitableNotify lifeCycleNotify() {
}
private AutoClose realOpen() {
+ try {
+ grpcServer = Grpc.newServerBuilderForPort(config.port(), InsecureServerCredentials.create())
+ .addService(new WeatherServiceImpl())
+ .addService(ProtoReflectionServiceV1.newInstance())
+ .addService(health.getHealthService())
+ .build()
+ .start();
+ health.setStatus("", HealthCheckResponse.ServingStatus.SERVING);
+ } catch (IOException e) {
+ throw new ServerException("Failed to start server.", e);
+ }
+
return this::exposedClose;
}
@@ -39,10 +70,22 @@ private void exposedClose() {
}
private void realClose() {
+ ofNullable(grpcServer).ifPresent(x -> {
+ x.shutdown();
+ try {
+ x.awaitTermination(config.shutdownTimeout().toMillis(), TimeUnit.MILLISECONDS);
+ } catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ x.shutdownNow();
+ });
closeRepository.close();
}
private final Server.Config config;
private final AutoClose closeRepository;
private final StateMachine stateMachine;
+
+ private final HealthStatusManager health = new HealthStatusManager();
+ private io.grpc.Server grpcServer;
}
diff --git a/server/src/main/java/io/github/jonloucks/example/server/WeatherServiceImpl.java b/server/src/main/java/io/github/jonloucks/example/server/WeatherServiceImpl.java
new file mode 100644
index 0000000..e872cf6
--- /dev/null
+++ b/server/src/main/java/io/github/jonloucks/example/server/WeatherServiceImpl.java
@@ -0,0 +1,23 @@
+package io.github.jonloucks.example.server;
+
+import io.github.jonloucks.examples.messages.weather.WeatherOuterClass.WeatherReply;
+import io.github.jonloucks.examples.messages.weather.WeatherOuterClass.WeatherRequest;
+import io.grpc.stub.StreamObserver;
+
+import static io.github.jonloucks.examples.messages.weather.WeatherGrpc.*;
+
+final class WeatherServiceImpl extends WeatherImplBase {
+
+ @Override
+ public void sayWeather(WeatherRequest request, StreamObserver responseObserver) {
+
+ // Generate a greeting message for the original method
+ WeatherReply reply = WeatherReply.newBuilder().setReport("Mostly Sunny").build();
+
+ // Send the reply back to the client.
+ responseObserver.onNext(reply);
+
+ // Indicate that no further messages will be sent to the client.
+ responseObserver.onCompleted();
+ }
+}
diff --git a/server/src/main/java/module-info.java b/server/src/main/java/module-info.java
index 4c2f498..888ceb1 100644
--- a/server/src/main/java/module-info.java
+++ b/server/src/main/java/module-info.java
@@ -2,6 +2,11 @@
requires transitive io.github.jonloucks.contracts;
requires transitive io.github.jonloucks.concurrency;
requires transitive io.github.jonloucks.metalog;
+ requires transitive io.github.jonloucks.examples.messages;
+ requires transitive io.github.jonloucks.examples.common;
+ requires io.grpc.services;
+ requires io.grpc.stub;
+ requires io.grpc;
uses io.github.jonloucks.contracts.api.ContractsFactory;
uses io.github.jonloucks.concurrency.api.ConcurrencyFactory;
diff --git a/settings.gradle b/settings.gradle
index ff37d8e..c30fce9 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,4 +1,6 @@
rootProject.name = 'examples'
+include 'messages'
include 'common'
+include 'server'
-include 'server'
\ No newline at end of file
+include 'client'
\ No newline at end of file
diff --git a/src/main/java/io/github/jonloucks/examples/ClientCommand.java b/src/main/java/io/github/jonloucks/examples/ClientCommand.java
new file mode 100644
index 0000000..2361d9d
--- /dev/null
+++ b/src/main/java/io/github/jonloucks/examples/ClientCommand.java
@@ -0,0 +1,29 @@
+package io.github.jonloucks.examples;
+
+import io.github.jonloucks.example.client.Client;
+import io.github.jonloucks.examples.common.Command;
+
+import java.util.List;
+
+import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract;
+import static io.github.jonloucks.examples.common.Constants.WEATHER;
+
+final class ClientCommand implements Command {
+ @Override
+ public String execute(List arguments) {
+ final Client client = claimContract(Client.CONTRACT);
+ final String report1 = client.getWeatherReport();
+ final String report2 = claimContract(WEATHER);
+
+ if (report1.equals(report2)) {
+ return report1;
+ } else {
+ return "Conflicting reports : " + report1 + " and " + report2;
+ }
+ }
+
+ @Override
+ public String getName() {
+ return "client";
+ }
+}
diff --git a/src/main/java/io/github/jonloucks/examples/Main.java b/src/main/java/io/github/jonloucks/examples/Main.java
index e705ac3..386e2f3 100644
--- a/src/main/java/io/github/jonloucks/examples/Main.java
+++ b/src/main/java/io/github/jonloucks/examples/Main.java
@@ -2,6 +2,9 @@
import io.github.jonloucks.contracts.api.AutoClose;
import io.github.jonloucks.contracts.api.Repository;
+import io.github.jonloucks.example.client.Client;
+import io.github.jonloucks.example.client.ClientFactory;
+import io.github.jonloucks.example.client.ClientFactoryFinder;
import io.github.jonloucks.example.server.Server;
import io.github.jonloucks.example.server.ServerFactory;
import io.github.jonloucks.example.server.ServerFactoryFinder;
@@ -9,6 +12,7 @@
import io.github.jonloucks.examples.common.Program;
import java.util.Arrays;
+import java.util.function.IntConsumer;
import static io.github.jonloucks.contracts.api.BindStrategy.ALWAYS;
import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract;
@@ -17,6 +21,17 @@
public final class Main {
+ /**
+ * For testing the smoke application
+ * @param consumer for the exit code
+ * @return the previous system exit method
+ */
+ public static IntConsumer setSystemExit(IntConsumer consumer) {
+ final IntConsumer save = SYSTEM_EXIT;
+ SYSTEM_EXIT = consumer;
+ return save;
+ }
+
/**
* Main entry point.
* Note. Entry points are where final decisions on dependency inversions are made.
@@ -25,21 +40,28 @@ public final class Main {
* @param args the command line arguments
*/
public static void main(String[] args) {
+ try {
+ innerMain(args);
+ SYSTEM_EXIT.accept(0);
+ } catch (Exception thrown) {
+ System.err.println(thrown.getMessage());
+ SYSTEM_EXIT.accept(1);
+ }
+ }
+
+ private static void innerMain(String[] args) {
final Repository repository = createMyRepository(args);
try (AutoClose closeRepository = repository.open()) {
final Program program = claimContract(PROGRAM);
installCommand(program);
- program.runCommandLine( c -> {});
+ program.runCommandLine(c -> {});
waitForQuitting();
- System.exit(0);
- } catch (Exception thrown) {
- System.err.println(thrown.getMessage());
- System.exit(1);
}
}
private static void installCommand(Program program) {
program.keepCommand(new ServerCommand());
+ program.keepCommand(new ClientCommand());
}
/**
@@ -60,6 +82,7 @@ private static Repository createMyRepository(String[] args) {
Common.install(repository);
installServer(repository);
+ installClient(repository);
// Save the command line for later use
repository.keep(PROGRAM_ARGUMENTS, () -> Arrays.asList(args), ALWAYS);
@@ -76,7 +99,15 @@ private static void installServer(Repository repository) {
serverFactory.install(Server.Config.DEFAULT, repository);
}
+ private static void installClient(Repository repository) {
+ final ClientFactoryFinder finder = new ClientFactoryFinder(Client.Config.DEFAULT);
+ final ClientFactory serverFactory = finder.get();
+ serverFactory.install(Client.Config.DEFAULT, repository);
+ }
+
private Main() {
}
+
+ private static IntConsumer SYSTEM_EXIT = System::exit;
}
diff --git a/src/main/java/io/github/jonloucks/examples/ServerCommand.java b/src/main/java/io/github/jonloucks/examples/ServerCommand.java
index aa1a6c0..160d750 100644
--- a/src/main/java/io/github/jonloucks/examples/ServerCommand.java
+++ b/src/main/java/io/github/jonloucks/examples/ServerCommand.java
@@ -2,20 +2,17 @@
import io.github.jonloucks.example.server.Server;
import io.github.jonloucks.examples.common.Command;
-import io.github.jonloucks.examples.common.Common;
import java.util.List;
-import static io.github.jonloucks.concurrency.api.Idempotent.CLOSED;
import static io.github.jonloucks.contracts.api.GlobalContracts.claimContract;
final class ServerCommand implements Command {
@Override
public String execute(List arguments) {
final Server server = claimContract(Server.CONTRACT);
- //noinspection resource, scope is full life of server
- server.lifeCycleNotify().notifyIf(s -> s == CLOSED, s -> Common.setQuitting());
- return "Server started";
+
+ return "Server is " + (server.getLifeCycleState().isRejecting() ? "rejecting requests" : "excepting requests");
}
@Override
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 1f5de32..31072d3 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -4,11 +4,14 @@
requires transitive io.github.jonloucks.metalog;
requires transitive io.github.jonloucks.examples.common;
requires transitive io.github.jonloucks.example.server;
-
+ requires transitive io.github.jonloucks.example.client;
+ requires transitive io.github.jonloucks.examples.messages;
+
uses io.github.jonloucks.contracts.api.ContractsFactory;
uses io.github.jonloucks.concurrency.api.ConcurrencyFactory;
uses io.github.jonloucks.metalog.api.MetalogFactory;
uses io.github.jonloucks.example.server.ServerFactory;
+ uses io.github.jonloucks.example.client.ClientFactory;
exports io.github.jonloucks.examples;
}
\ No newline at end of file