Implement and test some utilities
This commit is contained in:
parent
77af845a8a
commit
090a47ae86
@ -5,13 +5,26 @@ interfacesData:
|
|||||||
extendInterfaces: [PathDirectoryOptions]
|
extendInterfaces: [PathDirectoryOptions]
|
||||||
PathDirectoryOptions:
|
PathDirectoryOptions:
|
||||||
extendInterfaces: [LuceneDirectoryOptions]
|
extendInterfaces: [LuceneDirectoryOptions]
|
||||||
|
ClientBoundRequest:
|
||||||
|
extendInterfaces: [RPCEvent]
|
||||||
|
ClientBoundResponse:
|
||||||
|
extendInterfaces: [RPCEvent]
|
||||||
|
ServerBoundRequest:
|
||||||
|
extendInterfaces: [RPCEvent]
|
||||||
|
ServerBoundResponse:
|
||||||
|
extendInterfaces: [RPCEvent]
|
||||||
# versions must have only numbers, lowercase letters, dots, dashes. Maximum: 99.999.9999
|
# versions must have only numbers, lowercase letters, dots, dashes. Maximum: 99.999.9999
|
||||||
versions:
|
versions:
|
||||||
0.0.0:
|
0.0.0:
|
||||||
details:
|
details:
|
||||||
changelog: "First version"
|
changelog: "First version"
|
||||||
superTypes:
|
superTypes:
|
||||||
ServerBoundRequest: [
|
RPCEvent: [
|
||||||
|
Empty,
|
||||||
|
Binary,
|
||||||
|
BinaryOptional,
|
||||||
|
SingletonUpdateOldData,
|
||||||
|
GeneratedEntityId,
|
||||||
GetDatabase,
|
GetDatabase,
|
||||||
GetLuceneIndex,
|
GetLuceneIndex,
|
||||||
Disconnect,
|
Disconnect,
|
||||||
@ -19,19 +32,31 @@ versions:
|
|||||||
SingletonGet,
|
SingletonGet,
|
||||||
SingletonSet,
|
SingletonSet,
|
||||||
SingletonUpdateInit,
|
SingletonUpdateInit,
|
||||||
SingletonUpdateEnd
|
SingletonUpdateEnd,
|
||||||
|
RPCCrash
|
||||||
|
]
|
||||||
|
ServerBoundRequest: [
|
||||||
|
GetDatabase,
|
||||||
|
GetLuceneIndex,
|
||||||
|
Disconnect,
|
||||||
|
GetSingleton,
|
||||||
|
SingletonGet,
|
||||||
|
SingletonSet,
|
||||||
|
SingletonUpdateInit
|
||||||
]
|
]
|
||||||
ClientBoundResponse: [
|
ClientBoundResponse: [
|
||||||
Empty,
|
Empty,
|
||||||
GeneratedEntityId,
|
GeneratedEntityId,
|
||||||
Binary,
|
Binary,
|
||||||
BinaryOptional
|
BinaryOptional,
|
||||||
|
RPCCrash
|
||||||
]
|
]
|
||||||
ClientBoundRequest: [
|
ClientBoundRequest: [
|
||||||
SingletonUpdateOldData
|
SingletonUpdateOldData
|
||||||
]
|
]
|
||||||
ServerBoundResponse: [
|
ServerBoundResponse: [
|
||||||
Empty
|
Empty,
|
||||||
|
SingletonUpdateEnd
|
||||||
]
|
]
|
||||||
LuceneDirectoryOptions: [
|
LuceneDirectoryOptions: [
|
||||||
ByteBuffersDirectory,
|
ByteBuffersDirectory,
|
||||||
@ -94,12 +119,12 @@ versions:
|
|||||||
javaClass: java.util.Map<java.lang.String, org.rocksdb.ColumnFamilyHandle>
|
javaClass: java.util.Map<java.lang.String, org.rocksdb.ColumnFamilyHandle>
|
||||||
serializer: it.cavallium.dbengine.database.remote.String2ColumnFamilyHandleMapSerializer
|
serializer: it.cavallium.dbengine.database.remote.String2ColumnFamilyHandleMapSerializer
|
||||||
classes:
|
classes:
|
||||||
|
BoxedRPCEvent:
|
||||||
|
data:
|
||||||
|
val: RPCEvent
|
||||||
|
|
||||||
# Server-bound requests
|
# Server-bound requests
|
||||||
|
|
||||||
BoxedServerBoundRequest:
|
|
||||||
data:
|
|
||||||
val: ServerBoundRequest
|
|
||||||
GetDatabase:
|
GetDatabase:
|
||||||
data:
|
data:
|
||||||
name: String
|
name: String
|
||||||
@ -138,18 +163,16 @@ versions:
|
|||||||
|
|
||||||
# Client-bound responses
|
# Client-bound responses
|
||||||
|
|
||||||
BoxedClientBoundResponse:
|
|
||||||
data:
|
|
||||||
val: ClientBoundResponse
|
|
||||||
GeneratedEntityId:
|
GeneratedEntityId:
|
||||||
data:
|
data:
|
||||||
id: long
|
id: long
|
||||||
|
RPCCrash:
|
||||||
|
data:
|
||||||
|
code: int
|
||||||
|
message: -String
|
||||||
|
|
||||||
# Client-bound requests
|
# Client-bound requests
|
||||||
|
|
||||||
BoxedClientBoundRequest:
|
|
||||||
data:
|
|
||||||
val: ClientBoundRequest
|
|
||||||
SingletonUpdateOldData:
|
SingletonUpdateOldData:
|
||||||
data:
|
data:
|
||||||
exist: boolean
|
exist: boolean
|
||||||
@ -157,9 +180,6 @@ versions:
|
|||||||
|
|
||||||
# Server-bound responses
|
# Server-bound responses
|
||||||
|
|
||||||
BoxedServerBoundResponse:
|
|
||||||
data:
|
|
||||||
val: ServerBoundResponse
|
|
||||||
|
|
||||||
# Data
|
# Data
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import io.micrometer.core.instrument.MeterRegistry;
|
|||||||
import io.net5.buffer.api.Buffer;
|
import io.net5.buffer.api.Buffer;
|
||||||
import io.net5.buffer.api.BufferAllocator;
|
import io.net5.buffer.api.BufferAllocator;
|
||||||
import io.net5.buffer.api.Send;
|
import io.net5.buffer.api.Send;
|
||||||
import io.netty.handler.codec.PrematureChannelClosureException;
|
|
||||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||||
import io.netty.incubator.codec.quic.QuicSslContextBuilder;
|
import io.netty.incubator.codec.quic.QuicSslContextBuilder;
|
||||||
import it.cavallium.dbengine.client.IndicizerAnalyzers;
|
import it.cavallium.dbengine.client.IndicizerAnalyzers;
|
||||||
@ -19,10 +18,8 @@ import it.cavallium.dbengine.database.LLSingleton;
|
|||||||
import it.cavallium.dbengine.database.LLSnapshot;
|
import it.cavallium.dbengine.database.LLSnapshot;
|
||||||
import it.cavallium.dbengine.database.UpdateMode;
|
import it.cavallium.dbengine.database.UpdateMode;
|
||||||
import it.cavallium.dbengine.database.UpdateReturnMode;
|
import it.cavallium.dbengine.database.UpdateReturnMode;
|
||||||
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCClientBoundResponseDecoder;
|
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCEventCodec;
|
||||||
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCClientAlternateDecoder;
|
import it.cavallium.dbengine.database.serialization.SerializationException;
|
||||||
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCServerAlternateDecoder;
|
|
||||||
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCServerBoundRequestDecoder;
|
|
||||||
import it.cavallium.dbengine.database.serialization.SerializationFunction;
|
import it.cavallium.dbengine.database.serialization.SerializationFunction;
|
||||||
import it.cavallium.dbengine.lucene.LuceneHacks;
|
import it.cavallium.dbengine.lucene.LuceneHacks;
|
||||||
import it.cavallium.dbengine.rpc.current.data.BinaryOptional;
|
import it.cavallium.dbengine.rpc.current.data.BinaryOptional;
|
||||||
@ -33,12 +30,15 @@ import it.cavallium.dbengine.rpc.current.data.DatabaseOptions;
|
|||||||
import it.cavallium.dbengine.rpc.current.data.GeneratedEntityId;
|
import it.cavallium.dbengine.rpc.current.data.GeneratedEntityId;
|
||||||
import it.cavallium.dbengine.rpc.current.data.GetDatabase;
|
import it.cavallium.dbengine.rpc.current.data.GetDatabase;
|
||||||
import it.cavallium.dbengine.rpc.current.data.GetSingleton;
|
import it.cavallium.dbengine.rpc.current.data.GetSingleton;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCEvent;
|
||||||
import it.cavallium.dbengine.rpc.current.data.ServerBoundRequest;
|
import it.cavallium.dbengine.rpc.current.data.ServerBoundRequest;
|
||||||
import it.cavallium.dbengine.rpc.current.data.ServerBoundResponse;
|
import it.cavallium.dbengine.rpc.current.data.ServerBoundResponse;
|
||||||
import it.cavallium.dbengine.rpc.current.data.SingletonGet;
|
import it.cavallium.dbengine.rpc.current.data.SingletonGet;
|
||||||
import it.cavallium.dbengine.rpc.current.data.SingletonSet;
|
import it.cavallium.dbengine.rpc.current.data.SingletonSet;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.SingletonUpdateEnd;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.SingletonUpdateInit;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.SingletonUpdateOldData;
|
||||||
import it.cavallium.dbengine.rpc.current.data.nullables.NullableLLSnapshot;
|
import it.cavallium.dbengine.rpc.current.data.nullables.NullableLLSnapshot;
|
||||||
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
|
|
||||||
import it.unimi.dsi.fastutil.bytes.ByteList;
|
import it.unimi.dsi.fastutil.bytes.ByteList;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.net.SocketAddress;
|
import java.net.SocketAddress;
|
||||||
@ -48,10 +48,9 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import reactor.core.publisher.Flux;
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.publisher.Sinks;
|
import reactor.core.publisher.Sinks;
|
||||||
import reactor.core.scheduler.Schedulers;
|
import reactor.core.publisher.Sinks.Empty;
|
||||||
import reactor.netty.incubator.quic.QuicClient;
|
import reactor.netty.incubator.quic.QuicClient;
|
||||||
import reactor.netty.incubator.quic.QuicConnection;
|
import reactor.netty.incubator.quic.QuicConnection;
|
||||||
|
|
||||||
@ -136,73 +135,64 @@ public class LLQuicConnection implements LLDatabaseConnection {
|
|||||||
.thenReturn(this);
|
.thenReturn(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T extends ClientBoundResponse> Mono<T> sendRequest(ServerBoundRequest req) {
|
@SuppressWarnings("unchecked")
|
||||||
return Mono
|
private <T extends ClientBoundResponse> Mono<T> sendRequest(ServerBoundRequest serverBoundRequest) {
|
||||||
.<T>create(sink -> {
|
return QuicUtils.<RPCEvent, RPCEvent>sendSimpleRequest(quicConnection,
|
||||||
var sub = quicConnection
|
RPCEventCodec::new,
|
||||||
.createStream((in, out) -> {
|
RPCEventCodec::new,
|
||||||
var writerMono = out
|
serverBoundRequest
|
||||||
.withConnection(conn -> conn.addHandler(new RPCServerBoundRequestDecoder()))
|
).map(event -> (T) event);
|
||||||
.sendObject(req)
|
|
||||||
.then();
|
|
||||||
var readerMono = in
|
|
||||||
.withConnection(conn -> conn.addHandler(new RPCClientBoundResponseDecoder()))
|
|
||||||
.receiveObject()
|
|
||||||
.doOnNext(result -> {
|
|
||||||
if (result != null) {
|
|
||||||
//noinspection unchecked
|
|
||||||
sink.success((T) result);
|
|
||||||
} else {
|
|
||||||
sink.success();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.take(1, true)
|
|
||||||
.singleOrEmpty()
|
|
||||||
.then();
|
|
||||||
return writerMono
|
|
||||||
.then(readerMono)
|
|
||||||
.doOnCancel(() -> sink.error(new PrematureChannelClosureException("Request failed")));
|
|
||||||
})
|
|
||||||
.log("a", Level.INFO)
|
|
||||||
.subscribeOn(Schedulers.parallel())
|
|
||||||
.subscribe(x -> {}, sink::error);
|
|
||||||
sink.onDispose(sub);
|
|
||||||
})
|
|
||||||
.log("x", Level.INFO);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T extends ClientBoundResponse, U extends ClientBoundRequest> Mono<T> sendUpdateRequest(ServerBoundRequest req,
|
private <T extends ClientBoundResponse, U extends ClientBoundRequest> Mono<T> sendUpdateRequest(ServerBoundRequest serverBoundReq,
|
||||||
Function<U, ServerBoundResponse> updaterFunction) {
|
Function<U, ServerBoundResponse> updaterFunction) {
|
||||||
return Mono
|
return Mono.empty();
|
||||||
.<T>create(sink -> {
|
/*
|
||||||
var sub = quicConnection
|
|
||||||
.createStream((in, out) -> {
|
return Mono.defer(() -> {
|
||||||
var inConn = in.withConnection(conn -> conn.addHandler(new RPCServerAlternateDecoder()));
|
Empty<Void> streamTerminator = Sinks.empty();
|
||||||
var outConn = out.withConnection(conn -> conn.addHandler(new RPCClientAlternateDecoder()));
|
return QuicUtils.createStream(quicConnection, stream -> {
|
||||||
var request2 = Sinks.one();
|
Mono<Void> serverReq = Mono.defer(() -> stream.out()
|
||||||
var writerMono = outConn
|
.withConnection(conn -> conn.addHandler(new RPCCodecs.RPCServerBoundRequestDecoder()))
|
||||||
.sendObject(Mono.<Object>just(req).concatWith(request2.asMono()))
|
.sendObject(serverBoundReq)
|
||||||
.then();
|
.then()).doOnSubscribe(s -> System.out.println("out1"));
|
||||||
var responseMono = inConn
|
//noinspection unchecked
|
||||||
.receiveObject()
|
Mono<U> clientBoundReqMono = Mono.defer(() -> stream.in()
|
||||||
.switchOnFirst((first, flux) -> {
|
.withConnection(conn -> conn.addHandler(new RPCClientBoundRequestDecoder()))
|
||||||
//noinspection unchecked
|
.receiveObject()
|
||||||
var req2 = updaterFunction.apply((U) first);
|
.log("TO_CLIENT_REQ", Level.INFO)
|
||||||
request2.tryEmitValue(req2);
|
.take(1, true)
|
||||||
//noinspection unchecked
|
.singleOrEmpty()
|
||||||
return flux.skip(1).map(resp2 -> (T) resp2);
|
.map(req -> (U) req)
|
||||||
});
|
.doOnSubscribe(s -> System.out.println("in1"))
|
||||||
return writerMono
|
.switchIfEmpty((Mono<U>) QuicUtils.NO_RESPONSE_ERROR)
|
||||||
.thenMany(responseMono)
|
);
|
||||||
.doOnCancel(() -> sink.error(new PrematureChannelClosureException("Request failed")))
|
Mono<Void> serverBoundRespFlux = clientBoundReqMono
|
||||||
.then();
|
.map(updaterFunction)
|
||||||
})
|
.transform(respMono -> Mono.defer(() -> stream.out()
|
||||||
.log("a", Level.INFO)
|
.withConnection(conn -> conn.addHandler(new RPCServerBoundResponseDecoder()))
|
||||||
.subscribeOn(Schedulers.parallel())
|
.sendObject(respMono)
|
||||||
.subscribe(x -> {}, sink::error);
|
.then()
|
||||||
sink.onDispose(sub);
|
.doOnSubscribe(s -> System.out.println("out2"))
|
||||||
})
|
));
|
||||||
.log("x", Level.INFO);
|
//noinspection unchecked
|
||||||
|
Mono<T> clientBoundResponseMono = Mono.defer(() -> stream.in()
|
||||||
|
.withConnection(conn -> conn.addHandler(new RPCClientBoundResponseDecoder()))
|
||||||
|
.receiveObject()
|
||||||
|
.map(resp -> (T) resp)
|
||||||
|
.log("TO_SERVER_RESP", Level.INFO)
|
||||||
|
.take(1, true)
|
||||||
|
.doOnSubscribe(s -> System.out.println("out2"))
|
||||||
|
.singleOrEmpty()
|
||||||
|
.switchIfEmpty((Mono<T>) QuicUtils.NO_RESPONSE_ERROR));
|
||||||
|
return serverReq
|
||||||
|
.then(serverBoundRespFlux)
|
||||||
|
.then(clientBoundResponseMono)
|
||||||
|
.doFinally(s -> streamTerminator.tryEmitEmpty());
|
||||||
|
}, streamTerminator.asMono()).single();
|
||||||
|
});
|
||||||
|
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -232,7 +222,7 @@ public class LLQuicConnection implements LLDatabaseConnection {
|
|||||||
return sendRequest(new SingletonGet(singletonId, NullableLLSnapshot.ofNullable(snapshot)))
|
return sendRequest(new SingletonGet(singletonId, NullableLLSnapshot.ofNullable(snapshot)))
|
||||||
.cast(BinaryOptional.class)
|
.cast(BinaryOptional.class)
|
||||||
.mapNotNull(b -> b.val().getNullable())
|
.mapNotNull(b -> b.val().getNullable())
|
||||||
.map(binary -> toArrayNoCopy(binary.val()));
|
.map(binary -> QuicUtils.toArrayNoCopy(binary.val()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -244,7 +234,34 @@ public class LLQuicConnection implements LLDatabaseConnection {
|
|||||||
@Override
|
@Override
|
||||||
public Mono<Send<Buffer>> update(SerializationFunction<@Nullable Send<Buffer>, @Nullable Buffer> updater,
|
public Mono<Send<Buffer>> update(SerializationFunction<@Nullable Send<Buffer>, @Nullable Buffer> updater,
|
||||||
UpdateReturnMode updateReturnMode) {
|
UpdateReturnMode updateReturnMode) {
|
||||||
return null;
|
return LLQuicConnection.this.<BinaryOptional, SingletonUpdateOldData>sendUpdateRequest(new SingletonUpdateInit(singletonId, updateReturnMode), prev -> {
|
||||||
|
byte[] oldData = toArrayNoCopy(prev);
|
||||||
|
Send<Buffer> oldDataBuf;
|
||||||
|
if (oldData != null) {
|
||||||
|
oldDataBuf = allocator.copyOf(oldData).send();
|
||||||
|
} else {
|
||||||
|
oldDataBuf = null;
|
||||||
|
}
|
||||||
|
try (oldDataBuf) {
|
||||||
|
try (var result = updater.apply(oldDataBuf)) {
|
||||||
|
if (result == null) {
|
||||||
|
return new SingletonUpdateEnd(false, ByteList.of());
|
||||||
|
} else {
|
||||||
|
byte[] resultArray = new byte[result.readableBytes()];
|
||||||
|
result.readBytes(resultArray, 0, resultArray.length);
|
||||||
|
return new SingletonUpdateEnd(true, ByteList.of(resultArray));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SerializationException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}).mapNotNull(result -> {
|
||||||
|
if (result.val().isPresent()) {
|
||||||
|
return allocator.copyOf(QuicUtils.toArrayNoCopy(result.val().get().val())).send();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -306,11 +323,12 @@ public class LLQuicConnection implements LLDatabaseConnection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] toArrayNoCopy(ByteList b) {
|
@Nullable
|
||||||
if (b instanceof ByteArrayList bal) {
|
private static byte[] toArrayNoCopy(SingletonUpdateOldData oldData) {
|
||||||
return bal.elements();
|
if (oldData.exist()) {
|
||||||
|
return QuicUtils.toArrayNoCopy(oldData.oldValue());
|
||||||
} else {
|
} else {
|
||||||
return b.toByteArray();
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
package it.cavallium.dbengine.database.remote;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.ByteToMessageCodec;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.Sinks.Empty;
|
||||||
|
import reactor.netty.NettyOutbound;
|
||||||
|
|
||||||
|
public class MappedStream <SEND, RECV> implements AutoCloseable {
|
||||||
|
|
||||||
|
private final Flux<RECV> inConn;
|
||||||
|
private final NettyOutbound outConn;
|
||||||
|
private final Supplier<ByteToMessageCodec<? super SEND>> outCodec;
|
||||||
|
private final Empty<Void> streamTerminator;
|
||||||
|
|
||||||
|
public MappedStream(NettyOutbound outConn, Supplier<ByteToMessageCodec<? super SEND>> outCodec, Flux<RECV> inConn, Empty<Void> streamTerminator) {
|
||||||
|
this.inConn = inConn;
|
||||||
|
this.outConn = outConn;
|
||||||
|
this.outCodec = outCodec;
|
||||||
|
this.streamTerminator = streamTerminator;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NettyOutbound getOut() {
|
||||||
|
return outConn.withConnection(conn -> conn.addHandler(outCodec.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> send(SEND item) {
|
||||||
|
return getOut().sendObject(item).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<Void> sendMany(Flux<SEND> items) {
|
||||||
|
return getOut().sendObject(items).then();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Mono<RECV> receive() {
|
||||||
|
return inConn.take(1, true).singleOrEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Flux<RECV> receiveMany() {
|
||||||
|
return inConn.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
streamTerminator.tryEmitEmpty();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package it.cavallium.dbengine.database.remote;
|
||||||
|
|
||||||
|
public class NoResponseReceivedException extends IllegalStateException {
|
||||||
|
|
||||||
|
public NoResponseReceivedException() {
|
||||||
|
super("No response received");
|
||||||
|
}
|
||||||
|
|
||||||
|
public NoResponseReceivedException(Throwable cause) {
|
||||||
|
super("No response received", cause);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package it.cavallium.dbengine.database.remote;
|
||||||
|
|
||||||
|
import io.netty.handler.codec.ByteToMessageCodec;
|
||||||
|
import it.cavallium.data.generator.nativedata.NullableString;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCCrash;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCEvent;
|
||||||
|
import it.unimi.dsi.fastutil.bytes.ByteArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.bytes.ByteList;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.Sinks;
|
||||||
|
import reactor.core.publisher.Sinks.Empty;
|
||||||
|
import reactor.core.publisher.Sinks.One;
|
||||||
|
import reactor.netty.NettyInbound;
|
||||||
|
import reactor.netty.NettyOutbound;
|
||||||
|
import reactor.netty.incubator.quic.QuicConnection;
|
||||||
|
|
||||||
|
public class QuicUtils {
|
||||||
|
|
||||||
|
public static final Mono<?> NO_RESPONSE_ERROR = Mono.error(NoResponseReceivedException::new);
|
||||||
|
|
||||||
|
public static byte[] toArrayNoCopy(ByteList b) {
|
||||||
|
if (b instanceof ByteArrayList bal) {
|
||||||
|
return bal.elements();
|
||||||
|
} else {
|
||||||
|
return b.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toString(ByteList b) {
|
||||||
|
return new String(QuicUtils.toArrayNoCopy(b), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record QuicStream(NettyInbound in, NettyOutbound out) {}
|
||||||
|
|
||||||
|
public static Mono<RPCEvent> catchRPCErrors(Throwable error) {
|
||||||
|
return Mono.just(new RPCCrash(500, NullableString.ofNullableBlank(error.getMessage())));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <SEND, RECV> RECV extractResponse(SEND request, RECV response) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a general purpose QUIC stream
|
||||||
|
*/
|
||||||
|
public static Mono<QuicStream> createStream(QuicConnection quicConnection, Mono<Void> streamTerminator) {
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
One<QuicStream> inOutSink = Sinks.one();
|
||||||
|
return quicConnection
|
||||||
|
.createStream((in, out) -> Mono
|
||||||
|
.fromRunnable(() -> inOutSink.tryEmitValue(new QuicStream(in, out)).orThrow())
|
||||||
|
.then(streamTerminator))
|
||||||
|
.then(inOutSink.asMono());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a single request, receive a single response
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <SEND, RECV> Mono<MappedStream<SEND, RECV>> createMappedStream(
|
||||||
|
QuicConnection quicConnection,
|
||||||
|
Supplier<ByteToMessageCodec<? super SEND>> sendCodec,
|
||||||
|
Supplier<ByteToMessageCodec<? super RECV>> recvCodec) {
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
Empty<Void> streamTerminator = Sinks.empty();
|
||||||
|
return QuicUtils
|
||||||
|
.createStream(quicConnection, streamTerminator.asMono())
|
||||||
|
.map(stream -> {
|
||||||
|
Flux<RECV> inConn = Flux.defer(() -> (Flux<RECV>) stream
|
||||||
|
.in()
|
||||||
|
.withConnection(conn -> conn.addHandler(recvCodec.get()))
|
||||||
|
.receiveObject()
|
||||||
|
.log("RECEIVE_OBJECT_FROM_SERVER", Level.FINEST))
|
||||||
|
.publish(1)
|
||||||
|
.refCount();
|
||||||
|
return new MappedStream<>(stream.out, sendCodec, inConn, streamTerminator);
|
||||||
|
})
|
||||||
|
.single();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a single request, receive a single response
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <SEND, RECV> Mono<RECV> sendSimpleRequest(QuicConnection quicConnection,
|
||||||
|
Supplier<ByteToMessageCodec<? super SEND>> sendCodec,
|
||||||
|
Supplier<ByteToMessageCodec<? super RECV>> recvCodec,
|
||||||
|
SEND req) {
|
||||||
|
return QuicUtils
|
||||||
|
.createMappedStream(quicConnection, sendCodec, recvCodec)
|
||||||
|
.flatMap(stream -> {
|
||||||
|
var recv = stream.receive().log("SR-Receive", Level.FINEST);
|
||||||
|
var send = stream.send(req).log("SR-Send", Level.FINEST);
|
||||||
|
return send
|
||||||
|
.then(recv)
|
||||||
|
.doFinally(s -> stream.close());
|
||||||
|
})
|
||||||
|
.switchIfEmpty((Mono<RECV>) NO_RESPONSE_ERROR)
|
||||||
|
.log("SR-Result", Level.FINEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send n requests, receive n responses
|
||||||
|
*/
|
||||||
|
public static <SEND, RECV> Flux<RECV> sendSimpleRequestFlux(QuicConnection quicConnection,
|
||||||
|
Supplier<ByteToMessageCodec<? super SEND>> sendCodec,
|
||||||
|
Supplier<ByteToMessageCodec<? super RECV>> recvCodec,
|
||||||
|
Publisher<SEND> requestFlux) {
|
||||||
|
return QuicUtils
|
||||||
|
.createMappedStream(quicConnection, sendCodec, recvCodec)
|
||||||
|
.flatMapMany(stream -> {
|
||||||
|
var sends = Flux
|
||||||
|
.from(requestFlux)
|
||||||
|
.log("SR-Send", Level.FINEST)
|
||||||
|
.concatMap(request -> stream.send(request)
|
||||||
|
.thenReturn(request));
|
||||||
|
var receives = stream
|
||||||
|
.receiveMany()
|
||||||
|
.log("SR-Receive", Level.FINEST);
|
||||||
|
return Flux
|
||||||
|
.zip(sends, receives, QuicUtils::extractResponse)
|
||||||
|
.doFinally(s -> stream.close());
|
||||||
|
})
|
||||||
|
.log("SR-Result", Level.FINEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send update
|
||||||
|
*/
|
||||||
|
public static <T> Mono<T> sendUpdate(QuicConnection quicConnection,
|
||||||
|
Supplier<ByteToMessageCodec<? super T>> codec,
|
||||||
|
T request,
|
||||||
|
Function<T, Mono<T>> updater) {
|
||||||
|
return QuicUtils
|
||||||
|
.createMappedStream(quicConnection, codec, codec)
|
||||||
|
.flatMapMany(stream -> {
|
||||||
|
//noinspection unchecked
|
||||||
|
var firstRequest = (Mono<T>) stream
|
||||||
|
.send(request)
|
||||||
|
.then();
|
||||||
|
var receives = stream
|
||||||
|
.receiveMany();
|
||||||
|
One<T> firstResponseSink = Sinks.one();
|
||||||
|
//noinspection unchecked
|
||||||
|
var firstResponse = (Mono<T>) receives
|
||||||
|
.elementAt(0)
|
||||||
|
.switchIfEmpty((Mono<? extends T>) NO_RESPONSE_ERROR)
|
||||||
|
.mapNotNull(value -> {
|
||||||
|
if (value instanceof RPCCrash crash) {
|
||||||
|
firstResponseSink.tryEmitEmpty();
|
||||||
|
//noinspection unchecked
|
||||||
|
return (T) crash;
|
||||||
|
} else {
|
||||||
|
firstResponseSink.tryEmitValue(value);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnCancel(firstResponseSink::tryEmitEmpty);
|
||||||
|
//noinspection unchecked
|
||||||
|
var secondResponse = Mono
|
||||||
|
// FirstResponse returns only if it's RPCCrash.
|
||||||
|
// firstWithValue returns the crash first if it happens, otherwise it will
|
||||||
|
// return receives
|
||||||
|
.firstWithValue(
|
||||||
|
firstResponse,
|
||||||
|
receives.elementAt(1)
|
||||||
|
)
|
||||||
|
.switchIfEmpty((Mono<? extends T>) NO_RESPONSE_ERROR);
|
||||||
|
//noinspection unchecked
|
||||||
|
var secondRequest = (Mono<T>) firstResponseSink
|
||||||
|
.asMono()
|
||||||
|
.flatMap(updater)
|
||||||
|
.flatMap(stream::send);
|
||||||
|
return Flux
|
||||||
|
.merge(firstRequest, firstResponse.then(Mono.empty()), secondRequest, secondResponse)
|
||||||
|
.doFinally(s -> stream.close());
|
||||||
|
})
|
||||||
|
.singleOrEmpty();
|
||||||
|
}
|
||||||
|
}
|
@ -6,34 +6,29 @@ import io.netty.buffer.ByteBufOutputStream;
|
|||||||
import io.netty.channel.ChannelHandler;
|
import io.netty.channel.ChannelHandler;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.handler.codec.ByteToMessageCodec;
|
import io.netty.handler.codec.ByteToMessageCodec;
|
||||||
import it.cavallium.dbengine.rpc.current.data.BoxedClientBoundRequest;
|
import it.cavallium.dbengine.rpc.current.data.BoxedRPCEvent;
|
||||||
import it.cavallium.dbengine.rpc.current.data.BoxedClientBoundResponse;
|
|
||||||
import it.cavallium.dbengine.rpc.current.data.BoxedServerBoundRequest;
|
|
||||||
import it.cavallium.dbengine.rpc.current.data.BoxedServerBoundResponse;
|
|
||||||
import it.cavallium.dbengine.rpc.current.data.ClientBoundRequest;
|
import it.cavallium.dbengine.rpc.current.data.ClientBoundRequest;
|
||||||
import it.cavallium.dbengine.rpc.current.data.ClientBoundResponse;
|
import it.cavallium.dbengine.rpc.current.data.ClientBoundResponse;
|
||||||
import it.cavallium.dbengine.rpc.current.data.IBasicType;
|
import it.cavallium.dbengine.rpc.current.data.IBasicType;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCEvent;
|
||||||
import it.cavallium.dbengine.rpc.current.data.ServerBoundRequest;
|
import it.cavallium.dbengine.rpc.current.data.ServerBoundRequest;
|
||||||
import it.cavallium.dbengine.rpc.current.data.ServerBoundResponse;
|
import it.cavallium.dbengine.rpc.current.data.ServerBoundResponse;
|
||||||
import it.cavallium.dbengine.rpc.current.serializers.BoxedClientBoundRequestSerializer;
|
import it.cavallium.dbengine.rpc.current.serializers.BoxedRPCEventSerializer;
|
||||||
import it.cavallium.dbengine.rpc.current.serializers.BoxedClientBoundResponseSerializer;
|
|
||||||
import it.cavallium.dbengine.rpc.current.serializers.BoxedServerBoundRequestSerializer;
|
|
||||||
import it.cavallium.dbengine.rpc.current.serializers.BoxedServerBoundResponseSerializer;
|
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class RPCCodecs {
|
public class RPCCodecs {
|
||||||
|
|
||||||
public static class RPCClientBoundRequestDecoder extends ByteToMessageCodec<ClientBoundRequest> {
|
public static class RPCEventCodec extends ByteToMessageCodec<RPCEvent> {
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCClientBoundRequestDecoder();
|
public static final ChannelHandler INSTANCE = new RPCEventCodec();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void encode(ChannelHandlerContext ctx, ClientBoundRequest msg, ByteBuf out) throws Exception {
|
protected void encode(ChannelHandlerContext ctx, RPCEvent msg, ByteBuf out) throws Exception {
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
try (var bbos = new ByteBufOutputStream(out)) {
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
try (var dos = new DataOutputStream(bbos)) {
|
||||||
BoxedClientBoundRequestSerializer.INSTANCE.serialize(dos, BoxedClientBoundRequest.of(msg));
|
BoxedRPCEventSerializer.INSTANCE.serialize(dos, BoxedRPCEvent.of(msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,154 +37,7 @@ public class RPCCodecs {
|
|||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
try (var bbis = new ByteBufInputStream(msg)) {
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
try (var dis = new DataInputStream(bbis)) {
|
||||||
out.add(BoxedClientBoundRequestSerializer.INSTANCE.deserialize(dis).val());
|
out.add(BoxedRPCEventSerializer.INSTANCE.deserialize(dis).val());
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RPCServerBoundRequestDecoder extends ByteToMessageCodec<ServerBoundRequest> {
|
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCServerBoundRequestDecoder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, ServerBoundRequest msg, ByteBuf out) throws Exception {
|
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
|
||||||
BoxedServerBoundRequestSerializer.INSTANCE.serialize(dos, BoxedServerBoundRequest.of(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
|
||||||
out.add(BoxedServerBoundRequestSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RPCClientBoundResponseDecoder extends ByteToMessageCodec<ClientBoundResponse> {
|
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCClientBoundResponseDecoder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, ClientBoundResponse msg, ByteBuf out) throws Exception {
|
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
|
||||||
BoxedClientBoundResponseSerializer.INSTANCE.serialize(dos, BoxedClientBoundResponse.of(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
|
||||||
out.add(BoxedClientBoundResponseSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RPCServerBoundResponseDecoder extends ByteToMessageCodec<ServerBoundResponse> {
|
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCServerBoundResponseDecoder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, ServerBoundResponse msg, ByteBuf out) throws Exception {
|
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
|
||||||
BoxedServerBoundResponseSerializer.INSTANCE.serialize(dos, BoxedServerBoundResponse.of(msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
|
||||||
out.add(BoxedServerBoundResponseSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RPCClientAlternateDecoder extends ByteToMessageCodec<IBasicType> {
|
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCClientAlternateDecoder();
|
|
||||||
|
|
||||||
private boolean alternated = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, IBasicType msg, ByteBuf out) throws Exception {
|
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
|
||||||
if (!alternated) {
|
|
||||||
BoxedServerBoundRequestSerializer.INSTANCE.serialize(dos,
|
|
||||||
BoxedServerBoundRequest.of((ServerBoundRequest) msg)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
BoxedClientBoundResponseSerializer.INSTANCE.serialize(dos,
|
|
||||||
BoxedClientBoundResponse.of((ClientBoundResponse) msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
alternated = !alternated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
|
||||||
if (!alternated) {
|
|
||||||
out.add(BoxedServerBoundRequestSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
} else {
|
|
||||||
out.add(BoxedClientBoundResponseSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
}
|
|
||||||
alternated = !alternated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class RPCServerAlternateDecoder extends ByteToMessageCodec<IBasicType> {
|
|
||||||
|
|
||||||
public static final ChannelHandler INSTANCE = new RPCServerAlternateDecoder();
|
|
||||||
|
|
||||||
private boolean alternated = false;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void encode(ChannelHandlerContext ctx, IBasicType msg, ByteBuf out) throws Exception {
|
|
||||||
try (var bbos = new ByteBufOutputStream(out)) {
|
|
||||||
try (var dos = new DataOutputStream(bbos)) {
|
|
||||||
if (!alternated) {
|
|
||||||
BoxedClientBoundRequestSerializer.INSTANCE.serialize(dos,
|
|
||||||
BoxedClientBoundRequest.of((ClientBoundRequest) msg)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
BoxedServerBoundResponseSerializer.INSTANCE.serialize(dos,
|
|
||||||
BoxedServerBoundResponse.of((ServerBoundResponse) msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
alternated = !alternated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
|
|
||||||
try (var bbis = new ByteBufInputStream(msg)) {
|
|
||||||
try (var dis = new DataInputStream(bbis)) {
|
|
||||||
if (!alternated) {
|
|
||||||
out.add(BoxedClientBoundRequestSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
} else {
|
|
||||||
out.add(BoxedServerBoundResponseSerializer.INSTANCE.deserialize(dis).val());
|
|
||||||
}
|
|
||||||
alternated = !alternated;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,279 @@
|
|||||||
|
package it.cavallium.dbengine.database.remote;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.ClientAuth;
|
||||||
|
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||||
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
|
import io.netty.incubator.codec.quic.InsecureQuicTokenHandler;
|
||||||
|
import io.netty.incubator.codec.quic.QuicConnectionIdGenerator;
|
||||||
|
import io.netty.incubator.codec.quic.QuicSslContext;
|
||||||
|
import io.netty.incubator.codec.quic.QuicSslContextBuilder;
|
||||||
|
import it.cavallium.data.generator.nativedata.NullableString;
|
||||||
|
import it.cavallium.dbengine.database.remote.RPCCodecs.RPCEventCodec;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.Empty;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCCrash;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.RPCEvent;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.SingletonGet;
|
||||||
|
import it.cavallium.dbengine.rpc.current.data.nullables.NullableLLSnapshot;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.core.publisher.SignalType;
|
||||||
|
import reactor.netty.Connection;
|
||||||
|
import reactor.netty.incubator.quic.QuicClient;
|
||||||
|
import reactor.netty.incubator.quic.QuicConnection;
|
||||||
|
|
||||||
|
class QuicUtilsTest {
|
||||||
|
|
||||||
|
private static final int NORMAL = 0;
|
||||||
|
private static final int WAIT_TIME = 1;
|
||||||
|
private static final int FAIL_IMMEDIATELY = 2;
|
||||||
|
private static final int WAIT_TIME_THEN_FAIL = 3;
|
||||||
|
|
||||||
|
|
||||||
|
private Connection serverConn;
|
||||||
|
private QuicConnection clientConn;
|
||||||
|
private InetSocketAddress clientAddress;
|
||||||
|
private InetSocketAddress serverAddress;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws CertificateException {
|
||||||
|
var selfSignedCert = new SelfSignedCertificate();
|
||||||
|
this.clientAddress = new InetSocketAddress("localhost", 8081);
|
||||||
|
this.serverAddress = new InetSocketAddress("localhost", 8080);
|
||||||
|
QuicSslContext sslContext = QuicSslContextBuilder
|
||||||
|
.forServer(selfSignedCert.key(), null, selfSignedCert.cert())
|
||||||
|
.applicationProtocols("db/0.9")
|
||||||
|
.clientAuth(ClientAuth.NONE)
|
||||||
|
.build();
|
||||||
|
var qs = reactor.netty.incubator.quic.QuicServer
|
||||||
|
.create()
|
||||||
|
.tokenHandler(InsecureQuicTokenHandler.INSTANCE)
|
||||||
|
.bindAddress(() -> serverAddress)
|
||||||
|
.secure(sslContext)
|
||||||
|
.idleTimeout(Duration.ofSeconds(30))
|
||||||
|
.connectionIdAddressGenerator(QuicConnectionIdGenerator.randomGenerator())
|
||||||
|
.initialSettings(spec -> spec
|
||||||
|
.maxData(10000000)
|
||||||
|
.maxStreamDataBidirectionalLocal(1000000)
|
||||||
|
.maxStreamDataBidirectionalRemote(1000000)
|
||||||
|
.maxStreamsBidirectional(100)
|
||||||
|
.maxStreamsUnidirectional(100)
|
||||||
|
)
|
||||||
|
.handleStream((in, out) -> in
|
||||||
|
.withConnection(conn -> conn.addHandler(new RPCEventCodec()))
|
||||||
|
.receiveObject()
|
||||||
|
.cast(RPCEvent.class)
|
||||||
|
.log("recv", Level.FINEST)
|
||||||
|
.flatMapSequential(req -> (switch ((int) ((SingletonGet) req).singletonId()) {
|
||||||
|
case NORMAL -> Mono.<RPCEvent>just(Empty.of());
|
||||||
|
case FAIL_IMMEDIATELY -> Mono.<RPCEvent>error(new Throwable("Expected error"));
|
||||||
|
case WAIT_TIME -> Mono.delay(Duration.ofSeconds(3)).<RPCEvent>thenReturn(Empty.of());
|
||||||
|
case WAIT_TIME_THEN_FAIL -> Mono
|
||||||
|
.delay(Duration.ofSeconds(3))
|
||||||
|
.then(Mono.<RPCEvent>error(new Throwable("Expected error")));
|
||||||
|
default -> Mono.<RPCEvent>error(new UnsupportedOperationException("Unsupported request id " + req));
|
||||||
|
}).log("Server", Level.SEVERE, SignalType.ON_ERROR).onErrorResume(QuicUtils::catchRPCErrors))
|
||||||
|
.concatMap(message -> Mono.defer(() -> out
|
||||||
|
.withConnection(conn -> conn.addHandler(new RPCEventCodec()))
|
||||||
|
.sendObject(message)
|
||||||
|
.then())
|
||||||
|
.log("send", Level.FINEST)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.serverConn = qs.bindNow();
|
||||||
|
|
||||||
|
var clientSslContext = QuicSslContextBuilder
|
||||||
|
.forClient()
|
||||||
|
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
||||||
|
.applicationProtocols("db/0.9")
|
||||||
|
.build();
|
||||||
|
this.clientConn = QuicClient.create()
|
||||||
|
.bindAddress(() -> new InetSocketAddress(0))
|
||||||
|
.remoteAddress(() -> serverAddress)
|
||||||
|
.secure(clientSslContext)
|
||||||
|
.idleTimeout(Duration.ofSeconds(30))
|
||||||
|
.initialSettings(spec -> spec
|
||||||
|
.maxData(10000000)
|
||||||
|
.maxStreamDataBidirectionalLocal(1000000)
|
||||||
|
)
|
||||||
|
.connectNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
if (clientConn != null) {
|
||||||
|
clientConn.disposeNow();
|
||||||
|
}
|
||||||
|
if (serverConn != null) {
|
||||||
|
serverConn.disposeNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSimpleRequest() {
|
||||||
|
RPCEvent response = QuicUtils.<RPCEvent, RPCEvent>sendSimpleRequest(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty())
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(Empty.of(), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSimpleRequestFlux() {
|
||||||
|
List<RPCEvent> results = QuicUtils.<RPCEvent, RPCEvent>sendSimpleRequestFlux(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
Flux.just(
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty())
|
||||||
|
)
|
||||||
|
).collectList().blockOptional().orElseThrow();
|
||||||
|
assertEquals(5, results.size());
|
||||||
|
assertEquals(List.of(Empty.of(), Empty.of(), Empty.of(), Empty.of(), Empty.of()), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateFluxNormal() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.fromCallable(() -> {
|
||||||
|
assertEquals(Empty.of(), serverData);
|
||||||
|
return new SingletonGet(NORMAL, NullableLLSnapshot.empty());
|
||||||
|
})
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(Empty.of(), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateFluxSlowClient() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.<RPCEvent>fromCallable(() -> {
|
||||||
|
assertEquals(Empty.of(), serverData);
|
||||||
|
return new SingletonGet(NORMAL, NullableLLSnapshot.empty());
|
||||||
|
}).delayElement(Duration.ofSeconds(2))
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(Empty.of(), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateFluxSlowServer() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(WAIT_TIME, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.fromCallable(() -> {
|
||||||
|
assertEquals(Empty.of(), serverData);
|
||||||
|
return new SingletonGet(WAIT_TIME, NullableLLSnapshot.empty());
|
||||||
|
})
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(Empty.of(), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateFluxSlowClientAndServer() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(WAIT_TIME, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.<RPCEvent>fromCallable(() -> {
|
||||||
|
assertEquals(Empty.of(), serverData);
|
||||||
|
return new SingletonGet(WAIT_TIME, NullableLLSnapshot.empty());
|
||||||
|
}).delayElement(Duration.ofSeconds(2))
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(Empty.of(), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateClientFail() {
|
||||||
|
class ExpectedException extends Throwable {}
|
||||||
|
|
||||||
|
assertThrows(ExpectedException.class, () -> {
|
||||||
|
try {
|
||||||
|
RPCEvent results = QuicUtils
|
||||||
|
.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.error(new ExpectedException())
|
||||||
|
)
|
||||||
|
.blockOptional()
|
||||||
|
.orElseThrow();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw e.getCause();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateServerFail1() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(FAIL_IMMEDIATELY, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.fromCallable(() -> {
|
||||||
|
fail("Called update");
|
||||||
|
return new SingletonGet(NORMAL, NullableLLSnapshot.empty());
|
||||||
|
})
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(RPCCrash.of(500, NullableString.of("Expected error")), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendUpdateServerFail2() {
|
||||||
|
RPCEvent results = QuicUtils.<RPCEvent>sendUpdate(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty()),
|
||||||
|
serverData -> Mono.fromCallable(() -> {
|
||||||
|
assertEquals(Empty.of(), serverData);
|
||||||
|
return new SingletonGet(FAIL_IMMEDIATELY, NullableLLSnapshot.empty());
|
||||||
|
})
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(RPCCrash.of(500, NullableString.of("Expected error")), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendSimpleRequestConcurrently() {
|
||||||
|
// Send the request a second time
|
||||||
|
var requestMono = QuicUtils.<RPCEvent, RPCEvent>sendSimpleRequest(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(NORMAL, NullableLLSnapshot.empty())
|
||||||
|
);
|
||||||
|
var results = Flux
|
||||||
|
.merge(requestMono, requestMono, requestMono, requestMono, requestMono)
|
||||||
|
.collectList()
|
||||||
|
.blockOptional()
|
||||||
|
.orElseThrow();
|
||||||
|
assertEquals(5, results.size());
|
||||||
|
assertEquals(List.of(Empty.of(), Empty.of(), Empty.of(), Empty.of(), Empty.of()), results);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendFailedRequest() {
|
||||||
|
RPCEvent response = QuicUtils.<RPCEvent, RPCEvent>sendSimpleRequest(clientConn,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
RPCEventCodec::new,
|
||||||
|
new SingletonGet(FAIL_IMMEDIATELY, NullableLLSnapshot.empty())
|
||||||
|
).blockOptional().orElseThrow();
|
||||||
|
assertEquals(RPCCrash.of(500, NullableString.of("Expected error")), response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createStream() {
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user