Handle Subscription in vertx GraphQL - graphql

I tried to use Vertx HttpClient/WebClient to consume the GraphQLSubscritpion but it did not work as expected.
The server-side related code(written with Vertx Web GraphQL) is like the following, when a comment is added, then trigger onNext to send the comment to the Publisher.
public VertxDataFetcher<UUID> addComment() {
return VertxDataFetcher.create((DataFetchingEnvironment dfe) -> {
var commentInputArg = dfe.getArgument("commentInput");
var jacksonMapper = DatabindCodec.mapper();
var input = jacksonMapper.convertValue(commentInputArg, CommentInput.class);
return this.posts.addComment(input)
.onSuccess(id -> this.posts.getCommentById(id.toString())
.onSuccess(c ->subject.onNext(c)));
});
}
private BehaviorSubject<Comment> subject = BehaviorSubject.create();
public DataFetcher<Publisher<Comment>> commentAdded() {
return (DataFetchingEnvironment dfe) -> {
ConnectableObservable<Comment> connectableObservable = subject.share().publish();
connectableObservable.connect();
return connectableObservable.toFlowable(BackpressureStrategy.BUFFER);
};
}
In the client, I mixed to use the HttpClient/WebClient, most of the time, I would like to use WebClient, which easier for handling form post. But it seems it does not work have a WebSocket connection.
So the websocket part is returning to use HttpClient.
var options = new HttpClientOptions()
.setDefaultHost("localhost")
.setDefaultPort(8080);
var httpClient = vertx.createHttpClient(options);
httpClient.webSocket("/graphql")
.onSuccess(ws -> {
ws.textMessageHandler(text -> log.info("web socket message handler:{}", text));
JsonObject messageInit = new JsonObject()
.put("type", "connection_init")
.put("id", "1");
JsonObject message = new JsonObject()
.put("payload", new JsonObject()
.put("query", "subscription onCommentAdded { commentAdded { id content } }"))
.put("type", "start")
.put("id", "1");
ws.write(messageInit.toBuffer());
ws.write(message.toBuffer());
})
.onFailure(e -> log.error("error: {}", e));
// this client here is WebClient.
client.post("/graphql")
.sendJson(Map.of(
"query", "mutation addComment($input:CommentInput!){ addComment(commentInput:$input) }",
"variables", Map.of(
"input", Map.of(
"postId", id,
"content", "comment content of post id" + LocalDateTime.now()
)
)
))
.onSuccess(
data -> log.info("data of addComment: {}", data.bodyAsString())
)
.onFailure(e -> log.error("error: {}", e));
When running the client and server, the comment is added, but the WebSocket client does not print any info about websocket message. On the server console, there is an message like this.
2021-06-25 18:45:44,356 DEBUG [vert.x-eventloop-thread-1] graphql.GraphQL: Execution '182965bb-80de-416d-b5fe-fe157ab87f1c' completed with zero errors
It seems the backend commentAdded datafetcher is not invoked at all.
The complete codes of GraphQL client and server are shared on my Github.

After reading some testing codes of Vertx Web GraphQL, I found I have to add the ConnectionInitHandler on ApolloWSHandler like this.
.connectionInitHandler(connectionInitEvent -> {
JsonObject payload = connectionInitEvent.message().content().getJsonObject("payload");
if (payload != null && payload.containsKey("rejectMessage")) {
connectionInitEvent.fail(payload.getString("rejectMessage"));
return;
}
connectionInitEvent.complete(payload);
}
)
When the client sends connection_init message, the connectionInitEvent.complete is required to start the communication between the client and the server.

Related

Okio Throttler integration with OkHttp

My team is suffering from this issue with slack integration to upload files, so following the comments in that issue I would like to throttle the requests in our Kotlin implementation.
I am trying to integrate Okio Throttler within an OkHttp interceptor, so I have the setup:
val client = OkHttpClient.Builder()
.retryOnConnectionFailure(false)
.addInterceptor { chain ->
val request = chain.request()
val originalRequestBody = request.body
val newRequest = if (originalRequestBody != null) {
val wrappedRequestBody = ThrottledRequestBody(originalRequestBody)
request.newBuilder()
.method(request.method, wrappedRequestBody)
.build()
} else {
request
}
chain.proceed(newRequest)
}
.build()
class ThrottledRequestBody(private val delegate: RequestBody) : RequestBody() {
private val throttler = Throttler().apply {
bytesPerSecond(1024, 1024 * 4, 1024 * 8)
}
override fun contentType(): MediaType? {
return delegate.contentType()
}
override fun writeTo(sink: BufferedSink) {
delegate.writeTo(throttler.sink(sink).buffer())
}
}
It seems throttler.sink returns a Sink, but a BufferedSink is required to the method delegate.writeTo, so I called buffer() to get that BufferedSink.
Am I doing it wrong ? Is the call for .buffer() breaking the integration?
It's almost perfect. You just need to flush the buffer when you're done otherwise it'll finish with a few bytes inside.
override fun writeTo(sink: BufferedSink) {
throttler.sink(sink).buffer().use {
delegate.writeTo(it)
}
}

Spring reactive WebSocketHandler mono does not reach doFinally block

I am trying to handle closing web socket session in WebSocketHandler. My intuition was to do it in this way:
webSocketClient.execute(
URI.create("some-ws-endpoint")
) { session: WebSocketSession ->
session.receive()
.doOnEach { action(it) }
.then()
.doFinally { session.close() }
}
but I cannot reach doFinally block from Mono<Void> returned by webSocketClient.execute. My full test code for this case is:
fun test() = runBlocking {
val webSocketClient: WebSocketClient = StandardWebSocketClient()
val subscription = webSocketClient.execute(
URI.create("some-ws-endpoint")
) { session: WebSocketSession ->
session.receive()
.doOnEach { println("Message: $it") }
.then()
.doFinally { println("finally") }
}.subscribe()
delay(20000)
subscription.dispose()
delay(5000)
}
from which I have Messages printed, but finally is never shown on my console. From the other hand when I tried to do it on plain reactor-core components, everything works just fine:
runBlocking {
val publisher: Flux<Long> = Flux.interval(Duration.ofSeconds(1))
val subscription = publisher
.doOnEach { println("Value: $it") }
.then()
.doFinally { println("in doFinally") }
.subscribe()
delay(5_000)
subscription.dispose()
delay(1_000)
}
I am new to both WebSockets and Project Reactor, so maybe I am doing some basic mistake. Does anyone see what is wrong with my code?

How to create new chat room with play framework websocket?

I tried the chat example with websocket in play framework 2.6.x. It works fine. Now for the real application, I need to create multiple chat rooms based on user requests. And users will be able to access different chatrooms with an id or something. I think it might related to create a new flow for each room. Related code is here:
private val (chatSink, chatSource) = {
val source = MergeHub.source[WSMessage]
.log("source")
.map { msg =>
try {
val json = Json.parse(msg)
inputSanitizer.sanText((json \ "msg").as[String])
} catch {
case e: Exception => println(">>" + msg)
"Malfunction client"
}
}
.recoverWithRetries(-1, { case _: Exception ⇒ Source.empty })
val sink = BroadcastHub.sink[WSMessage]
source.toMat(sink)(Keep.both).run()
}
private val userFlow: Flow[WSMessage, WSMessage, _] = {
Flow.fromSinkAndSource(chatSink, chatSource)
}
But I really don't know how to create new flow with id and access it later. Can anyone help me on this?
I finally figured it out. Post the solution here in case anyone has similar problems.
My solution is to use the AsyncCacheApi to store Flows in cache with keys. Generate a new Flow when necessary instead of creating just one Sink and Source:
val chatRoom = cache.get[Flow[WSMessage, WSMessage, _]](s"id=$id")
chatRoom.map{room=>
val flow = if(room.nonEmpty) room.get else createNewFlow
cache.set(s"id=$id", flow)
Right(flow)
}
def createNewFlow: Flow[WSMessage, WSMessage, _] = {
val (chatSink, chatSource) = {
val source = MergeHub.source[WSMessage]
.map { msg =>
try {
inputSanitizer.sanitize(msg)
} catch {
case e: Exception => println(">>" + msg)
"Malfunction client"
}
}
.recoverWithRetries(-1, { case _: Exception ⇒ Source.empty })
val sink = BroadcastHub.sink[WSMessage]
source.toMat(sink)(Keep.both).run()
}
Flow.fromSinkAndSource(chatSink, chatSource)
}

Springboot. Reactive webclient. Connection prematurely closed BEFORE response

I've come across with this problem
Pooled connection observed an error
reactor.netty.http.client.HttpClientOperations$PrematureCloseException:
Connection prematurely closed BEFORE response".
I'm gathering metrics from the graphite server via reactive web-client for the requested timeframes (to reduce the amount of data that transfers via http I've divided days into chunks 24/4), then combine responses into a matrix and save it to csv file -> merge to another one.
The problem appears when the number of days increases (2 or 3 works fine, but more days will be more errors with closed connections happened). Tried to use delays, it helps a bit, but to process one more day without errors.
Stack-trace:
ClosedConnectionStacktrace
Found a bit similar issue https://github.com/reactor/reactor-netty/issues/413 , but not sure.
Here's the code snippets:
discoveryMono.thenReturn(true) // discover metrics
.flux()
.flatMap(m -> Flux.fromIterable(dates) // process all days
.delayElements(Duration.ofSeconds(1L))
.flatMap(date -> Flux.range(0, 24 / intervalHours) // divide day into chunks
.delayElements(Duration.of(100L, ChronoUnit.MILLIS))
.flatMap(timeFraction -> Flux.fromIterable(sequentialTasks) // task to invoke webclient
.flatMap(task -> {
Instant from = date.plus(timeFraction * intervalHours, ChronoUnit.HOURS);
Instant until = from.plus(intervalHours, ChronoUnit.HOURS);
TaskParams taskParams = new TaskParams(itSystem, from, until, TaskParams.PollingType.FULLDAY);
log.trace("workflow | from={}, until={}", from, until);
return task.apply(taskParams)
// .doOnNext(m -> log.trace("Matrix: {}", m))
.onErrorResume(err -> {
log.error("processFullDaysInChunks | Error: {}", err);
return Mono.empty();
});
}).flatMap(params -> Flux.fromIterable(fileTasks) // tasks to check/merge files, doesn't matter
.flatMap(fileTask -> parTask.apply(params)
.onErrorResume(err -> {
log.error("processFullDaysInChunks | Error: {}", err);
return Mono.empty();
})
)
)
)
)
).subscribeOn(fullDayScheduler).subscribe();
and part of the task with webclient invokation:
private Flux<GraphiteResultDTO> getGraphiteResults(ITSystem itSystem, Instant from, Instant until) {
String fromStr = FROM_PARAMETER + Long.valueOf(from.getEpochSecond()).toString();
String untilStr = UNTIL_PARAMETER + Long.valueOf(until.getEpochSecond()).toString();
String uri = RENDER_URI + TARGET_PARAMETER + "{targetParam}" + fromStr + untilStr + FORMAT_JSON_PARAMETER;
WebClient webClient = getGraphiteWebClient(itSystem.getDataSource());
Set<String> targetParams = storage.getValueByITSystemId(itSystem.getId()).getSecond();
Flux<GraphiteResultDTO> result = Flux.fromIterable(targetParams)
.delayElements(Duration.of(10, ChronoUnit.MILLIS))
.flatMap(targetParam -> {
Map<String, String> params = Map.ofEntries(entry("targetParam", targetParam));
if (log.isTraceEnabled()) {
log.trace("getGraphiteResults | Uri={}, TargetPatam: {}", uri, targetParam);
}
return webClient.get()
.uri(uri, params)
.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
log.trace("clientResponse | transforming body");
clientResponse.bodyToMono(String.class)
.doOnNext(errorString -> log.error("retrieve(), error={}", errorString));
// .flatMap(s -> Flux.error(clientResponse.bodyToFlux(WebClientException.class)));
return Mono.empty();
})
.bodyToFlux(GraphiteResultDTO.class)
.onErrorResume(throwable -> {
log.error("webclient | bodyToFlux error={}", throwable.getMessage());
return Flux.empty();
});
});
return result;
}
Resolved my problem with replacing the flatMap operator with concatMap with prefetch 1 and limiting the rate (limitRate operator). All the requests now process one by one sequentially. So there is no need to use time delays now.

Events not firing? Using java socket.io client & netty-socketio on server

I know the client and server are connecting because my connect/disconnect events are firing. However, my custom events are not. I am using socket.io java client, and netty-socketio on the server. I usually use the socket.io javascript library which works seamlessly, so I am a bit lost as to why this is happening. I am writing this in Kotlin.
Client-Side
fun connectToServer(ipAddress : String)
{
socket = IO.socket("$ipAddress")
socket!!.on(Socket.EVENT_CONNECT) { obj ->
println("Connected To Server!!!")
}.on(EventNames.signOn) { obj ->
println(EventNames.signOn)
//cast value to string from server, hope for encrypted password
val encryptedPassword = obj[0] as String
when(encryptedPassword)
{
"no user" -> {
}
else -> {
val result = encryptedPassword!!.split("OR")
val isMatch = passwordTextField.text == dataProcessing.Encryption3().decryptValue("decrypt", result[0],result[1])
if(isMatch)
{
}
}
}
println("Encrypted Password: "+encryptedPassword)
}
// socket!!.on(Socket.EVENT_DISCONNECT, object : Emitter.Listener {
//
// override fun call(vararg args: Any) {}
//
// })
socket!!.connect()
// socket!!.open()
// socket!!.emit(Socket.EVENT_CONNECT, "Hello!")
socket!!.send("hey")
socket!!.emit(EventNames.requestClientSignOn, usernameTextField.text)
}
Server-Side
#Throws(InterruptedException::class, UnsupportedEncodingException::class)
fun server()
{
val config = Configuration()
config.setHostname("localhost")
config.setPort(PORT)
server = SocketIOServer(config)
server!!.addConnectListener {
println("Hello World!")
}
server!!.addEventListener(EventNames.requestClientSignOn, String::class.java) { client, data, ackRequest ->
println("Hello from requestClientSignOn..")
}
server!!.addDisconnectListener {
println("Client Disconnecting...")
}
server!!.addConnectListener {
println("client connected!! client: $it")
}
server!!.start()
You cannot use lambda expression in your event listeners, using netty-socketio on the sever.
Using the traditional EventListener solves this problem. I also converted the server to Kotlin, as it was easier to use the demo project as a reference.
server.addEventListener(EventNames.requestClientSignOn, String.class, new DataListener<String>() {
#Override
public void onData(SocketIOClient client, String username, AckRequest ackRequest) {
String isEncryptedPassword = new KOTS_EmployeeManager().getKOTS_User(KOTS_EmployeeManager.kotsUserType.CLIENT, username)
if(isEncryptedPassword != null)
{
//send back ack with encrypted password
ackRequest.sendAckData(isEncryptedPassword);
}else{
//send back ack with no user string
ackRequest.sendAckData("no user");
}
}
});

Resources