├── CHANGES.md ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── buildViaTravis.sh ├── qiro-core ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── qiro │ │ │ ├── failures │ │ │ ├── Retryable.java │ │ │ ├── Exception.java │ │ │ ├── Exceptions.java │ │ │ ├── FailFastFactory.java │ │ │ └── FailureAccrualDetector.java │ │ │ ├── ThrowableFunction.java │ │ │ ├── util │ │ │ ├── Clock.java │ │ │ ├── Timer.java │ │ │ ├── EmptySubscriber.java │ │ │ ├── Backoff.java │ │ │ ├── HashwheelTimer.java │ │ │ ├── Availabilities.java │ │ │ └── RestartablePublisher.java │ │ │ ├── codec │ │ │ ├── Codec.java │ │ │ └── UTF8Codec.java │ │ │ ├── loadbalancing │ │ │ ├── loadestimator │ │ │ │ ├── LoadEstimator.java │ │ │ │ ├── MsgCountLoadEstimator.java │ │ │ │ ├── NullEstimator.java │ │ │ │ └── PeakEwmaLoadEstimator.java │ │ │ ├── selector │ │ │ │ ├── Selector.java │ │ │ │ ├── RoundRobin.java │ │ │ │ ├── Random.java │ │ │ │ ├── LinearMin.java │ │ │ │ └── P2C.java │ │ │ ├── RoundRobinBalancer.java │ │ │ ├── P2CBalancer.java │ │ │ ├── LeastLoadedBalancer.java │ │ │ ├── WeightedServiceFactory.java │ │ │ └── BalancerFactory.java │ │ │ ├── ServiceFactory.java │ │ │ ├── Server.java │ │ │ ├── ServiceFactoryProxy.java │ │ │ ├── resolver │ │ │ ├── Resolver.java │ │ │ ├── TransportConnector.java │ │ │ ├── DnsResolver.java │ │ │ └── Resolvers.java │ │ │ ├── Service.java │ │ │ ├── ServiceProxy.java │ │ │ ├── Filters.java │ │ │ ├── filter │ │ │ ├── StatsFilter.java │ │ │ ├── TimeoutFilter.java │ │ │ └── RetryFilter.java │ │ │ ├── builder │ │ │ ├── ServerBuilder.java │ │ │ └── ClientBuilder.java │ │ │ ├── ServiceFactories.java │ │ │ ├── Services.java │ │ │ ├── pool │ │ │ ├── SingletonPool.java │ │ │ └── WatermarkPool.java │ │ │ ├── Filter.java │ │ │ └── FactoryToService.java │ ├── test │ │ └── java │ │ │ └── io │ │ │ └── qiro │ │ │ ├── testing │ │ │ ├── FakeClock.java │ │ │ ├── PublishersTest.java │ │ │ ├── LoggerSubscriber.java │ │ │ ├── DelayFilter.java │ │ │ ├── FakeTimer.java │ │ │ ├── ThreadedService.java │ │ │ └── TestingService.java │ │ │ ├── FactoryToServiceTest.java │ │ │ ├── filter │ │ │ ├── StatsFilterTest.java │ │ │ ├── RetryFilterTest.java │ │ │ └── TimeoutFilterTest.java │ │ │ ├── pool │ │ │ ├── SingletonPoolTest.java │ │ │ └── WatermarkPoolTest.java │ │ │ ├── ConcurrencyTest.java │ │ │ ├── failures │ │ │ ├── FailFastFactoryTest.java │ │ │ └── FailureAccrualDetectorTest.java │ │ │ ├── ResolverTest.java │ │ │ ├── FilterTest.java │ │ │ ├── LoadBalancerTest.java │ │ │ └── ServiceTest.java │ └── perf │ │ └── java │ │ └── io │ │ └── qiro │ │ └── README.md └── build.gradle ├── qiro-reactivesocket ├── src │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── qiro │ │ │ └── reactivesocket │ │ │ ├── ClientBuilder.java │ │ │ ├── ReactiveSocketService.java │ │ │ ├── ReactiveSocketTransportConnector.java │ │ │ └── ServerBuilder.java │ └── test │ │ └── java │ │ └── io │ │ └── qiro │ │ └── reactivesocket │ │ └── ReactiveSocketServiceTest.java └── build.gradle ├── settings.gradle ├── qiro-http ├── build.gradle └── src │ ├── main │ └── java │ │ └── io │ │ └── qiro │ │ └── http │ │ ├── RxNettyResponse.java │ │ ├── NettyTransportConnector.java │ │ └── RxNettyService.java │ └── test │ └── java │ └── io │ └── qiro │ └── http │ └── SimpleHttpClientTest.java ├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── gradlew.bat ├── .travis.yml └── gradlew /CHANGES.md: -------------------------------------------------------------------------------- 1 | # ReactiveSocket Releases # 2 | 3 | No releases yet. -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Netflix-Skunkworks/qiro/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/failures/Retryable.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | public class Retryable extends Exception {} 4 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/ThrowableFunction.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | @FunctionalInterface 4 | public interface ThrowableFunction { 5 | U apply(T t) throws Throwable; 6 | } 7 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/Clock.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | @FunctionalInterface 4 | public interface Clock { 5 | public static Clock SYSTEM_CLOCK = System::currentTimeMillis; 6 | 7 | long nowMs(); 8 | } 9 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/codec/Codec.java: -------------------------------------------------------------------------------- 1 | package io.qiro.codec; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | public interface Codec { 6 | ByteBuffer encode(Req request); 7 | Resp decode(ByteBuffer serializedData); 8 | } 9 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/failures/Exception.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | public class Exception extends Throwable { 4 | @Override 5 | public synchronized Throwable fillInStackTrace() { 6 | return this; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Sep 24 22:35:45 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/loadestimator/LoadEstimator.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.loadestimator; 2 | 3 | public interface LoadEstimator { 4 | double load(long now); 5 | long start(); 6 | void end(long ts); 7 | 8 | default double load() { 9 | return load(System.nanoTime()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/selector/Selector.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.selector; 2 | 3 | import io.qiro.ServiceFactory; 4 | import io.qiro.loadbalancing.WeightedServiceFactory; 5 | 6 | import java.util.List; 7 | import java.util.function.Function; 8 | 9 | public interface Selector extends 10 | Function>, ServiceFactory> { 11 | } 12 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/ServiceFactory.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | public interface ServiceFactory { 6 | // TODO: add a client connection ? 7 | Publisher> apply(); 8 | double availability(); 9 | Publisher close(); 10 | 11 | default Service toService() { 12 | return new FactoryToService<>(this); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/FakeClock.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import io.qiro.util.Clock; 4 | 5 | public class FakeClock implements Clock { 6 | private long time; 7 | 8 | public FakeClock(long epoch) { 9 | this.time = epoch; 10 | } 11 | 12 | public void advance(long howMuch) { 13 | time += howMuch; 14 | } 15 | 16 | @Override 17 | public long nowMs() { 18 | return time; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/Timer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | public interface Timer { 6 | public interface TimerTask { 7 | void cancel(); 8 | boolean isCancel(); 9 | } 10 | 11 | TimerTask schedule(Runnable task, long delay, TimeUnit unit); 12 | 13 | default TimerTask schedule(Runnable task, long delayMs) { 14 | return schedule(task, delayMs, TimeUnit.MILLISECONDS); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/Server.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | import java.net.SocketAddress; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | public interface Server { 9 | 10 | SocketAddress boundAddress(); 11 | 12 | Publisher await(); 13 | 14 | Publisher close(long gracePeriod, TimeUnit unit); 15 | 16 | default Publisher close() { 17 | return close(0, TimeUnit.NANOSECONDS); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/failures/Exceptions.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | public class Exceptions { 4 | public static boolean isRetryable(Throwable t) { 5 | // TODO: to be extended 6 | if (t instanceof Retryable) { 7 | return true; 8 | } else { 9 | return false; 10 | } 11 | } 12 | 13 | public static boolean isApplicativeError(Throwable t) { 14 | // TODO: to be extended 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/loadestimator/MsgCountLoadEstimator.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.loadestimator; 2 | 3 | public class MsgCountLoadEstimator implements LoadEstimator { 4 | private int count = 0; 5 | 6 | @Override 7 | public double load(long now) { 8 | return (double) count; 9 | } 10 | 11 | @Override 12 | public long start() { 13 | count += 1; 14 | return 0; 15 | } 16 | 17 | @Override 18 | public void end(long ts) { 19 | count -= 1; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /qiro-reactivesocket/src/main/java/io/qiro/reactivesocket/ClientBuilder.java: -------------------------------------------------------------------------------- 1 | package io.qiro.reactivesocket; 2 | 3 | import io.qiro.codec.Codec; 4 | import io.qiro.resolver.TransportConnector; 5 | 6 | public class ClientBuilder extends io.qiro.builder.ClientBuilder { 7 | 8 | public static ClientBuilder get() { 9 | return new ClientBuilder<>(); 10 | } 11 | 12 | @Override 13 | public TransportConnector connector(Codec codec) { 14 | return new ReactiveSocketTransportConnector<>(codec); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/loadestimator/NullEstimator.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.loadestimator; 2 | 3 | import java.util.function.Supplier; 4 | 5 | public class NullEstimator implements LoadEstimator { 6 | public static LoadEstimator INSTANCE = new NullEstimator(); 7 | public static Supplier SUPPLIER = NullEstimator::new; 8 | 9 | @Override 10 | public double load(long now) { 11 | return 0.0; 12 | } 13 | 14 | @Override 15 | public long start() { 16 | return 0L; 17 | } 18 | 19 | @Override 20 | public void end(long ts) { 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/EmptySubscriber.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | import org.reactivestreams.Subscriber; 4 | import org.reactivestreams.Subscription; 5 | 6 | public class EmptySubscriber implements Subscriber { 7 | public static Subscriber INSTANCE = new EmptySubscriber<>(); 8 | 9 | @Override 10 | public void onSubscribe(Subscription s) { 11 | s.request(Long.MAX_VALUE); 12 | } 13 | 14 | @Override 15 | public void onNext(T t) {} 16 | 17 | @Override 18 | public void onError(Throwable t) {} 19 | 20 | @Override 21 | public void onComplete() {} 22 | } 23 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/PublishersTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import static io.qiro.util.Publishers.concat; 9 | import static io.qiro.util.Publishers.just; 10 | import static io.qiro.util.Publishers.toList; 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public class PublishersTest { 14 | @Test(timeout = 10_000L) 15 | public void testPublishersConcat() throws Exception { 16 | List integers = toList(concat(just(1), just(2))); 17 | assertEquals(Arrays.asList(1,2), integers); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/Backoff.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | public class Backoff { 4 | private final double multiplier; 5 | private final int max; 6 | private double backoff; 7 | 8 | public Backoff(double init, double multiplier, int max){ 9 | this.multiplier = multiplier; 10 | this.max = max; 11 | this.backoff = init; 12 | } 13 | 14 | public int nextBackoff() { 15 | if (backoff >= max) { 16 | return max; 17 | } else { 18 | int result = (int) Math.floor(backoff); 19 | backoff *= multiplier; 20 | return result; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/codec/UTF8Codec.java: -------------------------------------------------------------------------------- 1 | package io.qiro.codec; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | import static java.nio.charset.StandardCharsets.UTF_8; 6 | 7 | public class UTF8Codec implements Codec { 8 | public static UTF8Codec INSTANCE = new UTF8Codec(); 9 | 10 | @Override 11 | public ByteBuffer encode(String str) { 12 | return ByteBuffer.wrap(str.getBytes(UTF_8)); 13 | } 14 | 15 | @Override 16 | public String decode(ByteBuffer serializedData) { 17 | byte[] buffer = new byte[serializedData.capacity()]; 18 | serializedData.get(buffer); 19 | return new String(buffer, UTF_8); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/FactoryToServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | public class FactoryToServiceTest { 8 | @Test(timeout = 10_000L) 9 | public void testBasicServiceFactory() throws InterruptedException { 10 | 11 | ServiceFactory factory = ServiceFactories.fromFunctions( 12 | ServiceTest.DummyService::new, 13 | () -> { 14 | assertTrue("You shouldn't close the ServiceFactory", false); 15 | return null; 16 | } 17 | ); 18 | 19 | ServiceTest.testService(factory.toService()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /qiro-core/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | dependencies { 18 | compile 'io.netty:netty-buffer:4.1.0.Beta7' 19 | } 20 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/filter/StatsFilterTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.Services; 5 | import org.junit.Test; 6 | 7 | import java.util.List; 8 | 9 | import static io.qiro.util.Publishers.from; 10 | import static io.qiro.util.Publishers.toList; 11 | 12 | public class StatsFilterTest { 13 | 14 | @Test(timeout = 1000L) 15 | public void testStatsFilter() throws InterruptedException { 16 | StatsFilter statsFilter = new StatsFilter<>(); 17 | Service toStringService = 18 | statsFilter.andThen(Services.fromFunction(Object::toString)); 19 | 20 | List strings = toList(toStringService.requestChannel(from(1, 2, 3))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/selector/RoundRobin.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.selector; 2 | 3 | import io.qiro.ServiceFactory; 4 | import io.qiro.loadbalancing.WeightedServiceFactory; 5 | 6 | import java.util.List; 7 | 8 | public class RoundRobin implements Selector { 9 | private int i = 0; 10 | 11 | @Override 12 | public ServiceFactory apply(List> factories) { 13 | int n = factories.size(); 14 | if (n == 0) { 15 | throw new IllegalStateException("Empty Factory List"); 16 | } else { 17 | ServiceFactory factory = factories.get(i % n); 18 | i += 1; 19 | return factory; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | rootProject.name='qiro' 18 | include 'qiro-core' 19 | include 'qiro-reactivesocket' 20 | include 'qiro-http' 21 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/ServiceFactoryProxy.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | public class ServiceFactoryProxy implements ServiceFactory { 6 | protected ServiceFactory underlying; 7 | 8 | public ServiceFactoryProxy(ServiceFactory underlying) { 9 | this.underlying = underlying; 10 | } 11 | @Override 12 | public Publisher> apply() { 13 | return underlying.apply(); 14 | } 15 | 16 | @Override 17 | public double availability() { 18 | return underlying.availability(); 19 | } 20 | 21 | @Override 22 | public Publisher close() { 23 | return underlying.close(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /qiro-http/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | dependencies { 18 | compile project(':qiro-core') 19 | compile 'io.reactivex:rxnetty-http:0.5.0-SNAPSHOT' 20 | } 21 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/selector/Random.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.selector; 2 | 3 | import io.qiro.ServiceFactory; 4 | import io.qiro.loadbalancing.WeightedServiceFactory; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.ThreadLocalRandom; 8 | 9 | public class Random implements Selector 10 | { 11 | public static Random INSTANCE = new Random<>(); 12 | 13 | @Override 14 | public ServiceFactory apply(List> factories) { 15 | int n = factories.size(); 16 | if (n == 0) { 17 | throw new IllegalStateException("Empty Factory List"); 18 | } else { 19 | ThreadLocalRandom rng = ThreadLocalRandom.current(); 20 | return factories.get(rng.nextInt(n)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /qiro-reactivesocket/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | dependencies { 18 | compile project(':qiro-core') 19 | compile 'io.reactivesocket:reactivesocket:0.0.2-SNAPSHOT' 20 | compile 'io.reactivesocket:reactivesocket-websocket-rxnetty:0.0.1-SNAPSHOT' 21 | } 22 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/resolver/Resolver.java: -------------------------------------------------------------------------------- 1 | package io.qiro.resolver; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | 6 | import org.reactivestreams.Publisher; 7 | 8 | import java.net.SocketAddress; 9 | import java.net.URL; 10 | import java.util.Set; 11 | import java.util.function.Function; 12 | 13 | /** 14 | * A Resolver is responsible for converting an abstract name into a more meaningful 15 | * list of socket addresses. 16 | * 17 | * This process is asynchronous and the resolution may change over time. 18 | */ 19 | public interface Resolver { 20 | public Publisher> resolve(String url); 21 | 22 | default 23 | Publisher>> resolveFactory( 24 | String url, 25 | TransportConnector connector 26 | ) { 27 | return Resolvers.resolveFactory(resolve(url), connector); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/Service.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | import static io.qiro.util.Publishers.just; 6 | import static io.qiro.util.Publishers.map; 7 | 8 | public interface Service { 9 | default Publisher fireAndForget(Req request) { 10 | return subscriber -> map(requestResponse(request), x -> null); 11 | } 12 | 13 | default Publisher requestResponse(Req request) { 14 | return requestStream(request); 15 | } 16 | 17 | default Publisher requestStream(Req request) { 18 | return requestSubscription(request); 19 | } 20 | 21 | 22 | default Publisher requestSubscription(Req request) { 23 | return requestChannel(just(request)); 24 | } 25 | 26 | public Publisher requestChannel(Publisher inputs); 27 | 28 | public double availability(); 29 | public Publisher close(); 30 | } 31 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/HashwheelTimer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | import io.netty.util.Timeout; 4 | 5 | import java.util.concurrent.TimeUnit; 6 | 7 | public class HashwheelTimer implements Timer { 8 | public static Timer INSTANCE = new HashwheelTimer(); 9 | 10 | private io.netty.util.HashedWheelTimer netty = new io.netty.util.HashedWheelTimer(); 11 | 12 | @Override 13 | public TimerTask schedule(Runnable task, long delay, TimeUnit unit) { 14 | Timeout timeout = netty.newTimeout(to -> { 15 | if (!to.isCancelled()) { 16 | task.run(); 17 | } 18 | }, delay, unit); 19 | 20 | return new TimerTask() { 21 | @Override 22 | public void cancel() { 23 | timeout.cancel(); 24 | } 25 | 26 | @Override 27 | public boolean isCancel() { 28 | return timeout.isCancelled(); 29 | } 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/RoundRobinBalancer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.loadbalancing.selector.RoundRobin; 6 | import org.reactivestreams.Publisher; 7 | 8 | import java.util.Set; 9 | 10 | public class RoundRobinBalancer implements ServiceFactory { 11 | private final BalancerFactory balancer; 12 | 13 | public RoundRobinBalancer(Publisher>> factorySet) { 14 | RoundRobin selector = new RoundRobin<>(); 15 | balancer = new BalancerFactory<>(factorySet, selector); 16 | } 17 | 18 | @Override 19 | public Publisher> apply() { 20 | return balancer.apply(); 21 | } 22 | 23 | @Override 24 | public synchronized double availability() { 25 | return balancer.availability(); 26 | } 27 | 28 | @Override 29 | public Publisher close() { 30 | return balancer.close(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/selector/LinearMin.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.selector; 2 | 3 | import io.qiro.ServiceFactory; 4 | import io.qiro.loadbalancing.WeightedServiceFactory; 5 | 6 | import java.util.List; 7 | 8 | public class LinearMin implements Selector { 9 | @Override 10 | public ServiceFactory apply(List> factories) { 11 | int n = factories.size(); 12 | if (n == 0) { 13 | throw new IllegalStateException("Empty Factory List"); 14 | } else { 15 | WeightedServiceFactory leastLoaded = factories.get(0); 16 | for(WeightedServiceFactory factory: factories) { 17 | if (factory.getLoad() < leastLoaded.getLoad()) { 18 | leastLoaded = factory; 19 | } 20 | } 21 | System.out.println("Selecting SF " + leastLoaded + " load=" + leastLoaded.getLoad()); 22 | return leastLoaded; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/resolver/TransportConnector.java: -------------------------------------------------------------------------------- 1 | package io.qiro.resolver; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import org.reactivestreams.Publisher; 6 | 7 | import java.net.SocketAddress; 8 | import java.util.function.Function; 9 | 10 | public interface TransportConnector 11 | extends Function>> { 12 | 13 | default ServiceFactory toFactory(SocketAddress address) { 14 | return new ServiceFactory() { 15 | @Override 16 | public Publisher> apply() { 17 | return TransportConnector.this.apply(address); 18 | } 19 | 20 | @Override 21 | public double availability() { 22 | return 1.0; 23 | } 24 | 25 | @Override 26 | public Publisher close() { 27 | return s -> { 28 | s.onNext(null); 29 | s.onComplete(); 30 | }; 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs and databases # 24 | ###################### 25 | *.log 26 | 27 | # OS generated files # 28 | ###################### 29 | .DS_Store* 30 | ehthumbs.db 31 | Icon? 32 | Thumbs.db 33 | 34 | # Editor Files # 35 | ################ 36 | *~ 37 | *.swp 38 | 39 | # Gradle Files # 40 | ################ 41 | .gradle 42 | .gradletasknamecache 43 | .m2 44 | 45 | # Build output directies 46 | target/ 47 | build/ 48 | 49 | # IntelliJ specific files/directories 50 | out 51 | .idea 52 | *.ipr 53 | *.iws 54 | *.iml 55 | atlassian-ide-plugin.xml 56 | 57 | # Eclipse specific files/directories 58 | .classpath 59 | .project 60 | .settings 61 | .metadata 62 | bin/ 63 | 64 | # NetBeans specific files/directories 65 | .nbattrs 66 | /.nb-gradle/profiles/private/ 67 | .nb-gradle-properties 68 | 69 | # Scala build 70 | *.cache 71 | /.nb-gradle/private/ 72 | -------------------------------------------------------------------------------- /gradle/buildViaTravis.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script will build the project. 3 | 4 | if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then 5 | echo -e "Build Pull Request #$TRAVIS_PULL_REQUEST => Branch [$TRAVIS_BRANCH]" 6 | ./gradlew -Prelease.useLastTag=true build 7 | elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ]; then 8 | echo -e 'Build Branch with Snapshot => Branch ['$TRAVIS_BRANCH']' 9 | ./gradlew -Prelease.travisci=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" build snapshot --stacktrace 10 | elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ]; then 11 | echo -e 'Build Branch for Release => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG']' 12 | ./gradlew -Prelease.travisci=true -Prelease.useLastTag=true -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" final --stacktrace 13 | else 14 | echo -e 'WARN: Should not be here => Branch ['$TRAVIS_BRANCH'] Tag ['$TRAVIS_TAG'] Pull Request ['$TRAVIS_PULL_REQUEST']' 15 | ./gradlew -Prelease.useLastTag=true build 16 | fi 17 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/LoggerSubscriber.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import org.reactivestreams.Subscriber; 4 | import org.reactivestreams.Subscription; 5 | 6 | public class LoggerSubscriber implements Subscriber { 7 | private String name; 8 | private boolean completed; 9 | private boolean error; 10 | 11 | public LoggerSubscriber(String name) { 12 | this.name = name; 13 | this.completed = false; 14 | this.error = false; 15 | } 16 | 17 | @Override 18 | public void onSubscribe(Subscription s) { 19 | s.request(Long.MAX_VALUE); 20 | } 21 | 22 | @Override 23 | public void onNext(T s) { 24 | System.out.println(name + " received " + s); 25 | } 26 | 27 | @Override 28 | public void onError(Throwable t) { 29 | System.out.println(name + " received exception " + t); 30 | error = true; 31 | } 32 | 33 | @Override 34 | public void onComplete() { 35 | System.out.println(name + " is complete!"); 36 | completed = true; 37 | } 38 | 39 | public boolean isComplete() { 40 | return completed; 41 | } 42 | 43 | public boolean isError() { 44 | return error; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/P2CBalancer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.loadbalancing.loadestimator.LoadEstimator; 6 | import io.qiro.loadbalancing.loadestimator.PeakEwmaLoadEstimator; 7 | import io.qiro.loadbalancing.selector.P2C; 8 | import io.qiro.loadbalancing.selector.Selector; 9 | import org.reactivestreams.Publisher; 10 | 11 | import java.util.*; 12 | import java.util.function.Supplier; 13 | 14 | public class P2CBalancer implements ServiceFactory { 15 | private final BalancerFactory balancer; 16 | 17 | public P2CBalancer(Publisher>> factories) { 18 | Selector p2c = new P2C<>(); 19 | Supplier peakEwma = PeakEwmaLoadEstimator::new; 20 | balancer = new BalancerFactory<>(factories, p2c, peakEwma); 21 | } 22 | 23 | @Override 24 | public Publisher> apply() { 25 | return balancer.apply(); 26 | } 27 | 28 | @Override 29 | public synchronized double availability() { 30 | return balancer.availability(); 31 | } 32 | 33 | @Override 34 | public Publisher close() { 35 | return balancer.close(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/ServiceProxy.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | public class ServiceProxy implements Service { 6 | protected Service underlying; 7 | 8 | public ServiceProxy(Service underlying) { 9 | this.underlying = underlying; 10 | } 11 | 12 | @Override 13 | public Publisher fireAndForget(Req request) { 14 | return underlying.fireAndForget(request); 15 | } 16 | 17 | @Override 18 | public Publisher requestResponse(Req request) { 19 | return underlying.requestResponse(request); 20 | } 21 | 22 | @Override 23 | public Publisher requestStream(Req request) { 24 | return underlying.requestStream(request); 25 | } 26 | 27 | @Override 28 | public Publisher requestSubscription(Req request) { 29 | return underlying.requestSubscription(request); 30 | } 31 | 32 | @Override 33 | public Publisher requestChannel(Publisher inputs) { 34 | return underlying.requestChannel(inputs); 35 | } 36 | 37 | @Override 38 | public double availability() { 39 | return underlying.availability(); 40 | } 41 | 42 | @Override 43 | public Publisher close() { 44 | return underlying.close(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ReactiveSocket 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request (on a branch other than `master`, `0.x`, `1.x`, or `gh-pages`). 4 | 5 | When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 6 | 7 | ## License 8 | 9 | By contributing your code, you agree to license your contribution under the terms of the APLv2: https://github.com/ReactiveSocket/reactivesocket-java/blob/master/LICENSE 10 | 11 | All files are released with the Apache 2.0 license. 12 | 13 | If you are adding a new file it should have a header like this: 14 | 15 | ``` 16 | /** 17 | * Copyright 2015 Netflix, Inc. 18 | * 19 | * Licensed under the Apache License, Version 2.0 (the "License"); 20 | * you may not use this file except in compliance with the License. 21 | * You may obtain a copy of the License at 22 | * 23 | * http://www.apache.org/licenses/LICENSE-2.0 24 | * 25 | * Unless required by applicable law or agreed to in writing, software 26 | * distributed under the License is distributed on an "AS IS" BASIS, 27 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 28 | * See the License for the specific language governing permissions and 29 | * limitations under the License. 30 | */ 31 | ``` 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Qiro 2 | 3 | Qiro is an agnostic communication library supporting multiple interaction models 4 | (fire-and-forget, request-response, request-stream, subscription, channel). 5 | 6 | ## Build and Binaries 7 | 8 | 9 | 10 | Snapshots are available via JFrog. 11 | 12 | Example: 13 | 14 | ```groovy 15 | repositories { 16 | maven { url 'https://oss.jfrog.org/libs-snapshot' } 17 | } 18 | 19 | dependencies { 20 | compile 'io.qiro:qiro-core:0.0.1-SNAPSHOT' 21 | } 22 | ``` 23 | 24 | No releases to Maven Central or JCenter have occurred yet. 25 | 26 | 27 | ## Bugs and Feedback 28 | 29 | For bugs, questions and discussions please use the 30 | [Github Issues](https://github.com/qiro/qiro/issues). 31 | 32 | 33 | ## LICENSE 34 | 35 | Copyright 2015 Netflix, Inc. 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and 47 | limitations under the License. 48 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/LeastLoadedBalancer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.loadbalancing.loadestimator.LoadEstimator; 6 | import io.qiro.loadbalancing.loadestimator.MsgCountLoadEstimator; 7 | import io.qiro.loadbalancing.loadestimator.PeakEwmaLoadEstimator; 8 | import io.qiro.loadbalancing.selector.LinearMin; 9 | import io.qiro.loadbalancing.selector.P2C; 10 | import io.qiro.loadbalancing.selector.Selector; 11 | import org.reactivestreams.Publisher; 12 | 13 | import java.util.Set; 14 | import java.util.function.Supplier; 15 | 16 | public class LeastLoadedBalancer implements ServiceFactory { 17 | private final BalancerFactory balancer; 18 | 19 | public LeastLoadedBalancer(Publisher>> factories) { 20 | Selector linearMin = new LinearMin<>(); 21 | Supplier msgCount = MsgCountLoadEstimator::new; 22 | balancer = new BalancerFactory<>(factories, linearMin, msgCount); 23 | } 24 | 25 | @Override 26 | public Publisher> apply() { 27 | return balancer.apply(); 28 | } 29 | 30 | @Override 31 | public synchronized double availability() { 32 | return balancer.availability(); 33 | } 34 | 35 | @Override 36 | public Publisher close() { 37 | return balancer.close(); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /qiro-core/src/perf/java/io/qiro/README.md: -------------------------------------------------------------------------------- 1 | # JMH Benchmarks 2 | 3 | ### Run All 4 | 5 | ``` 6 | ./gradlew benchmarks 7 | ``` 8 | 9 | ### Run Specific Class 10 | 11 | ``` 12 | ./gradlew benchmarks '-Pjmh=.*FramePerf.*' 13 | ``` 14 | 15 | ### Arguments 16 | 17 | Optionally pass arguments for custom execution. Example: 18 | 19 | ``` 20 | ./gradlew benchmarks '-Pjmh=-f 1 -tu s -bm thrpt -wi 5 -i 5 -r 1 .*FramePerf.*' 21 | ``` 22 | 23 | gives output like this: 24 | 25 | ``` 26 | # Warmup Iteration 1: 12699094.396 ops/s 27 | # Warmup Iteration 2: 15101768.843 ops/s 28 | # Warmup Iteration 3: 14991750.686 ops/s 29 | # Warmup Iteration 4: 14819319.785 ops/s 30 | # Warmup Iteration 5: 14856301.193 ops/s 31 | Iteration 1: 14910334.272 ops/s 32 | Iteration 2: 14954589.540 ops/s 33 | Iteration 3: 15076277.267 ops/s 34 | Iteration 4: 14833413.303 ops/s 35 | Iteration 5: 14893188.328 ops/s 36 | 37 | 38 | Result "encodeNextCompleteHello": 39 | 14933560.542 ±(99.9%) 349800.467 ops/s [Average] 40 | (min, avg, max) = (14833413.303, 14933560.542, 15076277.267), stdev = 90842.071 41 | CI (99.9%): [14583760.075, 15283361.009] (assumes normal distribution) 42 | 43 | 44 | # Run complete. Total time: 00:00:10 45 | 46 | Benchmark Mode Cnt Score Error Units 47 | FramePerf.encodeNextCompleteHello thrpt 5 14933560.542 ± 349800.467 ops/s 48 | ``` 49 | 50 | To see all options: 51 | 52 | ``` 53 | ./gradlew benchmarks '-Pjmh=-h' 54 | ``` 55 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/pool/SingletonPoolTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.pool; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactories; 5 | import io.qiro.ServiceFactory; 6 | import io.qiro.testing.LoggerSubscriber; 7 | import io.qiro.testing.TestingService; 8 | import org.junit.Test; 9 | 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static io.qiro.util.Publishers.just; 13 | import static junit.framework.TestCase.assertTrue; 14 | 15 | public class SingletonPoolTest { 16 | @Test(timeout = 100_000L) 17 | public void testSingletonPool() throws InterruptedException { 18 | TestingService testingService = new TestingService<>(Object::toString); 19 | AtomicInteger serviceCreated = new AtomicInteger(0); 20 | ServiceFactory factory = ServiceFactories.fromFunctions( 21 | () -> { 22 | if (serviceCreated.getAndIncrement() == 1) { 23 | assertTrue("Shouldn't create more than one service!", false); 24 | } 25 | return testingService; 26 | }, 27 | () -> null 28 | ); 29 | Service service = new SingletonPool<>(factory).toService(); 30 | 31 | service.requestResponse(0).subscribe(new LoggerSubscriber<>("request 0")); 32 | service.requestResponse(1).subscribe(new LoggerSubscriber<>("request 1")); 33 | testingService.respond(); 34 | testingService.complete(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/Availabilities.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.Subscription; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.List; 12 | import java.util.function.Supplier; 13 | 14 | public class Availabilities { 15 | 16 | public static 17 | double avgOfServices(Collection> services) { 18 | if (services.isEmpty()) { 19 | return 0.0; 20 | } else { 21 | double sum = 0.0; 22 | int count = 0; 23 | for (Service svc : services) { 24 | sum += svc.availability(); 25 | count += 1; 26 | } 27 | if (count != 0) { 28 | return sum / count; 29 | } else { 30 | return 0.0; 31 | } 32 | } 33 | } 34 | 35 | public static 36 | double avgOfServiceFactories( 37 | Collection> factories 38 | ) { 39 | if (factories.isEmpty()) { 40 | return 0.0; 41 | } else { 42 | double sum = 0.0; 43 | int count = 0; 44 | for (ServiceFactory svc : factories) { 45 | sum += svc.availability(); 46 | count += 1; 47 | } 48 | if (count != 0) { 49 | return sum / count; 50 | } else { 51 | return 0.0; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/selector/P2C.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.selector; 2 | 3 | import io.qiro.ServiceFactory; 4 | import io.qiro.loadbalancing.WeightedServiceFactory; 5 | 6 | import java.util.List; 7 | import java.util.Random; 8 | import java.util.concurrent.ThreadLocalRandom; 9 | 10 | public class P2C implements Selector { 11 | private final int effort; 12 | 13 | public P2C() { 14 | this(5); 15 | } 16 | 17 | public P2C(int effort) { 18 | this.effort = effort; 19 | } 20 | 21 | @Override 22 | public ServiceFactory apply(List> factories) { 23 | int n = factories.size(); 24 | if (n == 0) { 25 | throw new IllegalStateException("Empty Factory List"); 26 | } else if (n == 1) { 27 | return factories.get(0); 28 | } else { 29 | int i = 0; 30 | int a = 0; 31 | int b = 0; 32 | Random rng = ThreadLocalRandom.current(); 33 | while (i < effort) { 34 | a = rng.nextInt(n); 35 | b = rng.nextInt(n - 1); 36 | if (b >= a) { 37 | b = b + 1; 38 | } 39 | if (factories.get(a).availability() != 0.0 40 | && factories.get(b).availability() != 0.0) { 41 | break; 42 | } 43 | i += 1; 44 | } 45 | 46 | double aLoad = factories.get(a).getLoad(); 47 | double bLoad = factories.get(b).getLoad(); 48 | if (aLoad < bLoad) { 49 | return factories.get(a); 50 | } else { 51 | return factories.get(b); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/ConcurrencyTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import io.qiro.testing.ThreadedService; 4 | import io.qiro.util.Publishers; 5 | import org.junit.Test; 6 | import org.reactivestreams.Publisher; 7 | 8 | import java.util.List; 9 | import java.util.concurrent.CountDownLatch; 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.Executors; 12 | 13 | import static io.qiro.util.Publishers.toList; 14 | import static org.junit.Assert.assertEquals; 15 | 16 | public class ConcurrencyTest { 17 | 18 | @Test(timeout = 10_000L) 19 | public void testConcurrency() throws InterruptedException { 20 | 21 | List integers = toList(Publishers.range(0, 200)); 22 | assertEquals(200, integers.size()); 23 | 24 | Filter toLowerCase = 25 | Filters.fromOutputFunction(String::toLowerCase); 26 | 27 | Service service = ServiceFactories.fromFunctions( 28 | () -> toLowerCase.andThen(new ThreadedService<>(Object::toString)), 29 | () -> null 30 | ).toService(); 31 | 32 | 33 | ExecutorService executor = Executors.newFixedThreadPool(64); 34 | int concurrency = 1000; 35 | CountDownLatch c = new CountDownLatch(concurrency); 36 | for (int i = 0; i < concurrency; i++) { 37 | executor.submit(() -> { 38 | Publisher inputs = Publishers.range(0, 1000); 39 | try { 40 | toList(service.requestChannel(inputs)); 41 | } catch (InterruptedException e) { 42 | e.printStackTrace(); 43 | } 44 | c.countDown(); 45 | }); 46 | } 47 | c.await(); 48 | System.out.println("All done!"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/DelayFilter.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import io.qiro.Filter; 4 | import io.qiro.Service; 5 | import io.qiro.util.Timer; 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.ScheduledExecutorService; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | public class DelayFilter implements Filter { 15 | private long delayMs; 16 | private Timer timer; 17 | 18 | public DelayFilter(long delayMs, Timer timer) { 19 | this.delayMs = delayMs; 20 | this.timer = timer; 21 | } 22 | 23 | private void doLater(Runnable runnable) { 24 | timer.schedule(runnable, delayMs, TimeUnit.MILLISECONDS); 25 | } 26 | 27 | @Override 28 | public Publisher requestChannel(Publisher inputs, Service service) { 29 | Publisher delayedInputs = subscriber -> 30 | inputs.subscribe(new Subscriber() { 31 | @Override 32 | public void onSubscribe(Subscription s) { 33 | subscriber.onSubscribe(s); 34 | } 35 | 36 | @Override 37 | public void onNext(Req req) { 38 | doLater(() -> subscriber.onNext(req)); 39 | } 40 | 41 | @Override 42 | public void onError(Throwable t) { 43 | doLater(() -> subscriber.onError(t)); 44 | } 45 | 46 | @Override 47 | public void onComplete() { 48 | doLater(() -> subscriber.onComplete()); 49 | } 50 | }); 51 | 52 | return service.requestChannel(delayedInputs); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/FakeTimer.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import io.qiro.util.Timer; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.TreeMap; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | public class FakeTimer implements Timer { 12 | private TreeMap> entries = new TreeMap<>(); 13 | private long time = 0L; 14 | 15 | public synchronized void advance(long delta, TimeUnit unit) { 16 | time += TimeUnit.NANOSECONDS.convert(delta, unit); 17 | Map.Entry> entry = entries.firstEntry(); 18 | while (entry != null && entry.getKey() < time) { 19 | entry.getValue().forEach(Runnable::run); 20 | entries.pollFirstEntry(); 21 | entry = entries.firstEntry(); 22 | } 23 | } 24 | 25 | public synchronized void advance() { 26 | Map.Entry> entry = entries.firstEntry(); 27 | if (entry != null) { 28 | time += entry.getKey(); 29 | entry.getValue().forEach(Runnable::run); 30 | } 31 | } 32 | 33 | @Override 34 | public synchronized TimerTask schedule(Runnable task, long delay, TimeUnit unit) { 35 | long t = time + TimeUnit.NANOSECONDS.convert(delay, unit); 36 | List runnables = entries.get(t); 37 | if (runnables == null) { 38 | runnables = new ArrayList<>(); 39 | } 40 | runnables.add(task); 41 | entries.put(t, runnables); 42 | return new TimerTask() { 43 | @Override 44 | public void cancel() { 45 | if (t < time) { 46 | entries.remove(t); 47 | } 48 | } 49 | 50 | @Override 51 | public boolean isCancel() { 52 | return false; 53 | } 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/Filters.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | 5 | import static io.qiro.util.Publishers.map; 6 | 7 | public class Filters { 8 | 9 | public static 10 | Filter fromInputFunction( 11 | ThrowableFunction fnIn 12 | ) { 13 | return fromFunction(fnIn, x -> x); 14 | } 15 | 16 | public static 17 | Filter fromOutputFunction( 18 | ThrowableFunction fnOut 19 | ) { 20 | return fromFunction(x -> x, fnOut); 21 | } 22 | 23 | public static 24 | Filter fromFunction( 25 | ThrowableFunction fnIn, 26 | ThrowableFunction fnOut 27 | ) { 28 | return new Filter() { 29 | @Override 30 | public Publisher requestSubscription(FilterReq input, Service service) { 31 | return s -> { 32 | try { 33 | fnIn.apply(input); 34 | s.onComplete(); 35 | } catch (Throwable throwable) { 36 | s.onError(throwable); 37 | } 38 | }; 39 | } 40 | 41 | @Override 42 | public Publisher requestChannel( 43 | Publisher inputs, 44 | Service service 45 | ) { 46 | Publisher requests = map(inputs, fnIn); 47 | Publisher outputs = service.requestChannel(requests); 48 | return map(outputs, fnOut); 49 | } 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /qiro-reactivesocket/src/main/java/io/qiro/reactivesocket/ReactiveSocketService.java: -------------------------------------------------------------------------------- 1 | package io.qiro.reactivesocket; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.codec.Codec; 5 | import io.qiro.util.Publishers; 6 | import io.reactivesocket.Payload; 7 | import io.reactivesocket.ReactiveSocket; 8 | import org.reactivestreams.Publisher; 9 | 10 | import java.nio.ByteBuffer; 11 | 12 | public class ReactiveSocketService implements Service { 13 | private static ByteBuffer EMPTY = ByteBuffer.allocate(0); 14 | 15 | private ReactiveSocket reactiveSocket; 16 | private Codec codec; 17 | 18 | public ReactiveSocketService(ReactiveSocket reactiveSocket, Codec codec) { 19 | this.reactiveSocket = reactiveSocket; 20 | this.codec = codec; 21 | } 22 | 23 | @Override 24 | public Publisher requestChannel(Publisher inputs) { 25 | Publisher payloadInputs = Publishers.map(inputs, 26 | req -> new Payload() { 27 | @Override 28 | public ByteBuffer getData() { 29 | return codec.encode(req); 30 | } 31 | 32 | @Override 33 | public ByteBuffer getMetadata() { 34 | return EMPTY; 35 | } 36 | }); 37 | 38 | Publisher payloadOutput = 39 | reactiveSocket.requestChannel(payloadInputs); 40 | 41 | return Publishers.map(payloadOutput, 42 | payload -> codec.decode(payload.getData())); 43 | } 44 | 45 | @Override 46 | public double availability() { 47 | return reactiveSocket.availability(); 48 | } 49 | 50 | @Override 51 | public Publisher close() { 52 | return s -> { 53 | try { 54 | reactiveSocket.close(); 55 | s.onComplete(); 56 | } catch (Exception e) { 57 | s.onError(e); 58 | } 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/filter/StatsFilter.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Filter; 4 | import io.qiro.Service; 5 | import org.reactivestreams.Publisher; 6 | import org.reactivestreams.Subscriber; 7 | import org.reactivestreams.Subscription; 8 | 9 | public class StatsFilter implements Filter { 10 | // TODO: add logger, trace recorder, or equivalent 11 | public StatsFilter() { 12 | 13 | } 14 | 15 | @Override 16 | public Publisher requestChannel(Publisher inputs, Service service) { 17 | return new Publisher() { 18 | private Long epoch = -1L; 19 | 20 | @Override 21 | public void subscribe(Subscriber responseSubscriber) { 22 | epoch = System.currentTimeMillis(); 23 | service.requestChannel(inputs).subscribe(new Subscriber() { 24 | @Override 25 | public void onSubscribe(Subscription s) { 26 | responseSubscriber.onSubscribe(s); 27 | } 28 | 29 | @Override 30 | public void onNext(Resp response) { 31 | System.out.println("Resp: +1"); 32 | responseSubscriber.onNext(response); 33 | } 34 | 35 | @Override 36 | public void onError(Throwable t) { 37 | System.out.println("Error: +1"); 38 | responseSubscriber.onError(t); 39 | } 40 | 41 | @Override 42 | public void onComplete() { 43 | long latency = System.currentTimeMillis() - epoch; 44 | System.out.println("Success: +1"); 45 | System.out.println("Latency: " + latency); 46 | responseSubscriber.onComplete(); 47 | } 48 | }); 49 | } 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/filter/RetryFilterTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Filter; 4 | import io.qiro.Filters; 5 | import io.qiro.Service; 6 | import io.qiro.Services; 7 | import io.qiro.failures.*; 8 | import io.qiro.failures.Exception; 9 | import org.junit.Test; 10 | 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | import static io.qiro.util.Publishers.from; 15 | import static io.qiro.util.Publishers.toList; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | public class RetryFilterTest { 19 | 20 | @Test(timeout = 10_000L) 21 | public void testRetryFilterOnRetryable() 22 | throws InterruptedException { 23 | testRetryFilter(new Retryable(), true); 24 | } 25 | 26 | @Test(timeout = 10_000L) 27 | public void testRetryFilterOnNonRetryable() 28 | throws InterruptedException { 29 | testRetryFilter(new Exception(), false); 30 | } 31 | 32 | private void testRetryFilter(io.qiro.failures.Exception ex, boolean expectResponse) 33 | throws InterruptedException { 34 | AtomicInteger i = new AtomicInteger(0); 35 | Filter failFirstFilter = 36 | Filters.fromOutputFunction(x -> { 37 | if (i.getAndIncrement() == 0) { 38 | throw ex; 39 | } else { 40 | return "OK: " + x; 41 | } 42 | }); 43 | 44 | Service service = new RetryFilter(1) 45 | .andThen(failFirstFilter) 46 | .andThen(Services.fromFunction(Object::toString)); 47 | 48 | List strings = toList(service.requestChannel(from(1, 2, 3))); 49 | 50 | if (expectResponse) { 51 | assertTrue(strings.size() == 3); 52 | assertTrue(strings.get(0).equals("OK: 1")); 53 | } else { 54 | assertTrue(strings.size() == 2); 55 | assertTrue(strings.get(0).equals("OK: 2")); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /qiro-http/src/main/java/io/qiro/http/RxNettyResponse.java: -------------------------------------------------------------------------------- 1 | package io.qiro.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.DecoderResult; 5 | import io.netty.handler.codec.http.HttpHeaders; 6 | import io.netty.handler.codec.http.HttpResponse; 7 | import io.netty.handler.codec.http.HttpResponseStatus; 8 | import io.netty.handler.codec.http.HttpVersion; 9 | import io.reactivex.netty.protocol.http.client.HttpClientResponse; 10 | 11 | class RxNettyResponse implements HttpResponse { 12 | private final HttpClientResponse response; 13 | 14 | private RxNettyResponse(HttpClientResponse response) { 15 | this.response = response; 16 | } 17 | 18 | public static RxNettyResponse wrap(HttpClientResponse response) { 19 | return new RxNettyResponse(response); 20 | } 21 | 22 | @Override 23 | public DecoderResult decoderResult() { 24 | throw new IllegalAccessError(); 25 | } 26 | 27 | @Override 28 | public void setDecoderResult(DecoderResult result) { 29 | throw new IllegalAccessError(); 30 | } 31 | 32 | @Override 33 | public DecoderResult getDecoderResult() { 34 | throw new IllegalAccessError(); 35 | } 36 | 37 | @Override 38 | public HttpResponseStatus getStatus() { 39 | return status(); 40 | } 41 | 42 | @Override 43 | public HttpResponseStatus status() { 44 | return response.getStatus(); 45 | } 46 | 47 | @Override 48 | public HttpResponse setStatus(HttpResponseStatus status) { 49 | throw new IllegalAccessError(); 50 | } 51 | 52 | @Override 53 | public HttpVersion getProtocolVersion() { 54 | return protocolVersion(); 55 | } 56 | 57 | @Override 58 | public HttpVersion protocolVersion() { 59 | return response.getHttpVersion(); 60 | } 61 | 62 | @Override 63 | public HttpResponse setProtocolVersion(HttpVersion version) { 64 | throw new IllegalAccessError(); 65 | } 66 | 67 | @Override 68 | public HttpHeaders headers() { 69 | throw new IllegalAccessError(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/filter/TimeoutFilterTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.Services; 5 | import io.qiro.testing.DelayFilter; 6 | import io.qiro.testing.FakeTimer; 7 | import io.qiro.util.EmptySubscriber; 8 | import org.junit.Test; 9 | 10 | import java.util.concurrent.CountDownLatch; 11 | 12 | import static io.qiro.util.Publishers.just; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public class TimeoutFilterTest { 16 | 17 | @Test(timeout = 5_000L) 18 | public void testTimeoutCase() throws InterruptedException { 19 | testTimeoutFilter(100, 50, true); 20 | } 21 | 22 | @Test(timeout = 5_000L) 23 | public void testNormalCase() throws InterruptedException { 24 | testTimeoutFilter(50, 100, false); 25 | } 26 | 27 | private void testTimeoutFilter(long delayMs, long timeoutMs, boolean mustFail) throws InterruptedException { 28 | System.out.println("TimeoutFilterTest delay: " + delayMs 29 | + ", timeout: " + timeoutMs + ", expecting failure: " + mustFail); 30 | 31 | FakeTimer timer = new FakeTimer(); 32 | Service timeoutService = 33 | new TimeoutFilter(timeoutMs, timer) 34 | .andThen(new DelayFilter<>(delayMs, timer)) 35 | .andThen(Services.fromFunction(Object::toString)); 36 | 37 | CountDownLatch latch = new CountDownLatch(1); 38 | timeoutService.requestResponse(1).subscribe(new EmptySubscriber() { 39 | @Override 40 | public void onNext(String s) { 41 | if (mustFail) { 42 | assertTrue("Shouldn't receive onNext event", false); 43 | } 44 | latch.countDown(); 45 | } 46 | 47 | @Override 48 | public void onError(Throwable t) { 49 | if (!mustFail) { 50 | assertTrue("Shouldn't receive onError event", false); 51 | } 52 | latch.countDown(); 53 | } 54 | }); 55 | timer.advance(); 56 | latch.await(); 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/ThreadedService.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import io.qiro.Service; 4 | import org.reactivestreams.Publisher; 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | import java.util.function.Function; 11 | 12 | public class ThreadedService implements Service { 13 | private static ExecutorService EXECUTOR = Executors.newFixedThreadPool(8, runnable -> { 14 | Thread thread = new Thread(runnable); 15 | thread.setDaemon(true); 16 | return thread; 17 | }); 18 | 19 | private Function function; 20 | 21 | public ThreadedService(Function function) { 22 | this.function = function; 23 | } 24 | 25 | @Override 26 | public Publisher requestChannel(Publisher inputs) { 27 | return subscriber -> 28 | EXECUTOR.submit(() -> { 29 | inputs.subscribe(new Subscriber() { 30 | @Override 31 | public void onSubscribe(Subscription s) { 32 | subscriber.onSubscribe(s); 33 | } 34 | 35 | @Override 36 | public void onNext(Req req) { 37 | Resp resp = function.apply(req); 38 | subscriber.onNext(resp); 39 | } 40 | 41 | @Override 42 | public void onError(Throwable t) { 43 | subscriber.onError(t); 44 | } 45 | 46 | @Override 47 | public void onComplete() { 48 | subscriber.onComplete(); 49 | } 50 | }); 51 | }); 52 | } 53 | 54 | @Override 55 | public double availability() { 56 | return 1.0; 57 | } 58 | 59 | @Override 60 | public Publisher close() { 61 | return s -> { 62 | s.onNext(null); 63 | s.onComplete(); 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/loadestimator/PeakEwmaLoadEstimator.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing.loadestimator; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | /** 6 | * Inspired by Finagle loadbalancer algorithm 7 | */ 8 | public class PeakEwmaLoadEstimator implements LoadEstimator { 9 | private static final double PENALTY = Integer.MAX_VALUE / 2.0; 10 | private static final double TAU = (double) TimeUnit.NANOSECONDS.convert(15, TimeUnit.SECONDS); 11 | 12 | private final long epoch = System.nanoTime(); 13 | private long stamp = epoch; // last timestamp in nanos we observed an rtt 14 | private int pending = 0; // instantaneous rate 15 | private double cost = 0.0; // ewma of rtt, sensitive to peaks. 16 | 17 | public synchronized double load(long now) { 18 | // update our view of the decay on `cost` 19 | observe(0.0); 20 | 21 | // If we don't have any latency history, we penalize the host on 22 | // the first probe. Otherwise, we factor in our current rate 23 | // assuming we were to schedule an additional request. 24 | if (cost == 0.0 && pending != 0) { 25 | return PENALTY + pending; 26 | } else { 27 | return cost * (pending+1); 28 | } 29 | } 30 | 31 | // Calculate the exponential weighted moving average of our 32 | // round trip time. It isn't exactly an ewma, but rather a 33 | // "peak-ewma", since `cost` is hyper-sensitive to latency peaks. 34 | // Note, because the frequency of observations represents an 35 | // unevenly spaced time-series[1], we consider the time between 36 | // observations when calculating our weight. 37 | // [1] http://www.eckner.com/papers/ts_alg.pdf 38 | private void observe(double rtt) { 39 | long t = System.nanoTime(); 40 | long td = Math.max(t - stamp, 0L); 41 | double w = Math.exp(-td / TAU); 42 | if (rtt > cost) { 43 | cost = rtt; 44 | } else { 45 | cost = cost*w + rtt*(1.0 - w); 46 | } 47 | stamp = t; 48 | } 49 | 50 | public synchronized long start() { 51 | pending += 1; 52 | return System.nanoTime(); 53 | } 54 | 55 | public synchronized void end(long ts) { 56 | long rtt = Math.max(System.nanoTime() - ts, 0L); 57 | pending -= 1; 58 | observe((double)rtt); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/failures/FailFastFactoryTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.Services; 6 | import io.qiro.testing.FakeTimer; 7 | import io.qiro.util.EmptySubscriber; 8 | import org.junit.Test; 9 | import org.reactivestreams.Publisher; 10 | 11 | import java.util.concurrent.atomic.AtomicBoolean; 12 | 13 | import static org.junit.Assert.*; 14 | 15 | public class FailFastFactoryTest { 16 | 17 | @Test(timeout = 10_000L) 18 | public void testFailFastFactory() throws InterruptedException { 19 | 20 | AtomicBoolean factoryUp = new AtomicBoolean(false); 21 | 22 | ServiceFactory factory = new ServiceFactory() { 23 | @Override 24 | public Publisher> apply() { 25 | return subscriber -> { 26 | if (factoryUp.get()) { 27 | Service service = Services.fromFunction(Object::toString); 28 | subscriber.onNext(service); 29 | } else { 30 | subscriber.onError(new Exception()); 31 | } 32 | }; 33 | } 34 | 35 | @Override 36 | public double availability() { 37 | return 1.0; 38 | } 39 | 40 | @Override 41 | public Publisher close() { 42 | return s -> { 43 | s.onNext(null); 44 | s.onComplete(); 45 | }; 46 | } 47 | }; 48 | FakeTimer timer = new FakeTimer(); 49 | 50 | Service service = new FailFastFactory<>(factory, timer).toService(); 51 | Double availability = service.availability(); 52 | assertTrue(availability == 1.0); 53 | 54 | service.requestResponse(1).subscribe(EmptySubscriber.INSTANCE); 55 | availability = service.availability(); 56 | assertTrue(availability == 0.0); 57 | 58 | service.requestResponse(1).subscribe(EmptySubscriber.INSTANCE); 59 | availability = service.availability(); 60 | assertTrue(availability == 0.0); 61 | 62 | factoryUp.set(true); 63 | timer.advance(); 64 | 65 | availability = service.availability(); 66 | assertTrue(availability == 1.0); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/failures/FailureAccrualDetectorTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | import io.qiro.*; 4 | import io.qiro.failures.FailureAccrualDetector; 5 | import io.qiro.testing.FakeClock; 6 | import io.qiro.util.EmptySubscriber; 7 | import org.junit.Test; 8 | 9 | import java.util.concurrent.TimeUnit; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static io.qiro.util.Publishers.just; 13 | import static io.qiro.util.Publishers.toSingle; 14 | import static org.junit.Assert.assertTrue; 15 | 16 | public class FailureAccrualDetectorTest { 17 | @Test(timeout = 100000L) 18 | public void testFailureDetector() throws InterruptedException { 19 | 20 | AtomicInteger i = new AtomicInteger(0); 21 | ServiceFactory factory = 22 | ServiceFactories.fromFunctions( 23 | () -> Services.fromFunction(x -> { 24 | if (i.getAndIncrement() < 2) { 25 | throw new io.qiro.failures.Exception(); 26 | } else { 27 | return "OK: " + x; 28 | } 29 | }), 30 | () -> null 31 | ); 32 | 33 | FakeClock fakeClock = new FakeClock(0L); 34 | ServiceFactory myFactory = 35 | new FailureAccrualDetector<>(factory, 5, 100, TimeUnit.MILLISECONDS, fakeClock); 36 | Service service = toSingle(myFactory.apply()); 37 | 38 | // 1 success for the service factory application 39 | // 1 failure for the service application 40 | // failures: 1, successes: 1 ===> ratio = 1 / 2 = 0.5 41 | service.requestResponse(1).subscribe(EmptySubscriber.INSTANCE); 42 | Double availability = myFactory.availability(); 43 | assertTrue(availability - 0.5 < 0.01); 44 | 45 | // failures: 2, successes: 1 ===> ratio = 1 / 3 = 0.333 46 | service.requestResponse(2).subscribe(EmptySubscriber.INSTANCE); 47 | availability = myFactory.availability(); 48 | assertTrue(availability - 0.333 < 0.01); 49 | 50 | // failures: 2, successes: 2 ===> ratio = 2 / 4 = 0.5 51 | service.requestResponse(3).subscribe(EmptySubscriber.INSTANCE); 52 | availability = myFactory.availability(); 53 | assertTrue(availability - 0.5 < 0.01); 54 | 55 | // old values are expired 56 | fakeClock.advance(1000); 57 | availability = myFactory.availability(); 58 | assertTrue(availability == 1.0); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /qiro-http/src/main/java/io/qiro/http/NettyTransportConnector.java: -------------------------------------------------------------------------------- 1 | package io.qiro.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelPipeline; 5 | import io.netty.handler.codec.http.HttpClientCodec; 6 | import io.netty.handler.codec.http.HttpRequest; 7 | import io.netty.handler.codec.http.HttpResponse; 8 | import io.netty.handler.logging.LogLevel; 9 | import io.qiro.Service; 10 | import io.qiro.resolver.TransportConnector; 11 | import io.reactivex.netty.channel.Connection; 12 | import io.reactivex.netty.client.ConnectionProvider; 13 | import io.reactivex.netty.protocol.http.HttpHandlerNames; 14 | import io.reactivex.netty.protocol.http.client.HttpClient; 15 | import io.reactivex.netty.protocol.http.client.HttpClientImpl; 16 | import io.reactivex.netty.protocol.http.client.HttpClientResponse; 17 | import io.reactivex.netty.protocol.http.client.internal.HttpClientToConnectionBridge; 18 | import io.reactivex.netty.protocol.http.client.internal.HttpEventPublisherFactory; 19 | import io.reactivex.netty.protocol.tcp.client.TcpClient; 20 | import io.reactivex.netty.protocol.tcp.client.TcpClientImpl; 21 | import org.reactivestreams.Publisher; 22 | import rx.Subscriber; 23 | import rx.functions.Action1; 24 | 25 | import java.net.SocketAddress; 26 | 27 | import static io.reactivex.netty.protocol.http.client.internal.HttpClientRequestImpl.NO_REDIRECTS; 28 | 29 | public class NettyTransportConnector implements TransportConnector { 30 | public NettyTransportConnector() {} 31 | 32 | @Override 33 | public Publisher> apply(SocketAddress address) { 34 | return subscriber -> { 35 | 36 | HttpClient httpClient = HttpClient.newClient(address); 37 | httpClient.createHead("/").subscribe(new Subscriber>() { 38 | @Override 39 | public void onCompleted() { 40 | subscriber.onComplete(); 41 | } 42 | 43 | @Override 44 | public void onError(Throwable e) { 45 | subscriber.onError(e); 46 | } 47 | 48 | @Override 49 | public void onNext(HttpClientResponse ignore) { 50 | Service service = 51 | new RxNettyService(httpClient, subscriber); 52 | subscriber.onNext(service); 53 | } 54 | }); 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/util/RestartablePublisher.java: -------------------------------------------------------------------------------- 1 | package io.qiro.util; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.reactivestreams.Subscriber; 5 | import org.reactivestreams.Subscription; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class RestartablePublisher implements Publisher { 11 | private final List cache; 12 | private final Publisher inputs; 13 | private boolean isCompleted; 14 | private final List> subscribeds; 15 | 16 | public RestartablePublisher(Publisher inputs) { 17 | this.cache = new ArrayList<>(); 18 | this.inputs = inputs; 19 | this.isCompleted = false; 20 | this.subscribeds = new ArrayList<>(); 21 | } 22 | 23 | public Publisher restart() { 24 | return new Publisher() { 25 | @Override 26 | public void subscribe(Subscriber s) { 27 | synchronized (RestartablePublisher.this) { 28 | cache.forEach(s::onNext); 29 | if (isCompleted) { 30 | s.onComplete(); 31 | } else { 32 | subscribeds.add(s); 33 | } 34 | } 35 | } 36 | }; 37 | } 38 | 39 | @Override 40 | public void subscribe(Subscriber subscriber) { 41 | inputs.subscribe(new Subscriber() { 42 | @Override 43 | public void onSubscribe(Subscription s) { 44 | subscriber.onSubscribe(s); 45 | } 46 | 47 | @Override 48 | public void onNext(T t) { 49 | synchronized (RestartablePublisher.this) { 50 | cache.add(t); 51 | } 52 | synchronized (RestartablePublisher.this) { 53 | subscribeds.forEach(s -> s.onNext(t)); 54 | } 55 | subscriber.onNext(t); 56 | } 57 | 58 | @Override 59 | public void onError(Throwable t) { 60 | subscriber.onError(t); 61 | synchronized (RestartablePublisher.this) { 62 | subscribeds.forEach(s -> s.onError(t)); 63 | } 64 | } 65 | 66 | @Override 67 | public void onComplete() { 68 | subscriber.onComplete(); 69 | synchronized (RestartablePublisher.this) { 70 | isCompleted = true; 71 | subscribeds.forEach(s -> s.onComplete()); 72 | } 73 | } 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/ResolverTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import io.qiro.loadbalancing.RoundRobinBalancer; 4 | import io.qiro.resolver.Resolvers; 5 | import io.qiro.resolver.TransportConnector; 6 | import io.qiro.testing.LoggerSubscriber; 7 | import io.qiro.testing.TestingService; 8 | import io.qiro.util.Publishers; 9 | import org.junit.Test; 10 | import org.reactivestreams.Publisher; 11 | 12 | import java.net.InetSocketAddress; 13 | import java.net.SocketAddress; 14 | import java.util.HashSet; 15 | import java.util.Set; 16 | 17 | import static io.qiro.util.Publishers.just; 18 | import static junit.framework.TestCase.assertEquals; 19 | 20 | public class ResolverTest { 21 | 22 | @Test(timeout = 10_000L) 23 | public void testResolver() throws InterruptedException { 24 | TestingService svc_1 = new TestingService<>(Object::toString); 25 | TestingService svc_2 = new TestingService<>(Object::toString); 26 | 27 | Set addresses = new HashSet<>(); 28 | addresses.add(new InetSocketAddress(1)); 29 | addresses.add(new InetSocketAddress(2)); 30 | Publishers.from(addresses); 31 | 32 | TransportConnector connector = new TransportConnector() { 33 | @Override 34 | public Publisher> apply(SocketAddress address) { 35 | return s -> { 36 | InetSocketAddress addr = (InetSocketAddress) address; 37 | if (addr.getPort() == 1) { 38 | s.onNext(svc_1); 39 | } else { 40 | s.onNext(svc_2); 41 | } 42 | }; 43 | } 44 | }; 45 | 46 | Publisher>> factories = 47 | Resolvers.resolveFactory(Publishers.from(addresses), connector); 48 | 49 | Service service = new RoundRobinBalancer<>(factories).toService(); 50 | 51 | service.requestResponse(1).subscribe(new LoggerSubscriber<>("req 1")); 52 | assertEquals(1, svc_1.queueSize() + svc_2.queueSize()); 53 | 54 | service.requestResponse(2).subscribe(new LoggerSubscriber<>("req 2")); 55 | assertEquals(1, svc_1.queueSize()); 56 | assertEquals(1, svc_2.queueSize()); 57 | 58 | svc_1.respond(); 59 | svc_1.complete(); 60 | assertEquals(1, svc_1.queueSize() + svc_2.queueSize()); 61 | 62 | svc_2.respond(); 63 | svc_2.complete(); 64 | assertEquals(0, svc_1.queueSize()); 65 | assertEquals(0, svc_2.queueSize()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/FilterTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.junit.Test; 4 | import org.reactivestreams.Publisher; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | 9 | import static io.qiro.util.Publishers.from; 10 | import static io.qiro.util.Publishers.toList; 11 | import static org.junit.Assert.assertTrue; 12 | 13 | public class FilterTest { 14 | @Test(timeout = 10_000L) 15 | public void testBasicFilter() throws InterruptedException { 16 | Filter square = 17 | Filters.fromInputFunction(x -> x * x); 18 | Filter plusOne = 19 | Filters.fromInputFunction(x -> x + 1); 20 | Service toStringService = Services.fromFunction(Object::toString); 21 | 22 | Filter xSquarePlusOne = square.andThen(plusOne); 23 | Filter xPlusOneSquare = plusOne.andThen(square); 24 | Service operation = xSquarePlusOne.andThen(toStringService); 25 | Service operation2 = xPlusOneSquare.andThen(toStringService); 26 | 27 | 28 | Publisher inputs = from(1, 2, 3, 4, 5); 29 | Publisher outputs = operation.requestChannel(inputs); 30 | Publisher outputs2 = operation2.requestChannel(inputs); 31 | 32 | List strings = toList(outputs); 33 | List strings2 = toList(outputs2); 34 | System.out.println(strings); 35 | System.out.println(strings2); 36 | assertTrue(strings.equals(Arrays.asList("2", "5", "10", "17", "26"))); 37 | assertTrue(strings2.equals(Arrays.asList("4", "9", "16", "25", "36"))); 38 | } 39 | 40 | @Test(timeout = 10_000L) 41 | public void testServiceFactoryFilter() throws InterruptedException { 42 | ServiceFactory factory = ServiceFactories.fromFunctions( 43 | () -> Services.fromFunction(Object::toString), 44 | () -> null 45 | ); 46 | Filter square = 47 | Filters.fromInputFunction(x -> x * x); 48 | 49 | Service squareToString = 50 | square 51 | .andThen(factory) 52 | .toService(); 53 | 54 | Publisher stringPublisher2 = 55 | squareToString.requestChannel(from(1, 2, 3, 4, 5)); 56 | List strings = toList(stringPublisher2); 57 | System.out.println(strings); 58 | assertTrue(strings.equals(Arrays.asList("1", "4", "9", "16", "25"))); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/WeightedServiceFactory.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.ServiceFactoryProxy; 6 | import io.qiro.ServiceProxy; 7 | import io.qiro.loadbalancing.loadestimator.LoadEstimator; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | 12 | /** 13 | * Add a load/weight to a ServiceFactory 14 | * The load/weight is the number of outstanding messages multiply by the availability. 15 | */ 16 | public class WeightedServiceFactory extends ServiceFactoryProxy { 17 | 18 | private static final double PENALTY_CONSTANT = 1_000_000; 19 | private final LoadEstimator estimator; 20 | 21 | WeightedServiceFactory(ServiceFactory underlying, LoadEstimator estimator) { 22 | super(underlying); 23 | this.estimator = estimator; 24 | } 25 | 26 | @Override 27 | public Publisher> apply() { 28 | return subscriber -> underlying.apply().subscribe(new Subscriber>() { 29 | @Override 30 | public void onSubscribe(Subscription s) { 31 | subscriber.onSubscribe(s); 32 | } 33 | 34 | @Override 35 | public void onNext(Service service) { 36 | ServiceProxy proxy = new ServiceProxy(service) { 37 | private long t0 = estimator.start(); 38 | 39 | @Override 40 | public Publisher close() { 41 | return s -> { 42 | estimator.end(t0); 43 | underlying.close().subscribe(s); 44 | }; 45 | } 46 | }; 47 | subscriber.onNext(proxy); 48 | } 49 | 50 | @Override 51 | public void onError(Throwable t) { 52 | subscriber.onError(t); 53 | } 54 | 55 | @Override 56 | public void onComplete() { 57 | subscriber.onComplete(); 58 | } 59 | }); 60 | } 61 | 62 | public synchronized double getLoad() { 63 | double availabilityValue = availability(); 64 | 65 | // in case all availabilities are zeros, it nicely degrades to a normal 66 | // least loaded loadbalancer. 67 | long now = System.nanoTime(); 68 | if (availabilityValue > 0.0) { 69 | return estimator.load(now) / availabilityValue; 70 | } else { 71 | return estimator.load(now) + PENALTY_CONSTANT; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/resolver/DnsResolver.java: -------------------------------------------------------------------------------- 1 | package io.qiro.resolver; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.reactivestreams.Subscriber; 5 | 6 | import java.net.*; 7 | import java.util.*; 8 | 9 | public class DnsResolver implements Resolver { 10 | private static String PROTOCOL_NAME = "dns"; 11 | 12 | @FunctionalInterface 13 | public interface LookupFunc { 14 | InetAddress[] apply(String host) throws UnknownHostException; 15 | } 16 | 17 | private LookupFunc lookupFunc; 18 | private long refreshPeriodMs; 19 | 20 | /*VisibleForTesting*/ 21 | public DnsResolver(long refreshPeriodMs, LookupFunc lookupFunc) { 22 | this.lookupFunc = lookupFunc; 23 | this.refreshPeriodMs = refreshPeriodMs; 24 | } 25 | 26 | public DnsResolver() { 27 | this(-1L, DnsResolver::nativeLookup); 28 | } 29 | 30 | @Override 31 | public Publisher> resolve(String url) { 32 | return new Publisher>() { 33 | @Override 34 | public void subscribe(Subscriber> subscriber) { 35 | // String protocol = url.getProtocol(); 36 | String protocol = "dns"; 37 | if (!protocol.equals(PROTOCOL_NAME)) { 38 | String message = "'" + protocol + "' isn't supported by the DnsResolver\n"; 39 | message += "URL should be in the form 'dns://hostname:port,hostname2:port2'"; 40 | subscriber.onError(new MalformedURLException(message)); 41 | } else { 42 | // TODO: fix that, make it asynchronous and check periodically 43 | // Set addresses = new HashSet<>(resolveDns(url.getHost())); 44 | Set addresses = new HashSet<>( 45 | resolveDns(url.substring("dns://".length()))); 46 | subscriber.onNext(addresses); 47 | } 48 | } 49 | }; 50 | } 51 | 52 | private List resolveDns(String hostPort) { 53 | String[] split = hostPort.split(":"); 54 | String host = split[0]; 55 | int port = Integer.parseInt(split[1]); 56 | 57 | List addresses = new ArrayList<>(); 58 | try { 59 | for (InetAddress addr: lookupFunc.apply(host)) { 60 | 61 | addresses.add(new InetSocketAddress(addr, port)); 62 | } 63 | } catch (UnknownHostException e) { 64 | e.printStackTrace(); 65 | } 66 | return addresses; 67 | } 68 | 69 | private static InetAddress[] nativeLookup(String host) throws UnknownHostException { 70 | return InetAddress.getAllByName(host); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /qiro-reactivesocket/src/main/java/io/qiro/reactivesocket/ReactiveSocketTransportConnector.java: -------------------------------------------------------------------------------- 1 | package io.qiro.reactivesocket; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.codec.Codec; 5 | import io.qiro.resolver.TransportConnector; 6 | import io.reactivesocket.ConnectionSetupPayload; 7 | import io.reactivesocket.ReactiveSocket; 8 | import io.reactivesocket.rx.Completable; 9 | import io.reactivesocket.websocket.rxnetty.WebSocketDuplexConnection; 10 | import io.reactivex.netty.protocol.http.client.HttpClient; 11 | import io.reactivex.netty.protocol.http.ws.WebSocketConnection; 12 | import io.reactivex.netty.protocol.http.ws.client.WebSocketResponse; 13 | import org.reactivestreams.Publisher; 14 | import rx.Observable; 15 | 16 | import java.net.SocketAddress; 17 | 18 | public class ReactiveSocketTransportConnector implements TransportConnector { 19 | private Codec codec; 20 | 21 | public ReactiveSocketTransportConnector(Codec codec) { 22 | this.codec = codec; 23 | } 24 | 25 | @Override 26 | public Publisher> apply(SocketAddress address) { 27 | return svcSubscriber -> { 28 | Observable wsConnection = 29 | HttpClient.newClient(address) 30 | .createGet("/rs") 31 | .requestWebSocketUpgrade() 32 | .flatMap(WebSocketResponse::getWebSocketConnection); 33 | 34 | wsConnection.subscribe(new rx.Subscriber() { 35 | @Override 36 | public void onCompleted() { 37 | svcSubscriber.onComplete(); 38 | } 39 | 40 | @Override 41 | public void onError(Throwable e) { 42 | svcSubscriber.onError(e); 43 | } 44 | 45 | @Override 46 | public void onNext(WebSocketConnection webSocket) { 47 | WebSocketDuplexConnection wsDuplexConnection = 48 | WebSocketDuplexConnection.create(webSocket); 49 | ReactiveSocket reactiveSocket = ReactiveSocket.fromClientConnection( 50 | wsDuplexConnection, ConnectionSetupPayload.create("UTF-8", "UTF-8")); 51 | 52 | reactiveSocket.start(new Completable() { 53 | @Override 54 | public void success() { 55 | svcSubscriber.onNext( 56 | new ReactiveSocketService<>(reactiveSocket, codec)); 57 | } 58 | 59 | @Override 60 | public void error(Throwable e) { 61 | svcSubscriber.onError(e); 62 | } 63 | }); 64 | } 65 | }); 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/filter/TimeoutFilter.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Filter; 4 | import io.qiro.Service; 5 | import io.qiro.util.Timer; 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | 10 | import java.util.concurrent.*; 11 | 12 | public class TimeoutFilter implements Filter { 13 | private final static ScheduledExecutorService EXECUTOR = 14 | Executors.newScheduledThreadPool(1, runnable -> { 15 | Thread thread = new Thread(runnable, "Timer-Thread"); 16 | thread.setDaemon(true); 17 | return thread; 18 | }); 19 | 20 | private final long maxDurationMs; 21 | private Timer timer; 22 | 23 | public TimeoutFilter(long maxDurationMs, Timer timer) { 24 | this.maxDurationMs = maxDurationMs; 25 | this.timer = timer; 26 | } 27 | 28 | @Override 29 | public Publisher requestChannel(Publisher inputs, Service service) { 30 | return new Publisher() { 31 | Timer.TimerTask timerTask = null; 32 | private volatile Subscription respSubscription = null; 33 | 34 | @Override 35 | public void subscribe(Subscriber subscriber) { 36 | service.requestChannel(inputs).subscribe(new Subscriber() { 37 | @Override 38 | public void onSubscribe(Subscription s) { 39 | respSubscription = s; 40 | subscriber.onSubscribe(s); 41 | armTimer(); 42 | } 43 | 44 | @Override 45 | public void onNext(Resp resp) { 46 | subscriber.onNext(resp); 47 | } 48 | 49 | @Override 50 | public void onError(Throwable t) { 51 | cancelTimer(); 52 | subscriber.onError(t); 53 | } 54 | 55 | @Override 56 | public void onComplete() { 57 | cancelTimer(); 58 | subscriber.onComplete(); 59 | } 60 | 61 | private void cancelTimer() { 62 | if (timerTask != null) { 63 | timerTask.cancel(); 64 | } 65 | } 66 | 67 | private void armTimer() { 68 | timerTask = timer.schedule(() -> { 69 | respSubscription.cancel(); 70 | subscriber.onError(new Exception("timeout")); 71 | }, maxDurationMs, TimeUnit.MILLISECONDS); 72 | } 73 | }); 74 | } 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/builder/ServerBuilder.java: -------------------------------------------------------------------------------- 1 | package io.qiro.builder; 2 | import io.qiro.Server; 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.codec.Codec; 6 | import io.qiro.util.Clock; 7 | import io.qiro.util.HashwheelTimer; 8 | import io.qiro.util.Timer; 9 | import org.reactivestreams.Publisher; 10 | 11 | import java.net.InetSocketAddress; 12 | import java.net.SocketAddress; 13 | 14 | public abstract class ServerBuilder { 15 | private Timer timer = HashwheelTimer.INSTANCE; 16 | private Clock clock = Clock.SYSTEM_CLOCK; 17 | private Codec codec = null; 18 | private SocketAddress bindingAddress; 19 | 20 | public ServerBuilder clock(Clock clock) { 21 | this.clock = clock; 22 | return this; 23 | } 24 | 25 | public ServerBuilder timer(Timer timer) { 26 | this.timer = timer; 27 | return this; 28 | } 29 | 30 | public ServerBuilder codec(Codec codec) { 31 | this.codec = codec; 32 | return this; 33 | } 34 | 35 | public ServerBuilder listen(SocketAddress address) { 36 | this.bindingAddress = address; 37 | return this; 38 | } 39 | 40 | public ServerBuilder listen(int port) { 41 | return listen(new InetSocketAddress(port)); 42 | } 43 | 44 | public Server build(Service service) { 45 | ServiceFactory factory = new ServiceFactory() { 46 | @Override 47 | public Publisher> apply() { 48 | return s -> { 49 | s.onNext(service); 50 | s.onComplete(); 51 | }; 52 | } 53 | 54 | @Override 55 | public double availability() { 56 | return service.availability(); 57 | } 58 | 59 | @Override 60 | public Publisher close() { 61 | return service.close(); 62 | } 63 | }; 64 | return build(factory); 65 | } 66 | 67 | public Server build(ServiceFactory factory) { 68 | if (bindingAddress == null) { 69 | throw new IllegalStateException("`binding address` is uninitialized!\n" 70 | + "please use `ServerBuilder.listen`\n" 71 | + "e.g. ServerBuilder.listen(8080)"); 72 | } 73 | if (codec == null) { 74 | throw new IllegalStateException("`codec` is uninitialized!\n" 75 | + "please use `ServerBuilder.codec`\n" 76 | + "e.g. ServerBuilder.codec(UTF8Codec.INSTANCE)"); 77 | } 78 | 79 | return start(factory, bindingAddress, codec); 80 | } 81 | 82 | protected abstract Server start( 83 | ServiceFactory factory, 84 | SocketAddress address, 85 | Codec codec 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/ServiceFactories.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import io.qiro.util.Availabilities; 4 | import org.reactivestreams.Publisher; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.function.Supplier; 9 | 10 | public class ServiceFactories { 11 | 12 | private static List> services; 13 | 14 | public static ServiceFactory 15 | fromFunctions( 16 | Supplier> createFn, 17 | Supplier closeFn 18 | ) { 19 | return fromFunctions(createFn, closeFn, () -> 1.0); 20 | } 21 | 22 | public static ServiceFactory 23 | fromFunctions( 24 | Supplier> createFn, 25 | Supplier closeFn, 26 | Supplier availabilityFn 27 | ) { 28 | return new ServiceFactory() { 29 | @Override 30 | public Publisher> apply() { 31 | return s -> { 32 | Service service = createFn.get(); 33 | s.onNext(service); 34 | s.onComplete(); 35 | }; 36 | } 37 | 38 | @Override 39 | public double availability() { 40 | return availabilityFn.get(); 41 | } 42 | 43 | @Override 44 | public Publisher close() { 45 | return s -> { 46 | s.onNext(closeFn.get()); 47 | s.onComplete(); 48 | }; 49 | } 50 | }; 51 | } 52 | 53 | @SafeVarargs 54 | public static ServiceFactory 55 | roundRobin(Service... services) { 56 | return roundRobin(Arrays.asList(services)); 57 | } 58 | 59 | public static ServiceFactory 60 | roundRobin(List> services) { 61 | return new ServiceFactory() { 62 | private int i = 0; 63 | 64 | @Override 65 | public Publisher> apply() { 66 | return s -> { 67 | s.onNext(services.get(i % services.size())); 68 | s.onComplete(); 69 | i += 1; 70 | }; 71 | } 72 | 73 | @Override 74 | public double availability() { 75 | return Availabilities.avgOfServices(services); 76 | } 77 | 78 | @Override 79 | public Publisher close() { 80 | return s -> { 81 | services.forEach(Service::close); 82 | s.onNext(null); 83 | s.onComplete(); 84 | }; 85 | } 86 | }; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/resolver/Resolvers.java: -------------------------------------------------------------------------------- 1 | package io.qiro.resolver; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.util.Availabilities; 6 | import io.qiro.util.EmptySubscriber; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | import java.net.SocketAddress; 12 | import java.util.ArrayList; 13 | import java.util.HashSet; 14 | import java.util.List; 15 | import java.util.Set; 16 | import java.util.function.Function; 17 | 18 | public class Resolvers { 19 | public static Publisher>> resolveFactory( 20 | Publisher> addresses, 21 | TransportConnector connector 22 | ) { 23 | return subscriber -> addresses.subscribe(new Subscriber>() { 24 | @Override 25 | public void onSubscribe(Subscription s) { 26 | subscriber.onSubscribe(s); 27 | } 28 | 29 | @Override 30 | public void onNext(Set socketAddresses) { 31 | Set> factories = new HashSet<>(); 32 | for (SocketAddress addr : socketAddresses) { 33 | ServiceFactory factory = 34 | connector.toFactory(addr); 35 | factories.add(factory); 36 | } 37 | subscriber.onNext(factories); 38 | } 39 | 40 | @Override 41 | public void onError(Throwable t) { 42 | subscriber.onError(t); 43 | } 44 | 45 | @Override 46 | public void onComplete() { 47 | subscriber.onComplete(); 48 | } 49 | }); 50 | } 51 | 52 | private static class InetServiceFactory implements ServiceFactory { 53 | private final List> services; 54 | private final Function> fn; 55 | private final SocketAddress addr; 56 | 57 | public InetServiceFactory(Function> fn, SocketAddress addr) { 58 | this.fn = fn; 59 | this.addr = addr; 60 | services = new ArrayList<>(); 61 | } 62 | 63 | @Override 64 | public Publisher> apply() { 65 | return s -> { 66 | Service service = fn.apply(addr); 67 | synchronized (services) { 68 | services.add(service); 69 | } 70 | s.onNext(service); 71 | s.onComplete(); 72 | }; 73 | } 74 | 75 | @Override 76 | public synchronized double availability() { 77 | return Availabilities.avgOfServices(services); 78 | } 79 | 80 | @Override 81 | public Publisher close() { 82 | return s -> { 83 | synchronized (services) { 84 | services.forEach(svc -> 85 | svc.close().subscribe(EmptySubscriber.INSTANCE) 86 | ); 87 | } 88 | }; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | 5 | # force upgrade Java8 as per https://github.com/travis-ci/travis-ci/issues/4042 (fixes compilation issue) 6 | addons: 7 | apt: 8 | packages: 9 | - oracle-java8-installer 10 | 11 | sudo: false 12 | # as per http://blog.travis-ci.com/2014-12-17-faster-builds-with-container-based-infrastructure/ 13 | 14 | # script for build and release via Travis to Bintray 15 | script: gradle/buildViaTravis.sh 16 | 17 | # cache between builds 18 | cache: 19 | directories: 20 | - $HOME/.m2 21 | - $HOME/.gradle 22 | 23 | env: 24 | global: 25 | - secure: "ZlZolyStexByrrZOL9KBCsjOD+jS8NM/+oRjen1MnskWhji/6ObLUfSzE+f7h7e8cHWE5dT4DoUOZ3a7WD7rC74hqhDgc/uFZLA4TtgmpJb9B0NYvA+qdw0HZLATOxs7ky9UEtojABXqEJF+5tSz2qB8Nq5mRUoCRAag2/SA+1QBiYIHmIKNR3EG46k37p7qnLWr2Vkc1WU5aO7IGFj22fxY5GUz0H+VMcIHuJ/UcM0W6CLldJ22WGfgdL+q11vHCP5C9Vv5LiJMTiVvYZOU6PNo7bolh+Ggdq8b4pRQWMuraTDuGHnenoF19N0ufLFFBekI15xNxBwlcWphkjjIVZXQx/oiNwSdc8PYafBScpyNaFZC3aBnR3GQV/x+jYXUxod1CQ3MtNWo66rSww0IXXOdfI3olDKcsp9pLkaYgOinvY0CKGUlpoYME48U2PYXnuuCViRMO6Z7MWoF6avLb/SOlP3ClDIl4GkOrxYK+qAfRKf9pAet45mpDr/RLf3cMVnfVyqRzgbOiMv7/kDMoyxmdR5iILpRO8pwwb6rxfvrcaQPbM8zz9P+fAQwhJPGIxIoK9CmwHmhMhocyhu9G5O2I5uAO6oYbRwEgVtnb6byiTQZZSG9C56ZdZh8vv1fA8LyYdKQpNPFc5AdjKWs2O2gjtpFbS8ZVf1MXc3nobQ=" 26 | - secure: "W1txSuj8wZKQVc1xaBpS8wM4dXiErUPzUAt8+eoXSh/SIT52MFMZIYFgW3oHatlOjQ31LibAaku+4AtngraeyCn1y/4u+FekDFMXvKujXTYC3MkjAbT82R3Ye1TttHj825/OLHr8QsepckUPsu05rzFXQr0i77d96hG7ltfgkePZgGxCsrETGFeXvMI/1OzBo/vLpEs1NiOiMUO9/tclcNMs6kxZyyp5+D6+m79Xe7hGCrueQjI/8IAxa21bUlfyACmIRKPJ6d0lxA5+82w3si9hcTLBKrl13Gb7OWmPCZDl+bSNdoD2Yb7yKs8YSxQKfm+TBJ2cm0o3xTnnIOJHcfF5zlZ1AR8mj5Dl0lsMy0TcGwOQRnHyU6UWku80Tq8YdBWdoMvkufQnn/ZFdcFQqRBf4LcBf3KPs0TH/SbsvFx7NDJLmyFgyAXrSiLjdJKH/fltwdB6I+1nzPkeJdR4WbP8sTeO9IETwOvnXpPDV6cnVB48vWxvD8nV5v06/vKueXElYbsfe9dgctluWwmZp5TVDjmhK4PdG6QH9gr+1gJvcF+yEtweaywK/L+K8GG2NQtKAniiQ+odGmH3VF1rdOlGqj/6iRq2lHIztEL3UWSuplcgRoyEychwnf3tHrKEszXDxhvruHNljmNgTXcHt9CaGph+tB1omKPBZoNKJdw=" 27 | - secure: "q31a5HAHWNHHfeQo2CmjDtV1lrfnSQsBlo1A4tyUdnEGps6nWuCdglETP9bLd2A7uTkj5Dzq/UUedoTnNP5Koh6hpfgFu9LkxlAXnzvkjCIvgWcP3ZqCAkj2J5hMkGTUNP+Fy4C1yfNd69TqtIfZk/NNqOqdWc6emtvJ2E3LMkdqcpWFZxmso1M5AdeoLOnhS98KRwvbYFm5x7HdyCXAl9fQg8SalGfePoxVLpwXzTlqvf7tU+L+1BLYTwUxQv/TERTg4VDZaUr1gIB6RAhWDHQLeawNmftr6f/8LNpPR2z5UU7/PzG5jsYzsrvm0jf2vhAKBxTTZMvOPgBYG9RetWJC8JLdi3svc814xAIG2Hg0GltBkPzLLG/D5ptS3fjzA3zq8RR+MZRHtiMvlTpgiTTze1QdYr7d8mPhGjI++aBNeLOXuqirv/xHwr2CX0xkJzuAGDkJAD+OIPUd+1HMzanVLrtisDgkCyjhzQhqdvMK+KIAp5xM+r3PQGn57i1ycX/XgQXgioxGgrDmWwwTQnUySWh1crttxKlH+NXJm51ofP0TMAtSDx2bSJuL9ZLU1fc3MLY7tuFkcBJpR4eztMZ7xp9MoHZsQvxEzM2s99mD4xjL0E+84eloF+EdDWbsk/MqzdLYOYCJ5nJ53c/WIREgM1Hgbyz1iNdWCmYSS8c=" 28 | - secure: "EPdqmJtpnDwuB71OaZbZxY2N4jZTBiQeHGFU6GxTLQEdPi1xZloIwPM4k8OURsMBBeP8e3irnR5KdbN4C77xkTLav/gLWqGMaP4tPCIHe9KHDindSzKIY0ZInBG8AG9Ix0P6KOfnE1AXdxIoMXLjUxuLC3Es1tVyWx7aD7FiUMJH2N2yvO13nc9dkHTp0Y9oClAP6DrsiQmXnj0vD5k12kdiJLrLJrbm2LZa69SWIds3NqE1D76KfxG5JmBYDf1IBGnvMO0l6GgsvhV/y2EUeHnqHjKTlX41lBM4j91Dq12O7gvnoC08sJPhw7a2MWDNSh827V8Q11Xu43khaU861u2asT2t03XSXBRKpe0XhQpdNI5gvCLrRunKbgcF+hyBVm7GyZa7ILW7lgsH+WsK73yjj+vN8vfkeYhYPz3MzejlPZhF02NKf8vDWNZVIt+sLu9PXVFoQajCaVf3OYmAEhpy+3DvwC9hRML/llbvhuofPInMACtVvUlTpt+uU9o9yBPjgYaR9rUEgHhZmA9yoS+VeuT4155QJR96EWlc0woiQR/6R4DxNOApYWsfHZe6ML+ozH7y7WJ5+m3NiOLb92Jx2rAR/RvthnrSIxB7ZHVZUZVNtAzSmI8Kk/1znZJ+GoLQIoOAkXQp1mXgtxJp37c6Vqgl9Z8GHxlp5tT5pqs=" 29 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/Services.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.reactivestreams.Subscriber; 5 | import org.reactivestreams.Subscription; 6 | 7 | import java.util.function.Supplier; 8 | 9 | import static io.qiro.util.Publishers.just; 10 | import static io.qiro.util.Publishers.never; 11 | 12 | public class Services { 13 | public static Supplier ALWAYS_AVAILABLE = () -> 1.0; 14 | public static Supplier EMPTY_CLOSE_FN = () -> null; 15 | 16 | public static Service fromFunction(ThrowableFunction fn) { 17 | return fromFunction(fn, EMPTY_CLOSE_FN); 18 | } 19 | 20 | public static Service fromFunction( 21 | ThrowableFunction fn, 22 | Supplier closeFn 23 | ) { 24 | return fromFunction(fn, closeFn, ALWAYS_AVAILABLE); 25 | } 26 | 27 | public static Service fromFunction( 28 | ThrowableFunction fn, 29 | Supplier closeFn, 30 | Supplier availability 31 | ) { 32 | return new Service() { 33 | @Override 34 | public Publisher requestResponse(Req req) { 35 | return requestStream(req); 36 | } 37 | 38 | @Override 39 | public Publisher requestStream(Req req) { 40 | return requestSubscription(req); 41 | } 42 | 43 | @Override 44 | public Publisher requestSubscription(Req req) { 45 | return requestChannel(just(req)); 46 | } 47 | 48 | @Override 49 | public Publisher requestChannel(Publisher inputs) { 50 | return subscriber -> inputs.subscribe(new Subscriber() { 51 | private Subscription subscription; 52 | 53 | @Override 54 | public void onSubscribe(Subscription s) { 55 | subscription = s; 56 | subscriber.onSubscribe(s); 57 | } 58 | 59 | @Override 60 | public void onNext(Req input) { 61 | try { 62 | Resp resp = fn.apply(input); 63 | subscriber.onNext(resp); 64 | } catch (Throwable ex) { 65 | onError(ex); 66 | } 67 | } 68 | 69 | @Override 70 | public void onError(Throwable t) { 71 | subscriber.onError(t); 72 | subscription.cancel(); 73 | } 74 | 75 | @Override 76 | public void onComplete() { 77 | subscriber.onComplete(); 78 | } 79 | }); 80 | 81 | } 82 | 83 | @Override 84 | public double availability() { 85 | return availability.get(); 86 | } 87 | 88 | @Override 89 | public Publisher close() { 90 | return s -> { 91 | s.onNext(closeFn.get()); 92 | s.onComplete(); 93 | }; 94 | } 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/failures/FailFastFactory.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.util.Backoff; 6 | import io.qiro.util.EmptySubscriber; 7 | import io.qiro.util.Publishers; 8 | import io.qiro.util.Timer; 9 | import org.reactivestreams.Publisher; 10 | import org.reactivestreams.Subscriber; 11 | import org.reactivestreams.Subscription; 12 | 13 | public class FailFastFactory implements ServiceFactory { 14 | private interface State {} 15 | private static class Ok implements State {} 16 | private static class Retrying implements State { 17 | private Backoff backoff = null; 18 | 19 | long nextBackoff() { 20 | if (backoff == null) { 21 | backoff = new Backoff(0.5, 2, 1<<15); 22 | return 0; 23 | } else { 24 | return backoff.nextBackoff(); // 1, 2, 4, 8, 16 ... 32768, 32768, 32768 ... 25 | } 26 | } 27 | } 28 | private static Ok OK = new Ok(); 29 | 30 | private State state; 31 | private final Timer timer; 32 | private ServiceFactory underlying; 33 | 34 | public FailFastFactory(ServiceFactory underlying, Timer timer) { 35 | this.state = OK; 36 | this.timer = timer; 37 | this.underlying = underlying; 38 | } 39 | 40 | @Override 41 | public Publisher> apply() { 42 | return subscriber -> Publishers.transform( 43 | underlying.apply(), 44 | svc -> svc, 45 | exc -> { 46 | // failure to create the Service toggle the state 47 | synchronized (FailFastFactory.this) { 48 | if (state == OK) { 49 | state = new Retrying(); 50 | update(); 51 | } 52 | } 53 | return exc; 54 | } 55 | ).subscribe(subscriber); 56 | } 57 | 58 | @Override 59 | public double availability() { 60 | return state == OK ? underlying.availability() : 0.0; 61 | } 62 | 63 | @Override 64 | public Publisher close() { 65 | // TODO: discard the task? 66 | return underlying.close(); 67 | } 68 | 69 | private synchronized void update() { 70 | if (state != OK) { 71 | long nextBackoffMs = ((Retrying) state).nextBackoff(); 72 | timer.schedule(() -> { 73 | underlying.apply().subscribe(new Subscriber>() { 74 | @Override 75 | public void onSubscribe(Subscription s) { 76 | s.request(1L); 77 | } 78 | 79 | @Override 80 | public void onNext(Service service) { 81 | synchronized (FailFastFactory.this) { 82 | state = OK; 83 | } 84 | service.close().subscribe(EmptySubscriber.INSTANCE); 85 | } 86 | 87 | @Override 88 | public void onError(Throwable t) { 89 | update(); 90 | } 91 | 92 | @Override 93 | public void onComplete() {} 94 | }); 95 | }, nextBackoffMs 96 | ); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/pool/SingletonPool.java: -------------------------------------------------------------------------------- 1 | package io.qiro.pool; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.util.Availabilities; 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | public class SingletonPool implements ServiceFactory { 13 | private final ServiceFactory underlying; 14 | private volatile Service singleton; 15 | private AtomicBoolean isCreated; 16 | 17 | public SingletonPool(ServiceFactory underlying) { 18 | this.underlying = underlying; 19 | this.isCreated = new AtomicBoolean(false); 20 | } 21 | 22 | @Override 23 | public Publisher> apply() { 24 | return new Publisher>() { 25 | @Override 26 | public void subscribe(Subscriber> subscriber) { 27 | if (isCreated.compareAndSet(false, true)) { 28 | System.out.println("SingletonPool: creating the singleton"); 29 | underlying.apply().subscribe(new Subscriber>() { 30 | @Override 31 | public void onSubscribe(Subscription s) { 32 | subscriber.onSubscribe(s); 33 | } 34 | 35 | @Override 36 | public void onNext(Service service) { 37 | if (singleton != null) { 38 | throw new RuntimeException("Singleton has already been created"); 39 | } 40 | System.out.println("SingletonPool: singleton created"); 41 | singleton = service; 42 | subscriber.onNext(service); 43 | } 44 | 45 | @Override 46 | public void onError(Throwable t) { 47 | subscriber.onError(t); 48 | } 49 | 50 | @Override 51 | public void onComplete() { 52 | subscriber.onComplete(); 53 | } 54 | }); 55 | } else { 56 | System.out.println("SingletonPool: reusing singleton " + singleton); 57 | // TODO: handle the race properly 58 | while (singleton == null) { 59 | try { 60 | Thread.sleep(10); 61 | } catch (InterruptedException e) { 62 | e.printStackTrace(); 63 | } 64 | } 65 | subscriber.onNext(singleton); 66 | subscriber.onComplete(); 67 | } 68 | } 69 | }; 70 | } 71 | 72 | @Override 73 | public double availability() { 74 | if (singleton == null) { 75 | return 0.0; 76 | } else { 77 | return singleton.availability(); 78 | } 79 | } 80 | 81 | @Override 82 | public Publisher close() { 83 | return s -> { 84 | if (isCreated.compareAndSet(true, false)) { 85 | singleton = null; 86 | } 87 | s.onNext(null); 88 | s.onComplete(); 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/filter/RetryFilter.java: -------------------------------------------------------------------------------- 1 | package io.qiro.filter; 2 | 3 | import io.qiro.Filter; 4 | import io.qiro.Service; 5 | import io.qiro.failures.Retryable; 6 | import io.qiro.util.RestartablePublisher; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | import java.io.IOException; 12 | import java.net.SocketException; 13 | import java.util.function.Function; 14 | 15 | public class RetryFilter implements Filter { 16 | private int limit; 17 | private Function retryThisThrowable; 18 | 19 | public RetryFilter(int limit, Function retryThisThrowable) { 20 | this.limit = limit; 21 | this.retryThisThrowable = retryThisThrowable; 22 | } 23 | 24 | public RetryFilter(int limit) { 25 | this(limit, t -> false); 26 | } 27 | 28 | @Override 29 | public Publisher requestChannel(Publisher inputs, Service service) { 30 | return apply(inputs, service, limit); 31 | } 32 | 33 | public Publisher apply(Publisher inputs, Service service, int retryBudget) { 34 | return new Publisher() { 35 | private boolean started = false; 36 | private boolean canceled = false; 37 | 38 | @Override 39 | public void subscribe(Subscriber subscriber) { 40 | RestartablePublisher restartablePublisher = new RestartablePublisher<>(inputs); 41 | service.requestChannel(restartablePublisher).subscribe(new Subscriber() { 42 | @Override 43 | public void onSubscribe(Subscription s) { 44 | subscriber.onSubscribe(s); 45 | } 46 | 47 | @Override 48 | public void onNext(Resp response) { 49 | if (canceled) { 50 | return; 51 | } 52 | started = true; 53 | subscriber.onNext(response); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable t) { 58 | if (canceled) { 59 | return; 60 | } 61 | // if the exception is Retryable and the stream of responses didn't started, 62 | // it's safe to retry the call. 63 | if (retryBudget > 0 && !started && isRetryable(t)) { 64 | canceled = true; 65 | Publisher newResponses = 66 | apply(restartablePublisher.restart(), service, retryBudget - 1); 67 | System.out.println("***** retrying"); 68 | newResponses.subscribe(subscriber); 69 | } else { 70 | subscriber.onError(t); 71 | } 72 | } 73 | 74 | @Override 75 | public void onComplete() { 76 | if (!canceled) { 77 | subscriber.onComplete(); 78 | } 79 | } 80 | }); 81 | } 82 | }; 83 | } 84 | 85 | private boolean isRetryable(Throwable t) { 86 | if (t instanceof Retryable) { 87 | return true; 88 | } else if (t instanceof SocketException) { 89 | return true; 90 | } else if (t instanceof IOException) { 91 | return true; 92 | } else { 93 | return retryThisThrowable.apply(t); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/loadbalancing/BalancerFactory.java: -------------------------------------------------------------------------------- 1 | package io.qiro.loadbalancing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.loadbalancing.loadestimator.LoadEstimator; 6 | import io.qiro.loadbalancing.loadestimator.NullEstimator; 7 | import io.qiro.loadbalancing.selector.Selector; 8 | import io.qiro.util.Availabilities; 9 | import io.qiro.util.EmptySubscriber; 10 | import org.reactivestreams.Publisher; 11 | 12 | import java.util.*; 13 | import java.util.function.Function; 14 | import java.util.function.Supplier; 15 | 16 | public class BalancerFactory implements ServiceFactory { 17 | 18 | private final Function>, ServiceFactory> selector; 19 | private volatile List> buffer = 20 | Collections.unmodifiableList(new ArrayList<>()); 21 | 22 | public BalancerFactory( 23 | Publisher>> factorySet, 24 | Selector selector 25 | ) { 26 | this(factorySet, selector, NullEstimator.SUPPLIER); 27 | } 28 | 29 | public BalancerFactory( 30 | Publisher>> factorySet, 31 | Selector selector, 32 | Supplier estimator 33 | ) { 34 | this.selector = selector; 35 | factorySet.subscribe(new EmptySubscriber>>() { 36 | @Override 37 | public void onNext(Set> newSet) { 38 | System.out.println("BalancerFactory: Storing ServiceFactory"); 39 | 40 | // Only the update of the server list is synchronized 41 | synchronized (BalancerFactory.this) { 42 | Set> current = new HashSet<>(buffer); 43 | List> newFactories = new ArrayList<>(); 44 | 45 | for (ServiceFactory factory: current) { 46 | if (!newSet.contains(factory)) { 47 | factory.close().subscribe(EmptySubscriber.INSTANCE); 48 | } else { 49 | WeightedServiceFactory wFactory = 50 | new WeightedServiceFactory<>(factory, estimator.get()); 51 | newFactories.add(wFactory); 52 | } 53 | } 54 | for (ServiceFactory factory: newSet) { 55 | if (!current.contains(factory)) { 56 | WeightedServiceFactory wFactory = 57 | new WeightedServiceFactory<>(factory, estimator.get()); 58 | newFactories.add(wFactory); 59 | } 60 | } 61 | buffer = Collections.unmodifiableList(newFactories); 62 | } 63 | } 64 | }); 65 | } 66 | 67 | @Override 68 | public Publisher> apply() { 69 | return subscriber -> { 70 | List> serverList = this.buffer; 71 | if (serverList.isEmpty()) { 72 | subscriber.onError(new Exception("No Server available in the Loadbalancer!")); 73 | } else { 74 | ServiceFactory factory = selector.apply(serverList); 75 | factory.apply().subscribe(subscriber); 76 | } 77 | }; 78 | } 79 | 80 | @Override 81 | public synchronized double availability() { 82 | return Availabilities.avgOfServiceFactories(buffer); 83 | } 84 | 85 | @Override 86 | public Publisher close() { 87 | return s -> { 88 | synchronized (BalancerFactory.this) { 89 | buffer.forEach(svc -> 90 | svc.close().subscribe(new EmptySubscriber<>()) 91 | ); 92 | } 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/testing/TestingService.java: -------------------------------------------------------------------------------- 1 | package io.qiro.testing; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.util.EmptySubscriber; 5 | import io.qiro.util.Publishers; 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | 9 | import java.util.ArrayList; 10 | import java.util.Deque; 11 | import java.util.List; 12 | import java.util.concurrent.LinkedBlockingDeque; 13 | import java.util.function.Function; 14 | 15 | public class TestingService implements Service { 16 | private class StreamInfo { 17 | private final Deque requests = new LinkedBlockingDeque<>(); 18 | private final Subscriber subscriber; 19 | 20 | StreamInfo(Subscriber subscriber) { 21 | this.subscriber = subscriber; 22 | } 23 | 24 | synchronized void offer(Req req) { 25 | requests.offer(req); 26 | } 27 | 28 | synchronized void respond() { 29 | if (!requests.isEmpty()) { 30 | Req request = requests.pollFirst(); 31 | Resp response = serviceFn.apply(request); 32 | subscriber.onNext(response); 33 | } 34 | } 35 | 36 | synchronized void complete() { 37 | subscriber.onComplete(); 38 | } 39 | } 40 | 41 | private boolean open = true; 42 | private double availabilityValue = 1.0; 43 | 44 | private List streamInfos; 45 | private Function serviceFn; 46 | 47 | public TestingService(Function fn) { 48 | serviceFn = fn; 49 | streamInfos = new ArrayList<>(); 50 | } 51 | 52 | @Override 53 | public Publisher requestChannel(Publisher requests) { 54 | if (!open) { 55 | throw new IllegalStateException("applying on a close TestingService"); 56 | } 57 | return subscriber -> { 58 | synchronized (TestingService.this) { 59 | StreamInfo info = new StreamInfo(subscriber); 60 | requests.subscribe(new EmptySubscriber() { 61 | @Override 62 | public void onNext(Req req) { 63 | info.offer(req); 64 | } 65 | }); 66 | streamInfos.add(info); 67 | } 68 | }; 69 | } 70 | 71 | public synchronized int queueSize() { 72 | int size = 0; 73 | for (StreamInfo info: streamInfos) { 74 | size += info.requests.size(); 75 | } 76 | return size; 77 | } 78 | 79 | public synchronized int queueSize(int streamId) { 80 | return streamInfos.get(streamId).requests.size(); 81 | } 82 | 83 | public synchronized void respond() { 84 | streamInfos.forEach(StreamInfo::respond); 85 | } 86 | 87 | public synchronized void respond(int streamId) { 88 | streamInfos.get(streamId).respond(); 89 | } 90 | 91 | public synchronized void complete() { 92 | while (!streamInfos.isEmpty()) { 93 | StreamInfo info = streamInfos.remove(0); 94 | info.complete(); 95 | } 96 | } 97 | 98 | public synchronized void complete(int streamId) { 99 | streamInfos.remove(streamId).complete(); 100 | } 101 | 102 | public synchronized boolean isOpen() { 103 | return open; 104 | } 105 | 106 | public synchronized boolean isClosed() { 107 | return !open; 108 | } 109 | 110 | @Override 111 | public double availability() { 112 | return availabilityValue; 113 | } 114 | 115 | @Override 116 | public Publisher close() { 117 | return s -> { 118 | synchronized (TestingService.this) { 119 | open = false; 120 | } 121 | s.onComplete(); 122 | }; 123 | } 124 | 125 | public synchronized void updateAvailability(double newValue) { 126 | availabilityValue = newValue; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /qiro-http/src/test/java/io/qiro/http/SimpleHttpClientTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.http; 2 | 3 | import io.netty.handler.codec.http.*; 4 | import io.qiro.Service; 5 | import io.qiro.ServiceFactory; 6 | import io.qiro.failures.FailureAccrualDetector; 7 | import io.qiro.filter.TimeoutFilter; 8 | import io.qiro.loadbalancing.P2CBalancer; 9 | import io.qiro.pool.WatermarkPool; 10 | import io.qiro.util.HashwheelTimer; 11 | import org.junit.Ignore; 12 | import org.junit.Test; 13 | import org.reactivestreams.Subscriber; 14 | import org.reactivestreams.Subscription; 15 | 16 | import java.net.InetSocketAddress; 17 | import java.util.HashSet; 18 | import java.util.Set; 19 | import java.util.concurrent.CountDownLatch; 20 | import java.util.concurrent.atomic.AtomicInteger; 21 | 22 | import static io.qiro.util.Publishers.*; 23 | import static junit.framework.TestCase.assertEquals; 24 | 25 | public class SimpleHttpClientTest { 26 | 27 | @Ignore 28 | @Test(timeout = 30_000L) 29 | public void testSimpleHttpClient() throws InterruptedException { 30 | NettyTransportConnector connector = new NettyTransportConnector(); 31 | 32 | Set> factories = new HashSet<>(); 33 | 34 | factories.add( 35 | new FailureAccrualDetector<>( 36 | new WatermarkPool<>(1, 10, 128, 37 | connector.toFactory(new InetSocketAddress(8080)) 38 | ) 39 | ) 40 | ); 41 | factories.add( 42 | new FailureAccrualDetector<>( 43 | new WatermarkPool<>(1, 10, 128, 44 | connector.toFactory(new InetSocketAddress(8081)) 45 | ) 46 | ) 47 | ); 48 | 49 | Service service = 50 | new TimeoutFilter(5000, HashwheelTimer.INSTANCE) 51 | .andThen(new P2CBalancer<>(from(factories))) 52 | .toService(); 53 | 54 | // Stack is: 55 | // 56 | // FactoryToService (S) 57 | // TimeoutFilter (S) 58 | // LoadBalancer (SF) 59 | // FailureAccrualDetector (SF) 60 | // WatermarkPool (SF) 61 | // RxNettyService (S) 62 | 63 | int n = 128; 64 | CountDownLatch latch = new CountDownLatch(n); 65 | 66 | int i = 0; 67 | AtomicInteger success = new AtomicInteger(0); 68 | AtomicInteger failure = new AtomicInteger(0); 69 | while (i < n) { 70 | HttpRequest get = createGetRequest("/", "127.0.0.1"); 71 | service.requestResponse(get) 72 | .subscribe(new Subscriber() { 73 | public void onSubscribe(Subscription s) { 74 | s.request(Long.MAX_VALUE); 75 | } 76 | 77 | @Override 78 | public void onNext(HttpResponse httpResponse) { 79 | System.out.println(httpResponse.status()); 80 | } 81 | 82 | @Override 83 | public void onError(Throwable t) { 84 | failure.incrementAndGet(); 85 | System.err.println("Exception " + t); 86 | latch.countDown(); 87 | } 88 | 89 | @Override 90 | public void onComplete() { 91 | success.incrementAndGet(); 92 | latch.countDown(); 93 | } 94 | }); 95 | i += 1; 96 | } 97 | latch.await(); 98 | Thread.sleep(10); 99 | System.out.println("### FINITO ###"); 100 | System.out.println("successes: " + success.get() + " failures: " + failure.get()); 101 | assertEquals(0, failure.get()); 102 | assertEquals(n, success.get()); 103 | } 104 | 105 | private HttpRequest createGetRequest(String path, String host) { 106 | HttpRequest request = new DefaultFullHttpRequest( 107 | HttpVersion.HTTP_1_1, HttpMethod.GET, path); 108 | request.headers().set(HttpHeaderNames.HOST, host); 109 | request.headers().set(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP); 110 | 111 | return request; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /qiro-reactivesocket/src/test/java/io/qiro/reactivesocket/ReactiveSocketServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.reactivesocket; 2 | 3 | import io.qiro.Server; 4 | import io.qiro.Service; 5 | import io.qiro.codec.UTF8Codec; 6 | import io.qiro.util.EmptySubscriber; 7 | import org.junit.Ignore; 8 | import org.junit.Test; 9 | import org.reactivestreams.Publisher; 10 | import org.reactivestreams.Subscriber; 11 | import org.reactivestreams.Subscription; 12 | 13 | import java.util.concurrent.CountDownLatch; 14 | 15 | import static io.qiro.util.Publishers.from; 16 | 17 | public class ReactiveSocketServiceTest { 18 | 19 | @Ignore 20 | @Test(timeout = 30_000L) 21 | public void testReactiveSocketPingPong() throws Exception { 22 | Server server = ServerBuilder.get() 23 | .listen(8888) 24 | .codec(UTF8Codec.INSTANCE) 25 | .build(new Service() { 26 | @Override 27 | public Publisher requestResponse(String input) { 28 | return output -> { 29 | output.onNext(input.toLowerCase()); 30 | output.onComplete(); 31 | }; 32 | } 33 | 34 | @Override 35 | public Publisher requestChannel(Publisher inputs) { 36 | return outputs -> 37 | inputs.subscribe(new Subscriber() { 38 | @Override 39 | public void onSubscribe(Subscription s) { 40 | s.request(128); 41 | } 42 | 43 | @Override 44 | public void onNext(String input) { 45 | if (input.toLowerCase().equals("ping")) { 46 | outputs.onNext("Pong!"); 47 | } else { 48 | outputs.onNext("I don't understand " + input); 49 | } 50 | } 51 | 52 | @Override 53 | public void onError(Throwable t) { 54 | outputs.onError(t); 55 | } 56 | 57 | @Override 58 | public void onComplete() { 59 | outputs.onComplete(); 60 | } 61 | }); 62 | } 63 | 64 | @Override 65 | public double availability() { 66 | return 1.0; 67 | } 68 | 69 | @Override 70 | public Publisher close() { 71 | return Subscriber::onComplete; 72 | } 73 | }); 74 | 75 | 76 | Service client = ClientBuilder.get() 77 | .destination("dns://127.0.0.1:8888") 78 | .codec(UTF8Codec.INSTANCE) 79 | .build(); 80 | 81 | long start = System.nanoTime(); 82 | int n = 1000; 83 | CountDownLatch latch = new CountDownLatch(n); 84 | while (0 < n) { 85 | // client.requestChannel(from("ping", "blabla")).subscribe(new Subscriber() { 86 | client.requestResponse("ping").subscribe(new Subscriber() { 87 | @Override 88 | public void onSubscribe(Subscription s) { 89 | s.request(Long.MAX_VALUE); 90 | } 91 | 92 | @Override 93 | public void onNext(String response) { 94 | // System.out.println("Receiving response: '" + response + "'"); 95 | } 96 | 97 | @Override 98 | public void onError(Throwable t) { 99 | // System.out.println("Receiving an error: " + t); 100 | latch.countDown(); 101 | } 102 | 103 | @Override 104 | public void onComplete() { 105 | latch.countDown(); 106 | } 107 | }); 108 | n -= 1; 109 | } 110 | 111 | latch.await(); 112 | long elapsed = System.nanoTime() - start; 113 | server.close().subscribe(EmptySubscriber.INSTANCE); 114 | 115 | System.out.println("### FINITO ###"); 116 | System.out.println("elapsed: " + elapsed / 1_000_000L); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /qiro-http/src/main/java/io/qiro/http/RxNettyService.java: -------------------------------------------------------------------------------- 1 | package io.qiro.http; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.handler.codec.http.HttpRequest; 5 | import io.netty.handler.codec.http.HttpResponse; 6 | import io.qiro.Service; 7 | import io.reactivex.netty.protocol.http.client.HttpClient; 8 | import io.reactivex.netty.protocol.http.client.HttpClientRequest; 9 | import io.reactivex.netty.protocol.http.client.HttpClientResponse; 10 | import org.reactivestreams.Publisher; 11 | import org.reactivestreams.Subscriber; 12 | import org.reactivestreams.Subscription; 13 | 14 | import java.net.ConnectException; 15 | import java.net.SocketAddress; 16 | import java.net.SocketException; 17 | 18 | 19 | class RxNettyService implements Service { 20 | private final SocketAddress address; 21 | private final Subscriber> subscriber; 22 | private HttpClient client; 23 | private double availability; 24 | 25 | public RxNettyService( 26 | SocketAddress address, 27 | Subscriber> subscriber 28 | ) { 29 | this.address = address; 30 | this.subscriber = subscriber; 31 | 32 | // this should be done outside, but AFAIK establishing a connection is lazy in RxNetty 33 | client = HttpClient.newClient(address); 34 | this.availability = 1.0; 35 | } 36 | 37 | public RxNettyService( 38 | HttpClient client, 39 | Subscriber> subscriber 40 | ) { 41 | this.address = null; 42 | this.subscriber = subscriber; 43 | this.client = client; 44 | this.availability = 1.0; 45 | } 46 | 47 | 48 | @Override 49 | public Publisher requestChannel(Publisher inputs) { 50 | return new Publisher() { 51 | @Override 52 | public void subscribe(Subscriber respSubcriber) { 53 | inputs.subscribe(new Subscriber() { 54 | @Override 55 | public void onSubscribe(Subscription s) { 56 | respSubcriber.onSubscribe(s); 57 | } 58 | 59 | @Override 60 | public void onNext(HttpRequest request) { 61 | synchronized (RxNettyService.this) { 62 | // Hack to trigger reconnection 63 | if (client == null) { 64 | client = HttpClient.newClient(address); 65 | } 66 | } 67 | HttpClientRequest rxNettyRequest = client.createRequest( 68 | request.protocolVersion(), request.method(), request.uri()); 69 | 70 | rxNettyRequest.subscribe(new rx.Subscriber>() { 71 | @Override 72 | public void onCompleted() { 73 | respSubcriber.onComplete(); 74 | } 75 | 76 | @Override 77 | public void onError(Throwable requestFailure) { 78 | // Hack to trigger reconnection 79 | if (requestFailure instanceof ConnectException 80 | || requestFailure instanceof SocketException) { 81 | synchronized (RxNettyService.this) { 82 | client = null; 83 | } 84 | } 85 | respSubcriber.onError(requestFailure); 86 | } 87 | 88 | @Override 89 | public void onNext(HttpClientResponse response) { 90 | HttpResponse httpResponse = RxNettyResponse.wrap(response); 91 | respSubcriber.onNext(httpResponse); 92 | } 93 | }); 94 | } 95 | 96 | @Override 97 | public void onError(Throwable inputFailure) { 98 | respSubcriber.onError(inputFailure); 99 | } 100 | 101 | @Override 102 | public void onComplete() { 103 | // inputs complete 104 | } 105 | }); 106 | } 107 | }; 108 | } 109 | 110 | @Override 111 | public double availability() { 112 | // AFAIK: No current way to know the state of the client 113 | return availability; 114 | } 115 | 116 | @Override 117 | public Publisher close() { 118 | // TODO: How to close a RxNetty client 119 | return s -> { 120 | s.onNext(null); 121 | s.onComplete(); 122 | }; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/pool/WatermarkPool.java: -------------------------------------------------------------------------------- 1 | package io.qiro.pool; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.ServiceProxy; 6 | import io.qiro.util.Availabilities; 7 | import io.qiro.util.EmptySubscriber; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Deque; 14 | import java.util.List; 15 | import java.util.concurrent.ConcurrentLinkedDeque; 16 | 17 | public class WatermarkPool implements ServiceFactory { 18 | private final int low; 19 | private final int high; 20 | private final int maxBuffer; 21 | private final ServiceFactory underlying; 22 | private final List> services; 23 | private final Deque> queue; 24 | private final Deque>> waiters; 25 | private int createdServices; 26 | 27 | public WatermarkPool(int low, int high, int maxBuffer, ServiceFactory underlying) { 28 | this.low = low; 29 | this.high = high; 30 | this.maxBuffer = maxBuffer; 31 | this.underlying = underlying; 32 | services = new ArrayList<>(); 33 | queue = new ConcurrentLinkedDeque<>(); 34 | waiters = new ConcurrentLinkedDeque<>(); 35 | createdServices = 0; 36 | } 37 | 38 | @Override 39 | public Publisher> apply() { 40 | return new Publisher>() { 41 | @Override 42 | public void subscribe(Subscriber> subscriber) { 43 | System.out.println("WatermarkPool: subscribing createdServices:" + 44 | createdServices + ", queue:" + queue + ", waiters:" + waiters); 45 | synchronized (WatermarkPool.this) { 46 | Service service = queue.pollFirst(); 47 | if (service != null) { 48 | subscriber.onNext(service); 49 | } else if (createdServices < high) { 50 | createdServices += 1; 51 | createAndPublishService(subscriber); 52 | } else if (waiters.size() >= maxBuffer) { 53 | subscriber.onError(new java.lang.Exception( 54 | "WatermarkPool: Max Capacity (" + high + ")")); 55 | } else { 56 | waiters.add(subscriber); 57 | } 58 | } 59 | } 60 | }; 61 | } 62 | 63 | private void createAndPublishService(final Subscriber> svcSubscriber) { 64 | underlying.apply().subscribe(new Subscriber>() { 65 | @Override 66 | public void onSubscribe(Subscription s) { 67 | s.request(1L); 68 | } 69 | 70 | @Override 71 | public void onNext(Service service) { 72 | services.add(service); 73 | Service proxy = new ServiceProxy(service) { 74 | @Override 75 | public Publisher close() { 76 | return closeSubscriber -> { 77 | synchronized (WatermarkPool.this) { 78 | if (!waiters.isEmpty()) { 79 | Subscriber> waitingSubscriber = 80 | waiters.pollFirst(); 81 | waitingSubscriber.onNext(this); 82 | } else if (createdServices > low) { 83 | createdServices -= 1; 84 | underlying.close().subscribe(closeSubscriber); 85 | } else { 86 | System.out.println("WatermarkPool: moving svc " + 87 | this + " to the queue"); 88 | queue.addLast(this); 89 | } 90 | } 91 | }; 92 | } 93 | }; 94 | System.out.println("WatermarkPool: Creating ServiceProxy " + proxy); 95 | svcSubscriber.onNext(proxy); 96 | } 97 | 98 | @Override 99 | public void onError(Throwable serviceCreationFailure) { 100 | svcSubscriber.onError(serviceCreationFailure); 101 | } 102 | 103 | @Override 104 | public void onComplete() { 105 | svcSubscriber.onComplete(); 106 | } 107 | }); 108 | } 109 | 110 | @Override 111 | public double availability() { 112 | if (createdServices < low) { 113 | return 1.0; 114 | } else { 115 | return Availabilities.avgOfServices(services); 116 | } 117 | } 118 | 119 | @Override 120 | public Publisher close() { 121 | return subscriber -> { 122 | createdServices = 2 * high; 123 | waiters.forEach(sub -> sub.onError( 124 | new Exception("Closing the WatermarkPool, killing the waiters"))); 125 | services.forEach(svc -> svc.close().subscribe(EmptySubscriber.INSTANCE)); 126 | subscriber.onNext(null); 127 | subscriber.onComplete(); 128 | }; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/pool/WatermarkPoolTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro.pool; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactories; 5 | import io.qiro.ServiceFactory; 6 | import io.qiro.testing.LoggerSubscriber; 7 | import io.qiro.testing.TestingService; 8 | import org.junit.Test; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.concurrent.atomic.AtomicInteger; 13 | 14 | import static io.qiro.util.Publishers.just; 15 | import static junit.framework.TestCase.assertFalse; 16 | import static junit.framework.TestCase.assertTrue; 17 | 18 | public class WatermarkPoolTest { 19 | @Test(timeout = 10_000L) 20 | public void testWatermarkPool() { 21 | TestingService testingSvc0 = 22 | new TestingService<>(Object::toString); 23 | TestingService testingSvc1 = 24 | new TestingService<>(Object::toString); 25 | List> testingServices = Arrays.asList( 26 | testingSvc0, 27 | testingSvc1 28 | ); 29 | 30 | int lowWatermark = 1; 31 | int highWatermark = 2; 32 | 33 | AtomicInteger serviceCreated = new AtomicInteger(0); 34 | ServiceFactory factory = ServiceFactories.fromFunctions( 35 | () -> { 36 | System.out.println("Creating a TestingService!"); 37 | if (serviceCreated.getAndIncrement() == highWatermark) { 38 | assertTrue("Shouldn't create more than " 39 | + highWatermark + " services!", false); 40 | } 41 | int index = (serviceCreated.get() - 1) % testingServices.size(); 42 | return testingServices.get(index); 43 | }, 44 | () -> null 45 | ); 46 | Service service = 47 | new WatermarkPool<>(lowWatermark, highWatermark, 0, factory).toService(); 48 | 49 | LoggerSubscriber subscriber0 = new LoggerSubscriber<>("request 0"); 50 | LoggerSubscriber subscriber1 = new LoggerSubscriber<>("request 1"); 51 | LoggerSubscriber subscriber2 = new LoggerSubscriber<>("request 2"); 52 | LoggerSubscriber subscriber3 = new LoggerSubscriber<>("request 3"); 53 | 54 | service.requestResponse(0).subscribe(subscriber0); 55 | service.requestResponse(1).subscribe(subscriber1); 56 | service.requestResponse(2).subscribe(subscriber2); // -> error maxCapacity 57 | assertFalse(subscriber0.isComplete()); 58 | assertFalse(subscriber1.isComplete()); 59 | assertTrue(subscriber2.isError()); 60 | assertTrue(testingSvc0.isOpen()); 61 | assertTrue(testingSvc1.isOpen()); 62 | 63 | testingSvc0.respond(); 64 | testingSvc0.complete(); 65 | assertTrue(subscriber0.isComplete()); 66 | assertFalse(subscriber1.isComplete()); 67 | assertTrue(testingSvc0.isClosed()); // > low watermark, svc has been closed 68 | assertTrue(testingSvc1.isOpen()); 69 | 70 | 71 | testingSvc1.respond(); 72 | testingSvc1.complete(); 73 | assertTrue(subscriber0.isComplete()); 74 | assertTrue(subscriber1.isComplete()); 75 | assertTrue(testingSvc1.isOpen()); // > low watermark, svc stays open 76 | 77 | service.requestResponse(3).subscribe(subscriber3); 78 | testingSvc0.respond(); 79 | testingSvc0.complete(); 80 | } 81 | 82 | @Test(timeout = 10_000L) 83 | public void testWatermarkPoolBuffering() { 84 | TestingService testingSvc0 = 85 | new TestingService<>(Object::toString); 86 | TestingService testingSvc1 = 87 | new TestingService<>(Object::toString); 88 | List> testingServices = Arrays.asList( 89 | testingSvc0, 90 | testingSvc1 91 | ); 92 | 93 | int lowWatermark = 1; 94 | int highWatermark = 2; 95 | int buffer = 1; 96 | 97 | AtomicInteger serviceCreated = new AtomicInteger(0); 98 | ServiceFactory factory = ServiceFactories.fromFunctions( 99 | () -> { 100 | System.out.println("Creating a TestingService!"); 101 | if (serviceCreated.getAndIncrement() == highWatermark) { 102 | assertTrue("Shouldn't create more than " 103 | + highWatermark + " services!", false); 104 | } 105 | int index = (serviceCreated.get() - 1) % testingServices.size(); 106 | return testingServices.get(index); 107 | }, 108 | () -> { 109 | assertTrue("Shouldn't destroy a service!", false); 110 | return null; 111 | } 112 | ); 113 | Service service = 114 | new WatermarkPool<>(lowWatermark, highWatermark, buffer, factory).toService(); 115 | 116 | LoggerSubscriber subscriber0 = new LoggerSubscriber<>("request 0"); 117 | LoggerSubscriber subscriber1 = new LoggerSubscriber<>("request 1"); 118 | LoggerSubscriber subscriber2 = new LoggerSubscriber<>("request 2"); 119 | LoggerSubscriber subscriber3 = new LoggerSubscriber<>("request 3"); 120 | 121 | service.requestResponse(0).subscribe(subscriber0); 122 | service.requestResponse(1).subscribe(subscriber1); 123 | service.requestResponse(2).subscribe(subscriber2); // -> svc is buffered 124 | service.requestResponse(3).subscribe(subscriber3); // -> error max # services 125 | assertFalse(subscriber0.isComplete()); 126 | assertFalse(subscriber1.isComplete()); 127 | assertFalse(subscriber2.isComplete()); 128 | assertTrue(subscriber3.isError()); 129 | 130 | testingSvc0.respond(0); //only reply to the first req to svc_0 131 | testingSvc0.complete(0); // this will close the service, and put it back to the WP queue 132 | // without the (0) it will also reply to the 2nd req ("request 2") 133 | assertTrue(subscriber0.isComplete()); 134 | assertFalse(subscriber1.isComplete()); 135 | assertFalse(subscriber2.isComplete()); 136 | 137 | testingSvc1.respond(); 138 | testingSvc1.complete(); 139 | assertTrue(subscriber0.isComplete()); 140 | assertTrue(subscriber1.isComplete()); 141 | assertFalse(subscriber2.isComplete()); 142 | 143 | testingSvc0.respond(); 144 | testingSvc0.complete(); 145 | assertFalse(subscriber3.isComplete()); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/Filter.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.reactivestreams.Publisher; 4 | import org.reactivestreams.Subscriber; 5 | import org.reactivestreams.Subscription; 6 | 7 | import static io.qiro.util.Publishers.just; 8 | import static io.qiro.util.Publishers.never; 9 | 10 | public interface Filter { 11 | default Publisher fireAndForget( 12 | FilterReq input, 13 | Service service 14 | ) { 15 | return s -> { 16 | requestResponse(input, service); 17 | s.onComplete(); 18 | }; 19 | } 20 | 21 | default Publisher requestResponse( 22 | FilterReq input, 23 | Service service 24 | ) { 25 | return requestStream(input, service); 26 | } 27 | 28 | default Publisher requestStream( 29 | FilterReq input, 30 | Service service 31 | ) { 32 | return requestSubscription(input, service); 33 | } 34 | 35 | default Publisher requestSubscription( 36 | FilterReq input, 37 | Service service 38 | ) { 39 | return requestChannel(just(input), service); 40 | } 41 | 42 | public Publisher requestChannel( 43 | Publisher inputs, 44 | Service service 45 | ); 46 | 47 | // FilterReq --> Req --> FilterReq2 48 | // this other 49 | // FilterResp <-- Resp <-- FilterResp2 50 | // 51 | default Filter andThen( 52 | Filter other 53 | ) { 54 | return new Filter() { 55 | @Override 56 | public Publisher fireAndForget( 57 | FilterReq input, 58 | Service service 59 | ) { 60 | return filteredService(other, service).fireAndForget(input); 61 | } 62 | 63 | @Override 64 | public Publisher requestResponse( 65 | FilterReq input, 66 | Service service 67 | ) { 68 | return filteredService(other, service).requestResponse(input); 69 | } 70 | 71 | @Override 72 | public Publisher requestStream( 73 | FilterReq input, 74 | Service service 75 | ) { 76 | return filteredService(other, service).requestStream(input); 77 | } 78 | 79 | @Override 80 | public Publisher requestSubscription( 81 | FilterReq input, 82 | Service service 83 | ) { 84 | return filteredService(other, service).requestSubscription(input); 85 | } 86 | 87 | @Override 88 | public Publisher requestChannel( 89 | Publisher inputs, 90 | Service service 91 | ) { 92 | return filteredService(other, service).requestChannel(inputs); 93 | } 94 | 95 | private Service filteredService( 96 | Filter other, 97 | Service service 98 | ) { 99 | return Filter.this.andThen(other.andThen(service)); 100 | } 101 | }; 102 | } 103 | 104 | default Service andThen(Service service) { 105 | return new Service() { 106 | @Override 107 | public Publisher fireAndForget(FilterReq filterReq) { 108 | return Filter.this.fireAndForget(filterReq, service); 109 | } 110 | 111 | @Override 112 | public Publisher requestResponse(FilterReq filterReq) { 113 | return Filter.this.requestResponse(filterReq, service); 114 | } 115 | 116 | @Override 117 | public Publisher requestStream(FilterReq filterReq) { 118 | return Filter.this.requestStream(filterReq, service); 119 | } 120 | 121 | @Override 122 | public Publisher requestSubscription(FilterReq filterReq) { 123 | return Filter.this.requestSubscription(filterReq, service); 124 | } 125 | 126 | @Override 127 | public Publisher requestChannel(Publisher inputs) { 128 | return Filter.this.requestChannel(inputs, service); 129 | } 130 | 131 | @Override 132 | public double availability() { 133 | return service.availability(); 134 | } 135 | 136 | @Override 137 | public Publisher close() { 138 | return service.close(); 139 | } 140 | }; 141 | } 142 | 143 | default ServiceFactory andThen(ServiceFactory factory) { 144 | return new ServiceFactory() { 145 | @Override 146 | public Publisher> apply() { 147 | return svcSubscriber -> 148 | factory.apply().subscribe(new Subscriber>() { 149 | @Override 150 | public void onSubscribe(Subscription subscription) { 151 | svcSubscriber.onSubscribe(subscription); 152 | } 153 | 154 | @Override 155 | public void onNext(Service service) { 156 | final Service filterService = 157 | Filter.this.andThen(service); 158 | svcSubscriber.onNext(filterService); 159 | } 160 | 161 | @Override 162 | public void onError(Throwable t) { 163 | svcSubscriber.onError(t); 164 | } 165 | 166 | @Override 167 | public void onComplete() { 168 | svcSubscriber.onComplete(); 169 | } 170 | }); 171 | } 172 | 173 | @Override 174 | public double availability() { 175 | return factory.availability(); 176 | } 177 | 178 | @Override 179 | public Publisher close() { 180 | return factory.close(); 181 | } 182 | }; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /qiro-reactivesocket/src/main/java/io/qiro/reactivesocket/ServerBuilder.java: -------------------------------------------------------------------------------- 1 | package io.qiro.reactivesocket; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelOption; 5 | import io.qiro.Server; 6 | import io.qiro.Service; 7 | import io.qiro.ServiceFactory; 8 | import io.qiro.builder.*; 9 | import io.qiro.codec.Codec; 10 | import io.reactivesocket.ConnectionSetupHandler; 11 | import io.reactivesocket.ConnectionSetupPayload; 12 | import io.reactivesocket.Payload; 13 | import io.reactivesocket.RequestHandler; 14 | import io.reactivesocket.exceptions.SetupException; 15 | import io.reactivesocket.websocket.rxnetty.server.ReactiveSocketWebSocketServer; 16 | import io.reactivex.netty.protocol.http.server.HttpServer; 17 | import org.reactivestreams.Publisher; 18 | import org.reactivestreams.Subscriber; 19 | import org.reactivestreams.Subscription; 20 | 21 | import java.net.SocketAddress; 22 | import java.nio.ByteBuffer; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import static io.qiro.util.Publishers.*; 26 | 27 | public class ServerBuilder extends io.qiro.builder.ServerBuilder { 28 | private static ByteBuffer EMPTY = ByteBuffer.allocate(0); 29 | 30 | 31 | public static io.qiro.builder.ServerBuilder get() { 32 | return new ServerBuilder<>(); 33 | } 34 | 35 | @Override 36 | protected Server start( 37 | ServiceFactory factory, 38 | SocketAddress address, 39 | Codec codec 40 | ) { 41 | 42 | ConnectionSetupHandler setupHandler = new ConnectionSetupHandler() { 43 | @Override 44 | public RequestHandler apply(ConnectionSetupPayload setupPayload) throws SetupException { 45 | return new RequestHandler() { 46 | @Override 47 | public Publisher handleRequestResponse(Payload payload) { 48 | return handleRequestStream(payload); 49 | } 50 | 51 | @Override 52 | public Publisher handleRequestStream(Payload payload) { 53 | return handleSubscription(payload); 54 | } 55 | 56 | @Override 57 | public Publisher handleSubscription(Payload payload) { 58 | return handleChannel(payload, never()); 59 | } 60 | 61 | @Override 62 | public Publisher handleFireAndForget(Payload payload) { 63 | return Subscriber::onComplete; 64 | } 65 | 66 | @Override 67 | public Publisher handleChannel(Payload initial, Publisher inputs) { 68 | return subscriber -> { 69 | Publisher> servicePublisher = factory.apply(); 70 | servicePublisher.subscribe(new Subscriber>() { 71 | @Override 72 | public void onSubscribe(Subscription s) { 73 | s.request(1L); 74 | } 75 | 76 | @Override 77 | public void onNext(Service service) { 78 | Publisher requests = map(inputs, 79 | payload -> codec.decode(payload.getData()) 80 | ); 81 | 82 | service.requestChannel(requests).subscribe(new Subscriber() { 83 | @Override 84 | public void onSubscribe(Subscription s) { 85 | subscriber.onSubscribe(s); 86 | } 87 | 88 | @Override 89 | public void onNext(Resp resp) { 90 | Payload responsePayload = new Payload() { 91 | 92 | @Override 93 | public ByteBuffer getData() { 94 | return codec.encode(resp); 95 | } 96 | 97 | @Override 98 | public ByteBuffer getMetadata() { 99 | return EMPTY; 100 | } 101 | }; 102 | subscriber.onNext(responsePayload); 103 | } 104 | 105 | @Override 106 | public void onError(Throwable t) { 107 | subscriber.onError(t); 108 | } 109 | 110 | @Override 111 | public void onComplete() { 112 | subscriber.onComplete(); 113 | } 114 | }); 115 | } 116 | 117 | @Override 118 | public void onError(Throwable t) { 119 | subscriber.onError(t); 120 | } 121 | 122 | @Override 123 | public void onComplete() {} 124 | }); 125 | }; 126 | } 127 | 128 | @Override 129 | public Publisher handleMetadataPush(Payload payload) { 130 | return s -> { 131 | // TODO: implement 132 | }; 133 | } 134 | }; 135 | } 136 | }; 137 | 138 | ReactiveSocketWebSocketServer serverHandler = 139 | ReactiveSocketWebSocketServer.create(setupHandler); 140 | 141 | HttpServer server = HttpServer.newServer(address) 142 | .clientChannelOption(ChannelOption.AUTO_READ, true) 143 | .start((req, resp) -> 144 | resp.acceptWebSocketUpgrade(serverHandler::acceptWebsocket)); 145 | 146 | return new Server() { 147 | @Override 148 | public SocketAddress boundAddress() { 149 | return server.getServerAddress(); 150 | } 151 | 152 | @Override 153 | public Publisher await() { 154 | return s -> { 155 | server.awaitShutdown(); 156 | s.onComplete(); 157 | }; 158 | } 159 | 160 | @Override 161 | public Publisher close(long gracePeriod, TimeUnit unit) { 162 | return s -> { 163 | server.shutdown(); 164 | s.onComplete(); 165 | }; 166 | } 167 | }; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/failures/FailureAccrualDetector.java: -------------------------------------------------------------------------------- 1 | package io.qiro.failures; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.ServiceProxy; 6 | import io.qiro.util.Clock; 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import static io.qiro.util.Publishers.map; 14 | 15 | public class FailureAccrualDetector implements ServiceFactory { 16 | private static class TimestampRPC { 17 | private long ts; 18 | private boolean success; 19 | 20 | TimestampRPC(long ts, boolean success) { 21 | this.ts = ts; 22 | this.success = success; 23 | } 24 | 25 | public long ts() { 26 | return ts; 27 | } 28 | 29 | public boolean isSuccess() { 30 | return success; 31 | } 32 | 33 | public static TimestampRPC zero() { 34 | return new TimestampRPC(Long.MIN_VALUE, true); 35 | } 36 | 37 | public static TimestampRPC success(long ts) { 38 | return new TimestampRPC(ts, true); 39 | } 40 | 41 | public static TimestampRPC failure(long ts) { 42 | return new TimestampRPC(ts, false); 43 | } 44 | } 45 | 46 | private final ServiceFactory underlying; 47 | private final TimestampRPC[] history; 48 | private final long expiration; 49 | private final Clock clock; 50 | private int historyIndex; 51 | 52 | public FailureAccrualDetector( 53 | ServiceFactory underlying, 54 | int requestHistory, 55 | long expiration, 56 | TimeUnit unit, 57 | Clock clock 58 | ) { 59 | this.underlying = underlying; 60 | this.history = new TimestampRPC[requestHistory]; 61 | for (int i = 0; i < history.length; i++) { 62 | history[i] = TimestampRPC.zero(); 63 | } 64 | this.historyIndex = 0; 65 | this.expiration = TimeUnit.MILLISECONDS.convert(expiration, unit); 66 | this.clock = clock; 67 | } 68 | 69 | public FailureAccrualDetector( 70 | ServiceFactory underlying, 71 | int history, 72 | long expiration, 73 | TimeUnit unit 74 | ) { 75 | this(underlying, history, expiration, unit, Clock.SYSTEM_CLOCK); 76 | } 77 | 78 | public FailureAccrualDetector(ServiceFactory underlying) { 79 | this(underlying, 5, 30, TimeUnit.SECONDS); 80 | } 81 | 82 | @Override 83 | public Publisher> apply() { 84 | return svcSubscriber -> 85 | underlying.apply().subscribe(new Subscriber>() { 86 | @Override 87 | public void onSubscribe(Subscription s) { 88 | svcSubscriber.onSubscribe(s); 89 | } 90 | 91 | @Override 92 | public void onNext(Service service) { 93 | markSuccess(); 94 | FailureAccrualService accrualService = 95 | new FailureAccrualService<>(service); 96 | svcSubscriber.onNext(accrualService); 97 | } 98 | 99 | @Override 100 | public void onError(Throwable serviceCreationFailure) { 101 | markFailure(); 102 | svcSubscriber.onError(serviceCreationFailure); 103 | } 104 | 105 | @Override 106 | public void onComplete() { 107 | svcSubscriber.onComplete(); 108 | } 109 | }); 110 | } 111 | 112 | private synchronized void markFailure() { 113 | history[historyIndex++ % history.length] = TimestampRPC.failure(clock.nowMs()); 114 | } 115 | 116 | private synchronized void markSuccess() { 117 | history[historyIndex++ % history.length] = TimestampRPC.success(clock.nowMs()); 118 | } 119 | 120 | @Override 121 | public synchronized double availability() { 122 | long now = clock.nowMs(); 123 | int successes = 0; 124 | int failures = 0; 125 | for (TimestampRPC rpc: history) { 126 | if (rpc.ts() >= now - expiration) { 127 | if (rpc.isSuccess()) 128 | successes += 1; 129 | else 130 | failures += 1; 131 | } 132 | } 133 | 134 | if (failures == 0) { 135 | return underlying.availability(); 136 | } else { 137 | double ratio = (double) successes / (successes + failures); 138 | return ratio * underlying.availability(); 139 | } 140 | } 141 | 142 | @Override 143 | public Publisher close() { 144 | return underlying.close(); 145 | } 146 | 147 | 148 | /** 149 | * Measure the number of Success/Error and update counters via markFailure/markSuccess 150 | */ 151 | private class FailureAccrualService extends ServiceProxy { 152 | public FailureAccrualService(Service service) { 153 | super(service); 154 | } 155 | 156 | @Override 157 | public Publisher fireAndForget(Req request) { 158 | return map(requestResponse(request), x -> null); 159 | } 160 | 161 | @Override 162 | public Publisher requestResponse(Req request) { 163 | return requestStream(request); 164 | } 165 | 166 | @Override 167 | public Publisher requestStream(Req request) { 168 | return requestSubscription(request); 169 | } 170 | 171 | @Override 172 | public Publisher requestSubscription(Req request) { 173 | return respSubscriber -> 174 | underlying.requestSubscription(request).subscribe(new Subscriber() { 175 | @Override 176 | public void onSubscribe(Subscription s) { 177 | respSubscriber.onSubscribe(s); 178 | } 179 | 180 | @Override 181 | public void onNext(Resp response) { 182 | respSubscriber.onNext(response); 183 | } 184 | 185 | @Override 186 | public void onError(Throwable responseFailure) { 187 | // TODO: do not count applicative failure 188 | markFailure(); 189 | respSubscriber.onError(responseFailure); 190 | } 191 | 192 | @Override 193 | public void onComplete() { 194 | markSuccess(); 195 | respSubscriber.onComplete(); 196 | } 197 | }); 198 | } 199 | 200 | @Override 201 | public Publisher requestChannel(Publisher requests) { 202 | return respSubscriber -> 203 | underlying.requestChannel(requests).subscribe(new Subscriber() { 204 | @Override 205 | public void onSubscribe(Subscription s) { 206 | respSubscriber.onSubscribe(s); 207 | } 208 | 209 | @Override 210 | public void onNext(Resp response) { 211 | respSubscriber.onNext(response); 212 | } 213 | 214 | @Override 215 | public void onError(Throwable responseFailure) { 216 | // TODO: do not count applicative failure 217 | markFailure(); 218 | respSubscriber.onError(responseFailure); 219 | } 220 | 221 | @Override 222 | public void onComplete() { 223 | markSuccess(); 224 | respSubscriber.onComplete(); 225 | } 226 | }); 227 | } 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/builder/ClientBuilder.java: -------------------------------------------------------------------------------- 1 | package io.qiro.builder; 2 | 3 | import io.qiro.Service; 4 | import io.qiro.ServiceFactory; 5 | import io.qiro.codec.Codec; 6 | import io.qiro.failures.FailFastFactory; 7 | import io.qiro.failures.FailureAccrualDetector; 8 | import io.qiro.loadbalancing.P2CBalancer; 9 | import io.qiro.pool.WatermarkPool; 10 | import io.qiro.resolver.DnsResolver; 11 | import io.qiro.resolver.Resolvers; 12 | import io.qiro.resolver.TransportConnector; 13 | import io.qiro.util.Clock; 14 | import io.qiro.util.HashwheelTimer; 15 | import io.qiro.util.Timer; 16 | import org.reactivestreams.Publisher; 17 | import org.reactivestreams.Subscriber; 18 | import org.reactivestreams.Subscription; 19 | 20 | import java.net.SocketAddress; 21 | import java.util.HashSet; 22 | import java.util.Set; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.function.Function; 25 | 26 | public abstract class ClientBuilder { 27 | private static class ConnectionPoolOptions { 28 | private int connectionPoolMin; 29 | private int connectionPoolMax; 30 | private int connectionPoolMaxWaiters; 31 | 32 | ConnectionPoolOptions(int connectionPoolMin, int connectionPoolMax, int connectionPoolMaxWaiters) { 33 | this.connectionPoolMin = connectionPoolMin; 34 | this.connectionPoolMax = connectionPoolMax; 35 | this.connectionPoolMaxWaiters = connectionPoolMaxWaiters; 36 | } 37 | 38 | public int min() { 39 | return connectionPoolMin; 40 | } 41 | 42 | public int max() { 43 | return connectionPoolMax; 44 | } 45 | 46 | public int maxWaiters() { 47 | return connectionPoolMaxWaiters; 48 | } 49 | } 50 | 51 | private static class FailureAccrualOptions { 52 | private int requestHistory; 53 | private long expiration; 54 | private TimeUnit unit; 55 | 56 | private FailureAccrualOptions(int requestHistory, long expiration, TimeUnit unit) { 57 | this.requestHistory = requestHistory; 58 | this.expiration = expiration; 59 | this.unit = unit; 60 | } 61 | 62 | public int history() { 63 | return requestHistory; 64 | } 65 | 66 | public long expiration() { 67 | return expiration; 68 | } 69 | 70 | public TimeUnit expirationUnit() { 71 | return unit; 72 | } 73 | } 74 | 75 | private String remote = null; 76 | private Timer timer = HashwheelTimer.INSTANCE; 77 | private Clock clock = Clock.SYSTEM_CLOCK; 78 | private ConnectionPoolOptions poolOptions = null; 79 | private FailureAccrualOptions accrualOptions = null; 80 | private Codec codec = null; 81 | 82 | public ClientBuilder destination(String remote) { 83 | this.remote = remote; 84 | return this; 85 | } 86 | 87 | public ClientBuilder clock(Clock clock) { 88 | this.clock = clock; 89 | return this; 90 | } 91 | 92 | public ClientBuilder timer(Timer timer) { 93 | this.timer = timer; 94 | return this; 95 | } 96 | 97 | public ClientBuilder codec(Codec codec) { 98 | this.codec = codec; 99 | return this; 100 | } 101 | 102 | public ClientBuilder connectionPool(int min, int max, int maxWaiters) { 103 | if (min == 0 && max == 0 && maxWaiters == 0) { 104 | this.poolOptions = null; 105 | } else { 106 | this.poolOptions = new ConnectionPoolOptions(min, max, maxWaiters); 107 | } 108 | return this; 109 | } 110 | 111 | public ClientBuilder failureAccrual( 112 | int requestHistory, 113 | long expiration, 114 | TimeUnit unit 115 | ) { 116 | this.accrualOptions = new FailureAccrualOptions(requestHistory, expiration, unit); 117 | return this; 118 | } 119 | 120 | public abstract TransportConnector connector(Codec codec); 121 | 122 | public Service build() { 123 | ServiceFactory factory = buildFactory(); 124 | return factory.toService(); 125 | } 126 | 127 | public ServiceFactory buildFactory() { 128 | if (remote == null) { 129 | throw new IllegalStateException("`destination` is uninitialized!\n" 130 | + "please use `ClientBuilder.destination`\n" 131 | + "e.g. ClientBuilder.destination(\"dns://www.netflix.com:80\")"); 132 | } 133 | if (codec == null) { 134 | throw new IllegalStateException("`codec` is uninitialized!\n" 135 | + "please use `ClientBuilder.codec`\n" 136 | + "e.g. ClientBuilder.codec(UTF8Codec.INSTANCE)"); 137 | } 138 | 139 | DnsResolver resolver = new DnsResolver(); 140 | Publisher> addresses = resolver.resolve(remote); 141 | Publisher>> factories = 142 | Resolvers.resolveFactory(addresses, connector(codec)); 143 | 144 | factories = decorateWithConnectionPool(factories); 145 | factories = decoracteWithFailureDetection(factories); 146 | factories = decoracteWithFailFast(factories); 147 | 148 | ServiceFactory balancedFactory = new P2CBalancer<>(factories); 149 | return balancedFactory; 150 | } 151 | 152 | private Publisher>> decoracteWithFailFast( 153 | Publisher>> factories 154 | ) { 155 | if (accrualOptions == null) { 156 | return factories; 157 | } else { 158 | return decorate(factories, factory -> 159 | new FailFastFactory<>(factory, timer) 160 | ); 161 | } 162 | } 163 | 164 | 165 | private Publisher>> decoracteWithFailureDetection( 166 | Publisher>> factories 167 | ) { 168 | if (accrualOptions == null) { 169 | return factories; 170 | } else { 171 | return decorate(factories, factory -> 172 | new FailureAccrualDetector<>(factory, accrualOptions.history(), 173 | accrualOptions.expiration(), accrualOptions.expirationUnit(), clock) 174 | ); 175 | } 176 | } 177 | 178 | private Publisher>> decorateWithConnectionPool( 179 | Publisher>> factories 180 | ) { 181 | if (poolOptions == null) { 182 | return factories; 183 | } else { 184 | return decorate(factories, factory -> 185 | new WatermarkPool<>( 186 | poolOptions.min(), 187 | poolOptions.max(), 188 | poolOptions.maxWaiters(), 189 | factory 190 | )); 191 | } 192 | } 193 | 194 | protected Publisher>> decorate( 195 | Publisher>> factories, 196 | Function, ServiceFactory> function 197 | ) { 198 | return subscriber -> factories.subscribe(new Subscriber>>() { 199 | @Override 200 | public void onSubscribe(Subscription s) { 201 | subscriber.onSubscribe(s); 202 | } 203 | 204 | @Override 205 | public void onNext(Set> serviceFactories) { 206 | Set> newSet = new HashSet<>(); 207 | for (ServiceFactory factory: serviceFactories) { 208 | newSet.add(function.apply(factory)); 209 | } 210 | subscriber.onNext(newSet); 211 | } 212 | 213 | @Override 214 | public void onError(Throwable t) { 215 | subscriber.onError(t); 216 | } 217 | 218 | @Override 219 | public void onComplete() { 220 | subscriber.onComplete(); 221 | } 222 | }); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/LoadBalancerTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import io.qiro.loadbalancing.BalancerFactory; 4 | import io.qiro.loadbalancing.LeastLoadedBalancer; 5 | import io.qiro.loadbalancing.P2CBalancer; 6 | import io.qiro.loadbalancing.RoundRobinBalancer; 7 | import io.qiro.testing.TestingService; 8 | import io.qiro.testing.LoggerSubscriber; 9 | import org.junit.Test; 10 | import org.reactivestreams.Publisher; 11 | 12 | import java.util.HashSet; 13 | import java.util.List; 14 | import java.util.Set; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.function.Function; 17 | 18 | import static io.qiro.util.Publishers.from; 19 | import static io.qiro.util.Publishers.toList; 20 | import static org.junit.Assert.assertEquals; 21 | import static org.junit.Assert.assertTrue; 22 | 23 | public class LoadBalancerTest { 24 | @Test(timeout = 10_000L) 25 | public void testRoundRobinBalancer() throws InterruptedException { 26 | AtomicInteger c0 = new AtomicInteger(0); 27 | ServiceFactory factory0 = createFactory("0", c0); 28 | AtomicInteger c1 = new AtomicInteger(0); 29 | ServiceFactory factory1 = createFactory("1", c1); 30 | Set> factories = new HashSet<>(); 31 | factories.add(factory0); 32 | factories.add(factory1); 33 | 34 | Service service = 35 | new RoundRobinBalancer<>(from(factories)).toService(); 36 | 37 | System.out.println("availability: " + service.availability()); 38 | 39 | List strings1 = toList(service.requestChannel(from(1, 2))); 40 | List strings2 = toList(service.requestChannel(from(3, 4))); 41 | System.out.println(strings1); 42 | System.out.println(strings2); 43 | 44 | assertEquals(2, c0.get()); 45 | assertEquals(2, c1.get()); 46 | } 47 | 48 | private ServiceFactory createFactory(String name, AtomicInteger counter) { 49 | return ServiceFactories.fromFunctions( 50 | () -> Services.fromFunction(x -> { 51 | counter.incrementAndGet(); 52 | System.out.println("Service["+name+"].apply("+x+")"); 53 | return x.toString(); 54 | }), 55 | () -> { 56 | assertTrue("Service["+name+"] shouldn't be closed!", false); 57 | return null; 58 | } 59 | ); 60 | } 61 | 62 | @Test(timeout = 10_000L) 63 | public void testAsynchronousRoundRobin() throws InterruptedException { 64 | testFairBalancing(RoundRobinBalancer::new); 65 | } 66 | 67 | @Test(timeout = 10_000L) 68 | public void testAsynchronousLeastLoadedBalancer() throws InterruptedException { 69 | testFairBalancing(LeastLoadedBalancer::new); 70 | } 71 | 72 | private void testFairBalancing( 73 | Function>>, ServiceFactory> balancerFactory 74 | ) throws InterruptedException { 75 | TestingService service0 = 76 | new TestingService<>(i -> i.toString() + " from service0"); 77 | TestingService service1 = 78 | new TestingService<>(i -> i.toString() + " from service1"); 79 | 80 | ServiceFactory factory0 = ServiceFactories.fromFunctions( 81 | () -> service0, 82 | () -> null 83 | ); 84 | ServiceFactory factory1 = ServiceFactories.fromFunctions( 85 | () -> service1, 86 | () -> null 87 | ); 88 | Set> factories = new HashSet<>(); 89 | factories.add(factory0); 90 | factories.add(factory1); 91 | 92 | ServiceFactory balancer = balancerFactory.apply(from(factories)); 93 | Service service = balancer.toService(); 94 | 95 | service.requestResponse(0).subscribe(new LoggerSubscriber<>("request 0")); 96 | service.requestResponse(1).subscribe(new LoggerSubscriber<>("request 1")); 97 | service.requestResponse(2).subscribe(new LoggerSubscriber<>("request 2")); 98 | service.requestResponse(3).subscribe(new LoggerSubscriber<>("request 3")); 99 | assertEquals("Fair balancing", service0.queueSize(), service1.queueSize()); 100 | 101 | service0.respond(); 102 | service0.complete(); 103 | assertEquals("Service0 load is null", service0.queueSize(), 0); 104 | 105 | service1.respond(); 106 | service1.complete(); 107 | assertEquals("Service1 load is null", service1.queueSize(), 0); 108 | } 109 | 110 | @Test(timeout = 10_000L) 111 | public void testLeastLoadedLoadBalancer() throws InterruptedException { 112 | testMoreFairBalancing(LeastLoadedBalancer::new); 113 | } 114 | 115 | @Test(timeout = 10_000L) 116 | public void testP2CBalancer() throws InterruptedException { 117 | // when the number of factories is 2, the P2C should behave exactly like LeastLoaded 118 | testMoreFairBalancing(P2CBalancer::new); 119 | } 120 | 121 | private void testMoreFairBalancing( 122 | Function>>, ServiceFactory> balancerFactory 123 | ) throws InterruptedException { 124 | // The goal of this test is to ensure that the load balancer always select 125 | // the least loaded ServiceFactory (or one of the least loaded when 126 | // there're more than one with the minimum load) 127 | 128 | TestingService service0 = 129 | new TestingService(i -> i.toString() + " from service0") { 130 | @Override 131 | public Publisher close() { 132 | // allow reuse of the same service for testing purposes 133 | return s -> s.onComplete(); 134 | } 135 | }; 136 | TestingService service1 = 137 | new TestingService(i -> i.toString() + " from service1") { 138 | @Override 139 | public Publisher close() { 140 | // allow reuse of the same service for testing purposes 141 | return s -> s.onComplete(); 142 | } 143 | }; 144 | 145 | ServiceFactory factory0 = ServiceFactories.fromFunctions( 146 | () -> service0, 147 | () -> null 148 | ); 149 | ServiceFactory factory1 = ServiceFactories.fromFunctions( 150 | () -> service1, 151 | () -> null 152 | ); 153 | Set> factories = new HashSet<>(); 154 | factories.add(factory0); 155 | factories.add(factory1); 156 | 157 | Service service = 158 | balancerFactory.apply(from(factories)).toService(); 159 | 160 | service.requestResponse(0).subscribe(new LoggerSubscriber<>("request 0")); 161 | service.requestResponse(1).subscribe(new LoggerSubscriber<>("request 1")); 162 | service.requestResponse(2).subscribe(new LoggerSubscriber<>("request 2")); 163 | service.requestResponse(3).subscribe(new LoggerSubscriber<>("request 3")); 164 | 165 | // loads: [svc0: 2, svc1: 2] 166 | assertEquals("Fair balancing", service0.queueSize(), service1.queueSize()); 167 | 168 | service0.respond(); 169 | service0.complete(); 170 | // loads: [svc0: 0, svc1: 2] 171 | assertEquals(0, service0.queueSize()); 172 | assertEquals(2, service1.queueSize()); 173 | 174 | // loads is [svc0: 0, svc1: 2] 175 | // next call will chose service0 176 | service.requestResponse(4).subscribe(new LoggerSubscriber<>("request 4")); 177 | 178 | // now loads are [svc0: 1, svc1: 2] 179 | assertEquals(1, service0.queueSize()); 180 | assertEquals(2, service1.queueSize()); 181 | 182 | service1.respond(); 183 | service1.complete(); 184 | // loads: [svc0: 1, svc1: 0] 185 | assertEquals(1, service0.queueSize()); 186 | assertEquals(0, service1.queueSize()); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /qiro-core/src/test/java/io/qiro/ServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import org.junit.Test; 4 | import org.reactivestreams.Publisher; 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.concurrent.CountDownLatch; 12 | 13 | import static org.junit.Assert.assertTrue; 14 | import static io.qiro.util.Publishers.*; 15 | 16 | public class ServiceTest { 17 | @Test 18 | public void testBasicService() throws InterruptedException { 19 | testService(new DummyService()); 20 | } 21 | 22 | static void testService(Service aService) throws InterruptedException { 23 | testServiceFnf(aService); 24 | testServiceRequestResponse(aService); 25 | testServiceStream(aService); 26 | testServiceSubscription(aService); 27 | testServiceChannel(aService); 28 | } 29 | 30 | static void testServiceFnf(Service aService) throws InterruptedException { 31 | Publisher forget = aService.fireAndForget(1); 32 | CountDownLatch latch = new CountDownLatch(1); 33 | forget.subscribe(new Subscriber() { 34 | @Override 35 | public void onSubscribe(Subscription s) { 36 | s.request(1L); 37 | } 38 | 39 | @Override 40 | public void onNext(Void aVoid) {} 41 | 42 | @Override 43 | public void onError(Throwable t) { 44 | throw new RuntimeException("Shouldn't generate an exception"); 45 | } 46 | 47 | @Override 48 | public void onComplete() { 49 | latch.countDown(); 50 | } 51 | }); 52 | 53 | latch.await(); 54 | } 55 | 56 | 57 | static void testServiceRequestResponse(Service aService) throws InterruptedException { 58 | Publisher stringPublisher = aService.requestResponse(1); 59 | List strings0 = toList(stringPublisher); 60 | assertTrue(strings0.equals(Arrays.asList("RESPONSE:1"))); 61 | } 62 | 63 | static void testServiceStream(Service aService) throws InterruptedException { 64 | Publisher stringPublisher = aService.requestStream(1); 65 | List strings0 = toList(stringPublisher); 66 | assertTrue(strings0.equals(Arrays.asList( 67 | "STREAM:1", "STREAM(+1):2", "STREAM(*2):2" 68 | ))); 69 | } 70 | 71 | static void testServiceSubscription(Service aService) throws InterruptedException { 72 | Publisher stringPublisher = aService.requestSubscription(1); 73 | CountDownLatch latch3 = new CountDownLatch(3); 74 | List strings0 = new ArrayList<>(); 75 | stringPublisher.subscribe(new Subscriber() { 76 | @Override 77 | public void onSubscribe(Subscription s) { 78 | s.request(Long.MAX_VALUE); 79 | } 80 | 81 | @Override 82 | public void onNext(String s) { 83 | strings0.add(s); 84 | latch3.countDown(); 85 | } 86 | 87 | @Override 88 | public void onError(Throwable t) {} 89 | 90 | @Override 91 | public void onComplete() { 92 | throw new RuntimeException("Should never complete!"); 93 | } 94 | }); 95 | assertTrue(strings0.equals(Arrays.asList( 96 | "SUBSCRIPTION:1", "SUBSCRIPTION(+1):2", "SUBSCRIPTION(*2):2" 97 | ))); 98 | } 99 | 100 | static void testServiceChannel(Service aService) throws InterruptedException { 101 | Publisher stringPublisher = aService.requestChannel(just(1)); 102 | List strings0 = toList(stringPublisher); 103 | assertTrue(strings0.equals(Arrays.asList( 104 | "CHANNEL:1", "CHANNEL(+1):2" 105 | ))); 106 | 107 | Publisher stringPublisher2 = 108 | aService.requestChannel(from(1, 2, 3, 4, 5)); 109 | List strings1 = toList(stringPublisher2); 110 | assertTrue(strings1.equals(Arrays.asList( 111 | "CHANNEL:1", "CHANNEL(+1):2", 112 | "CHANNEL:2", "CHANNEL(+1):3", 113 | "CHANNEL:3", "CHANNEL(+1):4", 114 | "CHANNEL:4", "CHANNEL(+1):5", 115 | "CHANNEL:5", "CHANNEL(+1):6" 116 | ))); 117 | 118 | Publisher doubles = from(1.0, 2.0, 3.0, 4.0, 5.0); 119 | Filter filter = 120 | Filters.fromFunction(x -> (int) (2 * x), str -> "'" + str + "'"); 121 | Publisher apply = filter.requestChannel(doubles, aService); 122 | List strings2 = toList(apply); 123 | assertTrue(strings2.equals(Arrays.asList( 124 | "'CHANNEL:2'", "'CHANNEL(+1):3'", 125 | "'CHANNEL:4'", "'CHANNEL(+1):5'", 126 | "'CHANNEL:6'", "'CHANNEL(+1):7'", 127 | "'CHANNEL:8'", "'CHANNEL(+1):9'", 128 | "'CHANNEL:10'", "'CHANNEL(+1):11'" 129 | ))); 130 | } 131 | 132 | static class DummyService implements Service { 133 | private boolean closed = false; 134 | 135 | @Override 136 | public Publisher requestChannel(Publisher inputs) { 137 | if (closed) { 138 | throw new RuntimeException("Closed service!"); 139 | } 140 | return subscriber -> { 141 | inputs.subscribe(new Subscriber() { 142 | @Override 143 | public void onSubscribe(Subscription s) { 144 | subscriber.onSubscribe(s); 145 | } 146 | 147 | @Override 148 | public void onNext(Integer integer) { 149 | subscriber.onNext("CHANNEL:" + integer); 150 | subscriber.onNext("CHANNEL(+1):" + (integer + 1)); 151 | } 152 | 153 | @Override 154 | public void onError(Throwable t) { 155 | subscriber.onError(t); 156 | } 157 | 158 | @Override 159 | public void onComplete() { 160 | subscriber.onComplete(); 161 | } 162 | }); 163 | }; 164 | } 165 | 166 | @Override 167 | public Publisher fireAndForget(Integer integer) { 168 | if (closed) { 169 | throw new RuntimeException("Closed service!"); 170 | } 171 | return Subscriber::onComplete; 172 | } 173 | 174 | @Override 175 | public Publisher requestResponse(Integer integer) { 176 | if (closed) { 177 | throw new RuntimeException("Closed service!"); 178 | } 179 | return subsriber -> { 180 | subsriber.onNext("RESPONSE:" + integer); 181 | subsriber.onComplete(); 182 | }; 183 | } 184 | 185 | @Override 186 | public Publisher requestStream(Integer integer) { 187 | if (closed) { 188 | throw new RuntimeException("Closed service!"); 189 | } 190 | return subsriber -> { 191 | subsriber.onNext("STREAM:" + integer); 192 | subsriber.onNext("STREAM(+1):" + (integer + 1)); 193 | subsriber.onNext("STREAM(*2):" + (integer * 2)); 194 | subsriber.onComplete(); 195 | }; 196 | } 197 | 198 | @Override 199 | public Publisher requestSubscription(Integer integer) { 200 | if (closed) { 201 | throw new RuntimeException("Closed service!"); 202 | } 203 | return subsriber -> { 204 | subsriber.onNext("SUBSCRIPTION:" + integer); 205 | subsriber.onNext("SUBSCRIPTION(+1):" + (integer + 1)); 206 | subsriber.onNext("SUBSCRIPTION(*2):" + (integer * 2)); 207 | }; 208 | } 209 | 210 | @Override 211 | public double availability() { 212 | return 1.0; 213 | } 214 | 215 | @Override 216 | public Publisher close() { 217 | return s -> { 218 | closed = true; 219 | s.onComplete(); 220 | }; 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /qiro-core/src/main/java/io/qiro/FactoryToService.java: -------------------------------------------------------------------------------- 1 | package io.qiro; 2 | 3 | import io.qiro.util.EmptySubscriber; 4 | import org.reactivestreams.Publisher; 5 | import org.reactivestreams.Subscriber; 6 | import org.reactivestreams.Subscription; 7 | 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | 11 | public class FactoryToService implements Service { 12 | private ServiceFactory factory; 13 | 14 | public FactoryToService(ServiceFactory factory) { 15 | this.factory = factory; 16 | } 17 | 18 | @Override 19 | public Publisher fireAndForget(Req req) { 20 | return responseSubscriber -> { 21 | Publisher> servicePublisher = factory.apply(); 22 | servicePublisher.subscribe(new Subscriber>() { 23 | private int count = 0; 24 | 25 | @Override 26 | public void onSubscribe(Subscription subscription) { 27 | // request only one service 28 | subscription.request(1L); 29 | } 30 | 31 | @Override 32 | public void onNext(Service service) { 33 | if (count > 1) { 34 | throw new IllegalStateException("Factory produced more than 1 Service!"); 35 | } 36 | count += 1; 37 | 38 | Publisher responses = service.fireAndForget(req); 39 | responses.subscribe(new Subscriber() { 40 | @Override 41 | public void onSubscribe(Subscription s) { 42 | responseSubscriber.onSubscribe(s); 43 | } 44 | 45 | @Override 46 | public void onNext(Void nothing) { 47 | responseSubscriber.onComplete(); 48 | } 49 | 50 | @Override 51 | public void onError(Throwable responseFailure) { 52 | service.close().subscribe(EmptySubscriber.INSTANCE); 53 | responseSubscriber.onError(responseFailure); 54 | } 55 | 56 | @Override 57 | public void onComplete() { 58 | service.close().subscribe(EmptySubscriber.INSTANCE); 59 | responseSubscriber.onComplete(); 60 | } 61 | }); 62 | } 63 | 64 | @Override 65 | public void onError(Throwable serviceCreationFailure) { 66 | responseSubscriber.onError(serviceCreationFailure); 67 | } 68 | 69 | @Override 70 | public void onComplete() { 71 | if (count != 1) { 72 | throw new IllegalStateException("Factory completed with number of " + 73 | "produced services equal to " + count); 74 | } 75 | } 76 | }); 77 | }; 78 | } 79 | 80 | @Override 81 | public Publisher requestResponse(Req req) { 82 | return applyFn(service -> service.requestResponse(req)); 83 | } 84 | 85 | @Override 86 | public Publisher requestStream(Req req) { 87 | return applyFn(service -> service.requestStream(req)); 88 | } 89 | 90 | @Override 91 | public Publisher requestSubscription(Req req) { 92 | return applyFn(service -> service.requestSubscription(req)); 93 | } 94 | 95 | @Override 96 | public Publisher requestChannel(Publisher inputs) { 97 | return responseSubscriber -> { 98 | Publisher> servicePublisher = factory.apply(); 99 | servicePublisher.subscribe(new Subscriber>() { 100 | private int count = 0; 101 | 102 | @Override 103 | public void onSubscribe(Subscription subscription) { 104 | // request only one service 105 | subscription.request(1L); 106 | } 107 | 108 | @Override 109 | public void onNext(Service service) { 110 | if (count > 1) { 111 | throw new IllegalStateException("Factory produced more than 1 Service!"); 112 | } 113 | count += 1; 114 | 115 | Publisher responses = service.requestChannel(inputs); 116 | responses.subscribe(new Subscriber() { 117 | @Override 118 | public void onSubscribe(Subscription s) { 119 | responseSubscriber.onSubscribe(s); 120 | } 121 | 122 | @Override 123 | public void onNext(Resp response) { 124 | responseSubscriber.onNext(response); 125 | } 126 | 127 | @Override 128 | public void onError(Throwable responseFailure) { 129 | service.close().subscribe(EmptySubscriber.INSTANCE); 130 | responseSubscriber.onError(responseFailure); 131 | } 132 | 133 | @Override 134 | public void onComplete() { 135 | service.close().subscribe(EmptySubscriber.INSTANCE); 136 | responseSubscriber.onComplete(); 137 | } 138 | }); 139 | } 140 | 141 | @Override 142 | public void onError(Throwable serviceCreationFailure) { 143 | responseSubscriber.onError(serviceCreationFailure); 144 | } 145 | 146 | @Override 147 | public void onComplete() { 148 | if (count != 1) { 149 | throw new IllegalStateException("Factory completed with number of " + 150 | "produced services equal to " + count); 151 | } 152 | } 153 | }); 154 | }; 155 | } 156 | 157 | @Override 158 | public double availability() { 159 | return factory.availability(); 160 | } 161 | 162 | @Override 163 | public Publisher close() { 164 | return factory.close(); 165 | } 166 | 167 | public Publisher applyFn(Function, Publisher> fn) { 168 | return responseSubscriber -> { 169 | Publisher> servicePublisher = factory.apply(); 170 | servicePublisher.subscribe(new Subscriber>() { 171 | private int count = 0; 172 | 173 | @Override 174 | public void onSubscribe(Subscription subscription) { 175 | // request only one service 176 | subscription.request(1L); 177 | } 178 | 179 | @Override 180 | public void onNext(Service service) { 181 | if (count > 1) { 182 | throw new IllegalStateException("Factory produced more than 1 Service!"); 183 | } 184 | count += 1; 185 | 186 | Publisher responses = fn.apply(service); 187 | responses.subscribe(new Subscriber() { 188 | @Override 189 | public void onSubscribe(Subscription s) { 190 | responseSubscriber.onSubscribe(s); 191 | } 192 | 193 | @Override 194 | public void onNext(Resp response) { 195 | responseSubscriber.onNext(response); 196 | } 197 | 198 | @Override 199 | public void onError(Throwable responseFailure) { 200 | service.close().subscribe(EmptySubscriber.INSTANCE); 201 | responseSubscriber.onError(responseFailure); 202 | } 203 | 204 | @Override 205 | public void onComplete() { 206 | service.close().subscribe(EmptySubscriber.INSTANCE); 207 | responseSubscriber.onComplete(); 208 | } 209 | }); 210 | } 211 | 212 | @Override 213 | public void onError(Throwable serviceCreationFailure) { 214 | responseSubscriber.onError(serviceCreationFailure); 215 | } 216 | 217 | @Override 218 | public void onComplete() { 219 | if (count != 1) { 220 | throw new IllegalStateException("Factory completed with number of " + 221 | "produced services equal to " + count); 222 | } 223 | } 224 | }); 225 | }; 226 | } 227 | } 228 | --------------------------------------------------------------------------------