├── common ├── build.gradle └── src │ └── main │ └── java │ └── discord4j │ └── connect │ └── common │ ├── PartialGatewayPayload.java │ ├── package-info.java │ ├── PayloadSink.java │ ├── Discord4JConnectException.java │ ├── SinkMapper.java │ ├── PayloadSource.java │ ├── SourceMapper.java │ ├── ConnectGatewayOptions.java │ ├── ConnectPayload.java │ ├── PayloadDestinationMapper.java │ └── UpstreamGatewayClient.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitattributes ├── gradle.properties ├── settings.gradle ├── examples ├── src │ └── main │ │ ├── docker │ │ └── redis.yml │ │ ├── java │ │ └── discord4j │ │ │ └── connect │ │ │ ├── rsocket │ │ │ ├── shared │ │ │ │ ├── package-info.java │ │ │ │ ├── servers │ │ │ │ │ ├── ExampleRSocketPayloadServer.java │ │ │ │ │ ├── ExampleRSocketShardCoordinatorServer.java │ │ │ │ │ └── ExampleRSocketGlobalRouterServer.java │ │ │ │ ├── ExampleRSocketWorker.java │ │ │ │ └── ExampleRSocketLeader.java │ │ │ └── README.md │ │ │ ├── rabbitmq │ │ │ ├── README.md │ │ │ └── shared │ │ │ │ ├── ExampleRabbitLocalCacheWorker.java │ │ │ │ ├── ExampleRabbitDistributedCacheWorker.java │ │ │ │ ├── ExampleRabbitLocalCacheLeader.java │ │ │ │ └── ExampleRabbitDistributedCacheLeader.java │ │ │ ├── ExampleSingleWithConnection.java │ │ │ ├── ExampleSingleConnect.java │ │ │ ├── support │ │ │ ├── EventHandler.java │ │ │ ├── NoBotSupport.java │ │ │ ├── LogoutHttpServer.java │ │ │ └── BotSupport.java │ │ │ └── Constants.java │ │ └── resources │ │ └── logback.xml ├── README.md └── build.gradle ├── rabbitmq ├── build.gradle └── src │ └── main │ └── java │ └── discord4j │ └── connect │ └── rabbitmq │ ├── gateway │ ├── JacksonJsonSinkMapper.java │ ├── JacksonJsonSourceMapper.java │ ├── RabbitMQBinarySinkMapper.java │ ├── RabbitMQBinarySourceMapper.java │ ├── RabbitMQSourceMapper.java │ ├── RoutingMetadata.java │ ├── RabbitMQPayloadSink.java │ ├── RabbitMQPayloadSource.java │ └── RabbitMQSinkMapper.java │ ├── ConnectRabbitMQ.java │ └── ConnectRabbitMQSettings.java ├── .editorconfig ├── rsocket ├── build.gradle └── src │ └── main │ └── java │ └── discord4j │ └── connect │ └── rsocket │ ├── package-info.java │ ├── gateway │ ├── package-info.java │ ├── RSocketPayloadSink.java │ ├── RSocketJacksonSourceMapper.java │ ├── RSocketJacksonSinkMapper.java │ ├── RSocketPayloadSource.java │ └── RSocketPayloadServer.java │ ├── global │ ├── package-info.java │ ├── RSocketGlobalRateLimiter.java │ └── RSocketGlobalRateLimiterServer.java │ ├── router │ ├── package-info.java │ ├── RequestBridge.java │ ├── RSocketRouterOptions.java │ ├── RequestBridgeStream.java │ └── RSocketRouterServer.java │ ├── shard │ ├── package-info.java │ ├── RSocketShardCoordinator.java │ └── RSocketShardCoordinatorServer.java │ └── ConnectRSocket.java ├── README.md ├── .gitignore ├── .github └── workflows │ └── gradle.yml ├── gradlew.bat ├── gradlew └── LICENSE.txt /common/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation "com.discord4j:discord4j-core:$discord4j_core_version" 3 | } 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Discord4J/connect/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.bat text eol=crlf 3 | *.java text eol=lf 4 | *.sh text eol=lf 5 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | systemProp.org.gradle.internal.publish.checksums.insecure=true 3 | version=0.7.0-SNAPSHOT 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'connect' 2 | 3 | include 'common' 4 | include 'rsocket' 5 | include 'examples' 6 | include 'rabbitmq' 7 | 8 | -------------------------------------------------------------------------------- /examples/src/main/docker/redis.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | redis: 4 | image: redis 5 | ports: 6 | - "6379:6379" 7 | restart: always 8 | -------------------------------------------------------------------------------- /rabbitmq/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':common') 3 | api "io.projectreactor.rabbitmq:reactor-rabbitmq:$rabbitmq_version" 4 | 5 | implementation "com.discord4j:discord4j-core:$discord4j_core_version" 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /rsocket/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':common') 3 | api "io.rsocket:rsocket-core:$rsocket_version" 4 | api "io.rsocket:rsocket-transport-netty:$rsocket_version" 5 | 6 | implementation "com.discord4j:discord4j-core:$discord4j_core_version" 7 | } -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Discord4J Connect Examples 2 | 3 | This folder contains examples to set up a reference distributed architecture using Discord4J leaders and workers. 4 | 5 | ## Implementations 6 | - RSocket (prototype completed, in testing) 7 | - Planned: RabbitMQ, Pulsar, Kafka 8 | -------------------------------------------------------------------------------- /examples/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(':rsocket') 3 | api project(':rabbitmq') 4 | 5 | implementation "ch.qos.logback:logback-classic:$logback_version" 6 | implementation "com.discord4j:discord4j-core:$discord4j_core_version" 7 | implementation "com.discord4j:stores-redis:$stores_redis_version" 8 | } 9 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Examples to a shared-subscription RSocket distributed Discord4J architecture. A shared subscription has a 3 | * leader-worker topology where workers consume payloads from every shard, distributing the load. 4 | */ 5 | package discord4j.connect.rsocket.shared; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord4J 2 | 3 | **Connect** project offers a range of tools to set up a distributed bot architecture. 4 | 5 | Work in progress. See [Examples](https://github.com/Discord4J/connect/tree/master/examples/src/main/java/discord4j/connect) folder. 6 | 7 | ![](https://github.com/Discord4J/connect/blob/master/distributed-discord4j-bot.svg) 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ 2 | .idea 3 | 4 | # Eclipse 5 | .classpath 6 | .project 7 | .settings 8 | 9 | # Netbeans 10 | nbproject/private/ 11 | build/ 12 | nbbuild/ 13 | dist/ 14 | nbdist/ 15 | .nb-gradle/ 16 | 17 | # Build artifacts 18 | *.jar 19 | out/ 20 | build/ 21 | **/bin/ 22 | 23 | # Gradle 24 | .gradle 25 | !gradle-wrapper.jar 26 | 27 | # Misc 28 | logs/ 29 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rabbitmq/README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ Discord4J Connect Examples 2 | 3 | This folder contains examples to set up an RSocket-based implementation of every component required to work with a distributed Discord4J-based bot. 4 | 5 | ## Components 6 | - D4J Leaders: connect to Gateway and communicates to RabbitMQ server 7 | - D4J Workers: connect to RabbitMQ broker and processes messages from leaders 8 | 9 | ## Usage 10 | 11 | Please check `shared` folder for a reference implementation with the following capabilities: 12 | 13 | - Leader/Worker topology: multiple leaders connect to Gateway, multiple workers consume payloads. 14 | - Shared subscription means every worker reads from a work queue consuming payloads. There is no guarantee about the shard index received by each worker. 15 | 16 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/ExampleSingleWithConnection.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect; 2 | 3 | import discord4j.common.store.Store; 4 | import discord4j.common.store.legacy.LegacyStoreLayout; 5 | import discord4j.connect.support.BotSupport; 6 | import discord4j.core.DiscordClientBuilder; 7 | import discord4j.core.shard.ShardingStrategy; 8 | import discord4j.store.jdk.JdkStoreService; 9 | 10 | public class ExampleSingleWithConnection { 11 | 12 | public static void main(String[] args) { 13 | DiscordClientBuilder.create(System.getenv("BOT_TOKEN")) 14 | .build() 15 | .gateway() 16 | .setStore(Store.fromLayout(LegacyStoreLayout.of(new JdkStoreService()))) 17 | .setSharding(ShardingStrategy.fixed(1)) 18 | .withGateway(client -> BotSupport.create(client).eventHandlers()) 19 | .block(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * Distributed bot components implemented using RSocket 19 | */ 20 | package discord4j.connect.rsocket; -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/PartialGatewayPayload.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.common; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import reactor.util.annotation.Nullable; 6 | 7 | public class PartialGatewayPayload { 8 | 9 | private int op; 10 | @JsonProperty("d") 11 | @Nullable 12 | private JsonNode data; 13 | @JsonProperty("s") 14 | @Nullable 15 | private Integer sequence; 16 | @JsonProperty("t") 17 | @Nullable 18 | private String type; 19 | 20 | public int getOp() { 21 | return op; 22 | } 23 | 24 | @Nullable 25 | public JsonNode getData() { 26 | return data; 27 | } 28 | 29 | @Nullable 30 | public Integer getSequence() { 31 | return sequence; 32 | } 33 | 34 | @Nullable 35 | public String getType() { 36 | return type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * RSocket-based gateway components for distributed bots. 19 | */ 20 | package discord4j.connect.rsocket.gateway; -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/JacksonJsonSinkMapper.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import discord4j.connect.common.ConnectPayload; 5 | import discord4j.connect.common.SinkMapper; 6 | import org.reactivestreams.Publisher; 7 | import reactor.core.publisher.Mono; 8 | 9 | /** 10 | * A mapper that can convert a {@link ConnectPayload} into a JSON-formatted byte array using Jackson. 11 | */ 12 | public class JacksonJsonSinkMapper implements SinkMapper { 13 | 14 | private final ObjectMapper objectMapper; 15 | 16 | public JacksonJsonSinkMapper(final ObjectMapper objectMapper) { 17 | this.objectMapper = objectMapper; 18 | } 19 | 20 | @Override 21 | public Publisher apply(final ConnectPayload payload) { 22 | return Mono.fromCallable(() -> objectMapper.writeValueAsBytes(payload)); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/global/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * RSocket-based global rate limiting components for distributed bots. 19 | */ 20 | package discord4j.connect.rsocket.global; -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/router/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * RSocket-based API request queueing and rate limit handling for distributed bots. 19 | */ 20 | package discord4j.connect.rsocket.router; -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/shard/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * RSocket-based websocket connection and IDENTIFY rate limit handling for distributed bots. 19 | */ 20 | package discord4j.connect.rsocket.shard; -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | /** 18 | * Core components to implement a distributed Discord4J bot architecture. 19 | */ 20 | @NonNullApi 21 | package discord4j.connect.common; 22 | 23 | import reactor.util.annotation.NonNullApi; 24 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/JacksonJsonSourceMapper.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import discord4j.connect.common.ConnectPayload; 5 | import discord4j.connect.common.SourceMapper; 6 | import org.reactivestreams.Publisher; 7 | import reactor.core.publisher.Mono; 8 | 9 | /** 10 | * A mapper that can convert a byte array source into a {@link ConnectPayload} using Jackson, if the format is JSON. An 11 | * error is emitted if the deserialization fails. 12 | */ 13 | public class JacksonJsonSourceMapper implements SourceMapper { 14 | 15 | private final ObjectMapper objectMapper; 16 | 17 | public JacksonJsonSourceMapper(final ObjectMapper objectMapper) { 18 | this.objectMapper = objectMapper; 19 | } 20 | 21 | @Override 22 | public Publisher apply(final byte[] source) { 23 | return Mono.fromCallable(() -> objectMapper.readValue(source, ConnectPayload.class)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/ExampleSingleConnect.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect; 2 | 3 | import discord4j.common.store.Store; 4 | import discord4j.common.store.legacy.LegacyStoreLayout; 5 | import discord4j.connect.support.BotSupport; 6 | import discord4j.core.DiscordClientBuilder; 7 | import discord4j.core.GatewayDiscordClient; 8 | import discord4j.core.shard.ShardingStrategy; 9 | import discord4j.store.jdk.JdkStoreService; 10 | 11 | public class ExampleSingleConnect { 12 | 13 | public static void main(String[] args) { 14 | GatewayDiscordClient client = DiscordClientBuilder.create(System.getenv("BOT_TOKEN")) 15 | .build() 16 | .gateway() 17 | .setStore(Store.fromLayout(LegacyStoreLayout.of(new JdkStoreService()))) 18 | .setSharding(ShardingStrategy.fixed(1)) 19 | .login() 20 | .blockOptional() 21 | .orElseThrow(RuntimeException::new); 22 | 23 | BotSupport.create(client) 24 | .eventHandlers() 25 | .block(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/support/EventHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.support; 19 | 20 | import discord4j.core.event.domain.message.MessageCreateEvent; 21 | import reactor.core.publisher.Mono; 22 | 23 | public abstract class EventHandler { 24 | 25 | public abstract Mono onMessageCreate(MessageCreateEvent event); 26 | } 27 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/support/NoBotSupport.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.support; 2 | 3 | import discord4j.core.GatewayDiscordClient; 4 | import discord4j.core.event.domain.lifecycle.ReadyEvent; 5 | import reactor.core.publisher.Mono; 6 | import reactor.util.Logger; 7 | import reactor.util.Loggers; 8 | 9 | public class NoBotSupport { 10 | 11 | private static final Logger log = Loggers.getLogger(NoBotSupport.class); 12 | 13 | private final GatewayDiscordClient client; 14 | 15 | public static NoBotSupport create(GatewayDiscordClient client) { 16 | return new NoBotSupport(client); 17 | } 18 | 19 | NoBotSupport(GatewayDiscordClient client) { 20 | this.client = client; 21 | } 22 | 23 | public Mono eventHandlers() { 24 | return readyHandler(client); 25 | } 26 | 27 | public static Mono readyHandler(GatewayDiscordClient client) { 28 | return client.on(ReadyEvent.class) 29 | .doOnNext(ready -> log.info("Shard [{}] logged in as {}", ready.getShardInfo(), ready.getSelf().getUsername())) 30 | .then(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/PayloadSink.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import reactor.core.publisher.Flux; 21 | 22 | /** 23 | * Reactive producer that sends from a {@link ConnectPayload} source. 24 | */ 25 | public interface PayloadSink { 26 | 27 | /** 28 | * Sends a sequence of messages and returns a {@link Flux} for a response. 29 | * 30 | * @param source sequence of messages to send 31 | * @return a {@link Flux} that can be used to signal a response 32 | */ 33 | Flux send(Flux source); 34 | } 35 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/Discord4JConnectException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | public class Discord4JConnectException extends RuntimeException { 21 | 22 | public Discord4JConnectException() { 23 | } 24 | 25 | public Discord4JConnectException(String message) { 26 | super(message); 27 | } 28 | 29 | public Discord4JConnectException(String message, Throwable cause) { 30 | super(message, cause); 31 | } 32 | 33 | public Discord4JConnectException(Throwable cause) { 34 | super(cause); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/SinkMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import org.reactivestreams.Publisher; 21 | 22 | /** 23 | * A function capable of converting a {@link ConnectPayload} into a type used by a {@link PayloadSink} implementation. 24 | * 25 | * @param the target type 26 | */ 27 | @FunctionalInterface 28 | public interface SinkMapper { 29 | 30 | /** 31 | * Transform a single {@link ConnectPayload} into a {@link Publisher} with a target type to be used by an 32 | * accompanying {@link PayloadSink}. 33 | * 34 | * @param payload the message to process 35 | * @return a reactive response with the target type 36 | */ 37 | Publisher apply(ConnectPayload payload); 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/PayloadSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import reactor.core.publisher.Flux; 21 | import reactor.core.publisher.Mono; 22 | 23 | import java.util.function.Function; 24 | 25 | /** 26 | * Reactive receiver for consuming messages. 27 | */ 28 | public interface PayloadSource { 29 | 30 | /** 31 | * Receive messages and consume them through a given processor. 32 | * 33 | * @param processor a {@link Function} taking a {@link ConnectPayload} message and processing it, acknowledging 34 | * its result through a {@link Mono} response. 35 | * @return a {@link Flux} sequence indicating a response 36 | */ 37 | Flux receive(Function> processor); 38 | } 39 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/SourceMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import org.reactivestreams.Publisher; 21 | 22 | /** 23 | * A function capable of converting a type used by a {@link PayloadSource} implementation into a sequence of 24 | * {@link ConnectPayload} messages. 25 | * 26 | * @param the source type 27 | */ 28 | @FunctionalInterface 29 | public interface SourceMapper { 30 | 31 | /** 32 | * Transform a single source into a {@link Publisher} of {@link ConnectPayload} instances. 33 | * 34 | * @param source the source element provided by a {@link PayloadSource} 35 | * @return a reactive sequence of {@link ConnectPayload} messages 36 | */ 37 | Publisher apply(R source); 38 | } 39 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/README.md: -------------------------------------------------------------------------------- 1 | # RSocket Discord4J Connect Examples 2 | 3 | This folder contains examples to set up an RSocket-based implementation of every component required to work with a distributed Discord4J-based bot. 4 | 5 | ## Components 6 | - D4J Leaders: connect to Gateway and communicates to payload server 7 | - D4J Workers: connect to payload broker and processes messages from leaders 8 | - Distributed global rate limit handling 9 | - Distributed API request queueing (Discord4J Router concept) 10 | - Distributed Gateway connection/IDENTIFY handling 11 | - Distributed payload server 12 | 13 | ## Usage 14 | 15 | Please check `shared` folder for a reference implementation with the following capabilities: 16 | 17 | - Leader/Worker topology: multiple leaders connect to Gateway, multiple workers consume payloads. 18 | - Shared subscription means every worker reads from a work queue consuming payloads. There is no guarantee about the shard index received by each worker. 19 | 20 | Requires you to start three services for: 21 | 22 | - Providing global and API bucket rate limit handling 23 | - Providing websocket connection and IDENTIFY rate limit handling 24 | - Providing payload transport across nodes 25 | 26 | - It is possible to split `RSocketGlobalRouterServer` into `RSocketGlobalRateLimiterServer` and `RSocketRouterServer` 27 | - It is possible to split `RSocketRouterServer` each covering a set of Discord's API request buckets 28 | - It is not possible to scale each individual component at this moment (WIP) 29 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/RSocketPayloadSink.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rsocket.gateway; 2 | 3 | import discord4j.common.retry.ReconnectOptions; 4 | import discord4j.connect.common.ConnectPayload; 5 | import discord4j.connect.common.PayloadSink; 6 | import discord4j.connect.common.SinkMapper; 7 | import discord4j.connect.rsocket.ConnectRSocket; 8 | import io.rsocket.Payload; 9 | import reactor.core.publisher.Flux; 10 | import reactor.util.Logger; 11 | import reactor.util.Loggers; 12 | 13 | import java.net.InetSocketAddress; 14 | 15 | /** 16 | * Implementation of {@link PayloadSink} that is capable of connecting to an RSocket server to send 17 | * {@link ConnectPayload} messages. 18 | */ 19 | public class RSocketPayloadSink implements PayloadSink { 20 | 21 | private static final Logger log = Loggers.getLogger(RSocketPayloadSink.class); 22 | 23 | private final ConnectRSocket socket; 24 | private final SinkMapper mapper; 25 | 26 | public RSocketPayloadSink(InetSocketAddress serverAddress, SinkMapper mapper) { 27 | this.socket = new ConnectRSocket("pl-sink", serverAddress, ctx -> true, ReconnectOptions.create()); 28 | this.mapper = mapper; 29 | } 30 | 31 | @Override 32 | public Flux send(Flux source) { 33 | return socket.withSocket(rSocket -> rSocket.requestChannel(source.flatMap(mapper::apply))) 34 | .doOnError(e -> log.error("Send failed", e)) 35 | .doOnSubscribe(s -> log.info("Begin sending to server")) 36 | .doFinally(s -> log.info("Sender completed after {}", s)); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/servers/ExampleRSocketPayloadServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shared.servers; 19 | 20 | import discord4j.connect.rsocket.gateway.RSocketPayloadServer; 21 | import discord4j.connect.Constants; 22 | import reactor.util.Logger; 23 | import reactor.util.Loggers; 24 | 25 | import java.net.InetSocketAddress; 26 | 27 | public class ExampleRSocketPayloadServer { 28 | 29 | private static final Logger log = Loggers.getLogger(ExampleRSocketPayloadServer.class); 30 | 31 | public static void main(String[] args) { 32 | new RSocketPayloadServer(new InetSocketAddress(Constants.PAYLOAD_SERVER_PORT)) 33 | .start() 34 | .doOnNext(cc -> log.info("Started payload server at {}", cc.address())) 35 | .blockOptional() 36 | .orElseThrow(RuntimeException::new) 37 | .onClose() 38 | .block(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/servers/ExampleRSocketShardCoordinatorServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shared.servers; 19 | 20 | import discord4j.connect.rsocket.shard.RSocketShardCoordinatorServer; 21 | import discord4j.connect.Constants; 22 | import reactor.util.Logger; 23 | import reactor.util.Loggers; 24 | 25 | import java.net.InetSocketAddress; 26 | 27 | public class ExampleRSocketShardCoordinatorServer { 28 | 29 | private static final Logger log = Loggers.getLogger(ExampleRSocketShardCoordinatorServer.class); 30 | 31 | public static void main(String[] args) { 32 | new RSocketShardCoordinatorServer(new InetSocketAddress(Constants.SHARD_COORDINATOR_SERVER_PORT)) 33 | .start() 34 | .doOnNext(cc -> log.info("Started shard coordinator server at {}", cc.address())) 35 | .blockOptional() 36 | .orElseThrow(RuntimeException::new) 37 | .onClose() 38 | .block(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/router/RequestBridge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.router; 19 | 20 | import reactor.core.publisher.MonoProcessor; 21 | 22 | /** 23 | * A container that is capable of signaling both permit acquire and permit release to execute requests. 24 | * 25 | * @param the type used in the request 26 | */ 27 | public class RequestBridge { 28 | 29 | private final String request; 30 | private final MonoProcessor acquire; 31 | private final MonoProcessor release; 32 | 33 | public RequestBridge(String request, MonoProcessor acquire, MonoProcessor release) { 34 | this.request = request; 35 | this.acquire = acquire; 36 | this.release = release; 37 | } 38 | 39 | public String getRequest() { 40 | return request; 41 | } 42 | 43 | public MonoProcessor getAcquire() { 44 | return acquire; 45 | } 46 | 47 | public MonoProcessor getRelease() { 48 | return release; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '*.*' 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | 12 | jobs: 13 | build: 14 | name: Build for JDK ${{ matrix.java }} 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | java: [ 8, 11 ] 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/cache@v1 22 | with: 23 | path: ~/.gradle/caches 24 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 25 | restore-keys: | 26 | ${{ runner.os }}-gradle- 27 | - name: Set up JDK ${{ matrix.java }} 28 | uses: actions/setup-java@v1 29 | with: 30 | java-version: ${{ matrix.java }} 31 | - name: Build with Gradle 32 | run: ./gradlew build 33 | release: 34 | name: Publish artifacts 35 | runs-on: ubuntu-latest 36 | if: github.event_name != 'pull_request' 37 | needs: build 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/cache@v1 41 | with: 42 | path: ~/.gradle/caches 43 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} 44 | restore-keys: | 45 | ${{ runner.os }}-gradle- 46 | - name: Set up JDK 47 | uses: actions/setup-java@v1 48 | with: 49 | java-version: 8 50 | - name: Publish with Gradle 51 | run: ./gradlew -x test publish 52 | env: 53 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.signingKey }} 54 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.signingPassword }} 55 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.sonatypeUsername }} 56 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.sonatypePassword }} -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQBinarySinkMapper.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import discord4j.connect.common.ConnectPayload; 4 | import discord4j.connect.common.SinkMapper; 5 | import org.reactivestreams.Publisher; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.io.ByteArrayOutputStream; 9 | import java.io.DataOutputStream; 10 | import java.nio.charset.StandardCharsets; 11 | 12 | /** 13 | * A mapper that can create a binary (UTF-8) representation of a {@link ConnectPayload}. 14 | */ 15 | public class RabbitMQBinarySinkMapper implements SinkMapper { 16 | 17 | /* 18 | Defined order: 19 | - Shard Count 20 | - Shard Index 21 | - Session Sequence 22 | - Session Id 23 | - Payload Length 24 | - Payload Bytes 25 | */ 26 | @Override 27 | public Publisher apply(ConnectPayload payload) { 28 | return Mono.fromCallable(() -> { 29 | try (final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) { 30 | try (final DataOutputStream outputStream = new DataOutputStream(byteArrayOutputStream)) { 31 | outputStream.writeInt(payload.getShard().getCount()); 32 | outputStream.writeInt(payload.getShard().getIndex()); 33 | outputStream.writeInt(payload.getSession().getSequence()); 34 | outputStream.writeUTF(payload.getSession().getId()); 35 | final byte[] payloadData = payload.getPayload().getBytes(StandardCharsets.UTF_8); 36 | outputStream.writeInt(payloadData.length); 37 | outputStream.write(payloadData); 38 | return byteArrayOutputStream.toByteArray(); 39 | } 40 | } 41 | }); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/RSocketJacksonSourceMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.gateway; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import discord4j.connect.common.ConnectPayload; 22 | import discord4j.connect.common.SourceMapper; 23 | import io.rsocket.Payload; 24 | import org.reactivestreams.Publisher; 25 | import reactor.core.publisher.Mono; 26 | 27 | import java.nio.ByteBuffer; 28 | 29 | /** 30 | * An implementation of {@link SourceMapper} converting RSocket {@link Payload} instances to {@link ConnectPayload}. 31 | */ 32 | public class RSocketJacksonSourceMapper implements SourceMapper { 33 | 34 | private final ObjectMapper mapper; 35 | 36 | public RSocketJacksonSourceMapper(ObjectMapper mapper) { 37 | this.mapper = mapper; 38 | } 39 | 40 | @Override 41 | public Publisher apply(Payload source) { 42 | return Mono.fromCallable(() -> { 43 | ByteBuffer buf = source.getData(); 44 | byte[] array = new byte[buf.remaining()]; 45 | buf.get(array); 46 | return mapper.readValue(array, ConnectPayload.class); 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/ConnectGatewayOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import discord4j.gateway.GatewayClient; 21 | import discord4j.gateway.GatewayOptions; 22 | 23 | /** 24 | * A set of options to configure {@link GatewayClient} instances capable of working in a distributed way. 25 | */ 26 | public class ConnectGatewayOptions extends GatewayOptions { 27 | 28 | private final PayloadSink payloadSink; 29 | private final PayloadSource payloadSource; 30 | 31 | public ConnectGatewayOptions(GatewayOptions parent, PayloadSink payloadSink, PayloadSource payloadSource) { 32 | super(parent.getToken(), parent.getReactorResources(), parent.getPayloadReader(), parent.getPayloadWriter(), 33 | parent.getReconnectOptions(), parent.getIdentifyOptions(), parent.getInitialObserver(), 34 | parent.getIdentifyLimiter(), parent.getMaxMissedHeartbeatAck()); 35 | this.payloadSink = payloadSink; 36 | this.payloadSource = payloadSource; 37 | } 38 | 39 | public PayloadSink getPayloadSink() { 40 | return payloadSink; 41 | } 42 | 43 | public PayloadSource getPayloadSource() { 44 | return payloadSource; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/RSocketJacksonSinkMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.gateway; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import discord4j.connect.common.ConnectPayload; 22 | import discord4j.connect.common.SinkMapper; 23 | import io.rsocket.Payload; 24 | import io.rsocket.util.DefaultPayload; 25 | import org.reactivestreams.Publisher; 26 | import reactor.core.publisher.Mono; 27 | 28 | import java.nio.charset.StandardCharsets; 29 | 30 | /** 31 | * An implementation of {@link SinkMapper} converting {@link ConnectPayload} instances to RSocket {@link Payload}. 32 | */ 33 | public class RSocketJacksonSinkMapper implements SinkMapper { 34 | 35 | private final ObjectMapper mapper; 36 | private final String topic; 37 | 38 | public RSocketJacksonSinkMapper(ObjectMapper mapper, String topic) { 39 | this.mapper = mapper; 40 | this.topic = topic; 41 | } 42 | 43 | @Override 44 | public Publisher apply(ConnectPayload payload) { 45 | return Mono.fromCallable(() -> DefaultPayload.create(mapper.writeValueAsBytes(payload), 46 | ("produce:" + topic).getBytes(StandardCharsets.UTF_8))); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-20.20thread %5p %-36.36logger{36} - %m%n 18 | 19 | 20 | 21 | 22 | 23 | logs/%d{yyyy-MM-dd}.%i.log 24 | 100MB 25 | 90 26 | 27 | 28 | UTF-8 29 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-20.20thread %5p %-36.36logger{36} - %m%n 30 | 31 | true 32 | 33 | 34 | 35 | 512 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQBinarySourceMapper.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import discord4j.connect.common.ConnectPayload; 4 | import discord4j.connect.common.SourceMapper; 5 | import discord4j.gateway.SessionInfo; 6 | import discord4j.gateway.ShardInfo; 7 | import org.reactivestreams.Publisher; 8 | import reactor.core.publisher.Mono; 9 | 10 | import java.io.ByteArrayInputStream; 11 | import java.io.DataInputStream; 12 | import java.nio.charset.StandardCharsets; 13 | 14 | 15 | /** 16 | * A mapper that can read a binary source and extract a {@link ConnectPayload} from it. Emits an error if the 17 | * deserialization fails. 18 | */ 19 | public class RabbitMQBinarySourceMapper implements SourceMapper { 20 | 21 | /* 22 | Defined order: 23 | - Shard Count 24 | - Shard Index 25 | - Session Sequence 26 | - Session Id 27 | - Payload Count 28 | - Payload(s) 29 | */ 30 | @Override 31 | public Publisher apply(byte[] source) { 32 | return Mono.fromCallable(() -> { 33 | try (final DataInputStream dataInputStream = new DataInputStream(new ByteArrayInputStream(source))) { 34 | final int shardCount = dataInputStream.readInt(); 35 | final int shardIndex = dataInputStream.readInt(); 36 | final int sessionSeq = dataInputStream.readInt(); 37 | final String sessionId = dataInputStream.readUTF(); 38 | final int payloadLength = dataInputStream.readInt(); 39 | final byte[] payloadData = new byte[payloadLength]; 40 | dataInputStream.read(payloadData, 0, payloadLength); 41 | final String payload = new String(payloadData, StandardCharsets.UTF_8); 42 | return new ConnectPayload( 43 | ShardInfo.create(shardIndex, shardCount), 44 | SessionInfo.create(sessionId, sessionSeq), 45 | payload 46 | ); 47 | } 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/ConnectPayload.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import com.fasterxml.jackson.annotation.JsonCreator; 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import discord4j.gateway.SessionInfo; 23 | import discord4j.gateway.ShardInfo; 24 | 25 | /** 26 | * Basic messaging type across Discord4J-connect implementations. Allows wrapping a payload while providing information 27 | * about its source. 28 | */ 29 | public class ConnectPayload { 30 | 31 | private final ShardInfo shard; 32 | private final SessionInfo session; 33 | private final String payload; 34 | 35 | @JsonCreator 36 | public ConnectPayload(@JsonProperty("shard") ShardInfo shard, 37 | @JsonProperty("session") SessionInfo session, 38 | @JsonProperty("payload") String payload) { 39 | this.shard = shard; 40 | this.session = session; 41 | this.payload = payload; 42 | } 43 | 44 | public ShardInfo getShard() { 45 | return shard; 46 | } 47 | 48 | public SessionInfo getSession() { 49 | return session; 50 | } 51 | 52 | public String getPayload() { 53 | return payload; 54 | } 55 | 56 | @Override 57 | public String toString() { 58 | return "ConnectPayload{" + 59 | "shard=" + shard + 60 | ", session=" + session + 61 | ", payload='" + payload + '\'' + 62 | '}'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/RSocketPayloadSource.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rsocket.gateway; 2 | 3 | import discord4j.common.retry.ReconnectOptions; 4 | import discord4j.connect.common.ConnectPayload; 5 | import discord4j.connect.common.PayloadSource; 6 | import discord4j.connect.common.SourceMapper; 7 | import discord4j.connect.rsocket.ConnectRSocket; 8 | import io.rsocket.Payload; 9 | import io.rsocket.util.DefaultPayload; 10 | import reactor.core.publisher.Flux; 11 | import reactor.core.publisher.Mono; 12 | import reactor.core.publisher.UnicastProcessor; 13 | import reactor.util.Logger; 14 | import reactor.util.Loggers; 15 | 16 | import java.net.InetSocketAddress; 17 | import java.util.function.Function; 18 | 19 | /** 20 | * Implementation of {@link PayloadSource} that is capable of connecting to an RSocket server to receive 21 | * {@link ConnectPayload} messages. 22 | */ 23 | public class RSocketPayloadSource implements PayloadSource { 24 | 25 | private static final Logger log = Loggers.getLogger(RSocketPayloadSource.class); 26 | 27 | private final SourceMapper mapper; 28 | private final Flux inbound; 29 | 30 | public RSocketPayloadSource(InetSocketAddress serverAddress, String topic, SourceMapper mapper) { 31 | ConnectRSocket socket = new ConnectRSocket("pl-source", serverAddress, ctx -> true, ReconnectOptions.create()); 32 | this.mapper = mapper; 33 | this.inbound = socket.withSocket( 34 | rSocket -> { 35 | UnicastProcessor acks = UnicastProcessor.create(); 36 | acks.onNext(DefaultPayload.create("START", "consume:" + topic)); 37 | return rSocket.requestChannel(acks) 38 | .doOnNext(payload -> acks.onNext(DefaultPayload.create("ACK"))); 39 | }) 40 | .doOnSubscribe(s -> log.info("Begin receiving from server")) 41 | .doFinally(s -> log.info("Receiver completed after {}", s)) 42 | .share(); // allow multicasting inbound payload 43 | } 44 | 45 | @Override 46 | public Flux receive(Function> processor) { 47 | return this.inbound.flatMap(mapper::apply).flatMap(processor); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQSourceMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.gateway; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import com.rabbitmq.client.Delivery; 22 | import discord4j.connect.common.ConnectPayload; 23 | import discord4j.connect.common.SourceMapper; 24 | import org.reactivestreams.Publisher; 25 | 26 | /** 27 | * A higher level mapper that can consume {@link Delivery} instances. 28 | */ 29 | public class RabbitMQSourceMapper implements SourceMapper { 30 | 31 | private final SourceMapper contentMapper; 32 | 33 | RabbitMQSourceMapper(SourceMapper contentMapper) { 34 | this.contentMapper = contentMapper; 35 | } 36 | 37 | /** 38 | * Create a mapper that can consume sources formatted according to {@link RabbitMQBinarySinkMapper}. 39 | * 40 | * @return a binary formatted direct source mapper 41 | */ 42 | public static RabbitMQSourceMapper createBinarySource() { 43 | return new RabbitMQSourceMapper(new RabbitMQBinarySourceMapper()); 44 | } 45 | 46 | /** 47 | * Create a mapper that can consume sources formatted according to {@link JacksonJsonSinkMapper}. 48 | * 49 | * @param objectMapper Jackson resources to use for mapping 50 | * @return a JSON formatted direct source mapper 51 | */ 52 | public static RabbitMQSourceMapper createJsonSource(ObjectMapper objectMapper) { 53 | return new RabbitMQSourceMapper(new JacksonJsonSourceMapper(objectMapper)); 54 | } 55 | 56 | @Override 57 | public Publisher apply(Delivery source) { 58 | return contentMapper.apply(source.getBody()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/ConnectRSocket.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rsocket; 2 | 3 | import discord4j.common.retry.ReconnectOptions; 4 | import io.rsocket.RSocket; 5 | import io.rsocket.core.RSocketConnector; 6 | import io.rsocket.transport.netty.client.TcpClientTransport; 7 | import org.reactivestreams.Publisher; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | import reactor.util.Logger; 11 | import reactor.util.Loggers; 12 | import reactor.util.retry.Retry; 13 | import reactor.util.retry.RetryBackoffSpec; 14 | 15 | import java.net.InetSocketAddress; 16 | import java.util.function.Function; 17 | import java.util.function.Predicate; 18 | 19 | public class ConnectRSocket { 20 | 21 | private static final Logger log = Loggers.getLogger(ConnectRSocket.class); 22 | 23 | private final String name; 24 | private final RetryBackoffSpec retrySpec; 25 | private final Mono rSocketMono; 26 | 27 | public ConnectRSocket(String name, 28 | InetSocketAddress serverAddress, 29 | Predicate retryPredicate, 30 | ReconnectOptions reconnectOptions) { 31 | this.name = name; 32 | this.retrySpec = Retry.backoff(reconnectOptions.getMaxRetries(), reconnectOptions.getFirstBackoff()) 33 | .maxBackoff(reconnectOptions.getMaxBackoffInterval()) 34 | .scheduler(reconnectOptions.getBackoffScheduler()) 35 | .transientErrors(true) 36 | .filter(retryPredicate); 37 | this.rSocketMono = RSocketConnector.create() 38 | .reconnect(retrySpec.doBeforeRetry(signal -> log.debug("[{}] Reconnecting to server (attempt {}): {}", 39 | id(), signal.totalRetriesInARow() + 1, signal.failure().toString()))) 40 | .connect(TcpClientTransport.create(serverAddress)) 41 | .doOnSubscribe(s -> log.debug("[{}] Connecting to RSocket server: {}", id(), serverAddress)); 42 | } 43 | 44 | public Flux withSocket(Function> socketFunction) { 45 | return rSocketMono.flatMapMany(socketFunction) 46 | .retryWhen(retrySpec.doBeforeRetry(signal -> 47 | log.debug("[{}] Retrying action (attempt {}):" + " {}", 48 | id(), signal.totalRetriesInARow() + 1, signal.failure().toString()))); 49 | } 50 | 51 | private String id() { 52 | return name + "-" + Integer.toHexString(hashCode()); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/servers/ExampleRSocketGlobalRouterServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shared.servers; 19 | 20 | import discord4j.connect.Constants; 21 | import discord4j.connect.rsocket.global.RSocketGlobalRouterServer; 22 | import discord4j.rest.request.BucketGlobalRateLimiter; 23 | import discord4j.rest.request.RequestQueueFactory; 24 | import io.rsocket.transport.netty.server.CloseableChannel; 25 | import reactor.core.scheduler.Schedulers; 26 | import reactor.util.Logger; 27 | import reactor.util.Loggers; 28 | import reactor.util.retry.Retry; 29 | 30 | import java.net.InetSocketAddress; 31 | import java.time.Duration; 32 | 33 | /** 34 | * An example of an {@link RSocketGlobalRouterServer}, capable of providing permits to coordinate REST requests across 35 | * multiple nodes and also handle global rate limits. 36 | */ 37 | public class ExampleRSocketGlobalRouterServer { 38 | 39 | private static final Logger log = Loggers.getLogger(ExampleRSocketGlobalRouterServer.class); 40 | 41 | public static void main(String[] args) { 42 | // this server combines a Router and a GlobalRateLimiter servers 43 | // the server keeps a GRL locally to coordinate requests across nodes 44 | RSocketGlobalRouterServer routerServer = 45 | new RSocketGlobalRouterServer(new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_PORT), 46 | BucketGlobalRateLimiter.create(), Schedulers.parallel(), RequestQueueFactory.buffering()); 47 | 48 | // start the server 49 | routerServer.start() 50 | .doOnNext(cc -> log.info("Started global router server at {}", cc.address())) 51 | .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)).maxBackoff(Duration.ofMinutes(1))) 52 | .flatMap(CloseableChannel::onClose) 53 | .block(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/support/LogoutHttpServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.support; 19 | 20 | import discord4j.core.GatewayDiscordClient; 21 | import reactor.core.publisher.Mono; 22 | import reactor.netty.http.server.HttpServer; 23 | import reactor.util.Logger; 24 | import reactor.util.Loggers; 25 | 26 | /** 27 | * A basic {@link HttpServer} example exposing an endpoint to logout a bot from Discord. 28 | */ 29 | public class LogoutHttpServer { 30 | 31 | private static final Logger log = Loggers.getLogger(LogoutHttpServer.class); 32 | 33 | private final GatewayDiscordClient client; 34 | 35 | public LogoutHttpServer(GatewayDiscordClient client) { 36 | this.client = client; 37 | } 38 | 39 | public static void startAsync(GatewayDiscordClient client) { 40 | new LogoutHttpServer(client).start(); 41 | } 42 | 43 | public void start() { 44 | HttpServer.create() 45 | .port(0) // use an ephemeral port 46 | .route(routes -> routes 47 | .get("/logout", 48 | (req, res) -> { 49 | return client.logout() 50 | .then(Mono.from(res.addHeader("content-type", "application/json") 51 | .status(200) 52 | .chunkedTransfer(false) 53 | .sendString(Mono.just("OK")))); 54 | }) 55 | ) 56 | .bind() 57 | .doOnNext(facade -> { 58 | log.info("*************************************************************"); 59 | log.info("Server started at {}:{}", facade.host(), facade.port()); 60 | log.info("*************************************************************"); 61 | // kill the server on JVM exit 62 | Runtime.getRuntime().addShutdownHook(new Thread(() -> facade.disposeNow())); 63 | }) 64 | .subscribe(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/ConnectRabbitMQ.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq; 2 | 3 | import com.rabbitmq.client.Address; 4 | import com.rabbitmq.client.ConnectionFactory; 5 | import reactor.rabbitmq.*; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * An aggregation of a RabbitMQ sender and receiver abstractions. 11 | */ 12 | public class ConnectRabbitMQ { 13 | 14 | private final Sender sender; 15 | private final Receiver receiver; 16 | 17 | ConnectRabbitMQ(final SenderOptions senderOptions, final ReceiverOptions receiverOptions) { 18 | this.sender = RabbitFlux.createSender(senderOptions); 19 | this.receiver = RabbitFlux.createReceiver(receiverOptions); 20 | } 21 | 22 | /** 23 | * Create a default RabbitMQ client. Uses the default parameters as given by {@link ConnectionFactory}. 24 | * 25 | * @return a default client that can connect to a RabbitMQ broker 26 | */ 27 | public static ConnectRabbitMQ createDefault() { 28 | return new ConnectRabbitMQ(new SenderOptions(), new ReceiverOptions()); 29 | } 30 | 31 | /** 32 | * Create a RabbitMQ client from the given settings object. 33 | * 34 | * @param settings a group of settings to customize a client 35 | * @return a client that can connect to a RabbitMQ broker 36 | */ 37 | public static ConnectRabbitMQ createFromSettings(ConnectRabbitMQSettings settings) { 38 | return new ConnectRabbitMQ(settings.getSenderOptions(), settings.getReceiverOptions()); 39 | } 40 | 41 | /** 42 | * Create a RabbitMQ client that can connect to a broker, using the semantics provided in 43 | * {@link ConnectionFactory#newConnection(List)}. 44 | * 45 | * @param clusterIps known broker addresses to try in order 46 | * @return a client that can connect to a RabbitMQ broker 47 | */ 48 | public static ConnectRabbitMQ createWithAddresses(Address... clusterIps) { 49 | return new ConnectRabbitMQ( 50 | new SenderOptions().connectionSupplier(connectionFactory -> connectionFactory.newConnection(clusterIps)), 51 | new ReceiverOptions().connectionSupplier(connectionFactory -> connectionFactory.newConnection(clusterIps)) 52 | ); 53 | } 54 | 55 | /** 56 | * Return the created RabbitMQ sender for broker operations. 57 | * 58 | * @return a RabbitMQ sender 59 | */ 60 | public Sender getSender() { 61 | return sender; 62 | } 63 | 64 | /** 65 | * Return the created RabbitMQ receiver for broker operations. 66 | * 67 | * @return a RabbitMQ receiver 68 | */ 69 | public Receiver getReceiver() { 70 | return receiver; 71 | } 72 | 73 | /** 74 | * Close and free resources created by this client. Follows the semantics of {@link AutoCloseable#close()}. 75 | */ 76 | public void close() { 77 | sender.close(); 78 | receiver.close(); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/router/RSocketRouterOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.router; 19 | 20 | import discord4j.connect.rsocket.global.RSocketGlobalRouterServer; 21 | import discord4j.rest.request.BucketKey; 22 | import discord4j.rest.request.DiscordWebRequest; 23 | import discord4j.rest.request.RouterOptions; 24 | 25 | import java.net.InetSocketAddress; 26 | import java.util.Objects; 27 | import java.util.function.Function; 28 | 29 | /** 30 | * Additional {@link RouterOptions} to configure an {@link RSocketRouter}. 31 | */ 32 | public class RSocketRouterOptions extends RouterOptions { 33 | 34 | private final Function requestTransportMapper; 35 | 36 | /** 37 | * Build a new options instance, based off a parent {@link RouterOptions}. 38 | * 39 | * @param parent the original options instance 40 | * @param requestTransportMapper a mapper to select the proper {@link RSocketRouterServer} or 41 | * {@link RSocketGlobalRouterServer} from a given {@link DiscordWebRequest}. Use 42 | * {@link BucketKey#of(DiscordWebRequest)} to identify the Discord API bucket a requests belongs and return the 43 | * server address. 44 | */ 45 | public RSocketRouterOptions(RouterOptions parent, 46 | Function requestTransportMapper) { 47 | super(parent.getToken(), 48 | parent.getReactorResources(), 49 | parent.getExchangeStrategies(), 50 | parent.getResponseTransformers(), 51 | parent.getGlobalRateLimiter(), 52 | parent.getRequestQueueFactory(), 53 | parent.getDiscordBaseUrl()); 54 | 55 | this.requestTransportMapper = Objects.requireNonNull(requestTransportMapper, "requestTransportMapper"); 56 | } 57 | 58 | /** 59 | * Return a mapper from a {@link DiscordWebRequest} to a {@link RSocketRouterServer} or 60 | * {@link RSocketGlobalRouterServer} address. 61 | * 62 | * @return a function to get a server address 63 | */ 64 | public Function getRequestTransportMapper() { 65 | return requestTransportMapper; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RoutingMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.gateway; 19 | 20 | import reactor.rabbitmq.OutboundMessage; 21 | import reactor.util.function.Tuple2; 22 | import reactor.util.function.Tuples; 23 | 24 | import java.util.Map; 25 | import java.util.Objects; 26 | import java.util.concurrent.ConcurrentHashMap; 27 | 28 | /** 29 | * A pair of exchange and routingKey for RabbitMQ based payloads. 30 | */ 31 | public class RoutingMetadata { 32 | 33 | private static final Map, RoutingMetadata> CACHE = new ConcurrentHashMap<>(); 34 | 35 | private final String exchange; 36 | private final String routingKey; 37 | 38 | RoutingMetadata(String exchange, String routingKey) { 39 | this.exchange = exchange; 40 | this.routingKey = routingKey; 41 | } 42 | 43 | public static RoutingMetadata create(String exchange, String routingKey) { 44 | return CACHE.computeIfAbsent(Tuples.of(exchange, routingKey), 45 | t2 -> new RoutingMetadata(t2.getT1(), t2.getT2())); 46 | } 47 | 48 | public static RoutingMetadata create(OutboundMessage outboundMessage) { 49 | return CACHE.computeIfAbsent(Tuples.of(outboundMessage.getExchange(), outboundMessage.getRoutingKey()), 50 | t2 -> new RoutingMetadata(t2.getT1(), t2.getT2())); 51 | } 52 | 53 | public String getExchange() { 54 | return exchange; 55 | } 56 | 57 | public String getRoutingKey() { 58 | return routingKey; 59 | } 60 | 61 | @Override 62 | public boolean equals(Object o) { 63 | if (this == o) { 64 | return true; 65 | } 66 | if (o == null || getClass() != o.getClass()) { 67 | return false; 68 | } 69 | RoutingMetadata that = (RoutingMetadata) o; 70 | return Objects.equals(exchange, that.exchange) && 71 | Objects.equals(routingKey, that.routingKey); 72 | } 73 | 74 | @Override 75 | public int hashCode() { 76 | return Objects.hash(exchange, routingKey); 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return "RoutingMetadata{" + 82 | "exchange='" + exchange + '\'' + 83 | ", routingKey='" + routingKey + '\'' + 84 | '}'; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect; 19 | 20 | import java.net.InetAddress; 21 | import java.net.UnknownHostException; 22 | 23 | public final class Constants { 24 | 25 | 26 | // TODO: use env variables? 27 | // also better for kubernetes 28 | 29 | // rabbitmq variables 30 | private static final String RABBITMQ_HOST_INTERNAL = System.getenv("RABBITMQ_HOST"); 31 | public static final String RABBITMQ_HOST = (null == RABBITMQ_HOST_INTERNAL || RABBITMQ_HOST_INTERNAL.isEmpty()) ? "": RABBITMQ_HOST_INTERNAL; 32 | // internal default in rabbitmq : -1 33 | public static final String RABBITMQ_PORT_INTERNAL = System.getenv("RABBITMQ_PORT"); 34 | public static int RABBITMQ_PORT; 35 | static { 36 | try { 37 | RABBITMQ_PORT = (null == RABBITMQ_PORT_INTERNAL || RABBITMQ_PORT_INTERNAL.isEmpty()) ? -1 : Integer.parseInt(RABBITMQ_PORT_INTERNAL); 38 | } catch (NumberFormatException e) { 39 | RABBITMQ_PORT = -1; 40 | } 41 | } 42 | 43 | // rsocket variables 44 | private static String LOCALHOST; 45 | static { 46 | try { 47 | LOCALHOST = InetAddress.getLocalHost().getHostName(); 48 | } catch (UnknownHostException e) { 49 | LOCALHOST = "0.0.0.0"; 50 | } 51 | } 52 | private static final String RSOCKET_ROUTER_HOST = System.getenv("RSOCKET_ROUTER_HOST"); 53 | private static final String RSOCKET_SHARD_COORDINATOR_HOST = System.getenv("RSOCKET_SHARD_COORDINATOR_HOST"); 54 | private static final String RSOCKET_PAYLOAD_HOST = System.getenv("RSOCKET_PAYLOAD_HOST"); 55 | 56 | public static String GLOBAL_ROUTER_SERVER_HOST = (null == RSOCKET_ROUTER_HOST || RSOCKET_ROUTER_HOST.isEmpty()) ? LOCALHOST : RSOCKET_ROUTER_HOST; 57 | public static String SHARD_COORDINATOR_SERVER_HOST = (null == RSOCKET_SHARD_COORDINATOR_HOST || RSOCKET_SHARD_COORDINATOR_HOST.isEmpty()) ? LOCALHOST : RSOCKET_SHARD_COORDINATOR_HOST; 58 | public static String PAYLOAD_SERVER_HOST = (null == RSOCKET_PAYLOAD_HOST || RSOCKET_PAYLOAD_HOST.isEmpty()) ? LOCALHOST : RSOCKET_PAYLOAD_HOST; 59 | public static int GLOBAL_ROUTER_SERVER_PORT = 33331; 60 | public static int SHARD_COORDINATOR_SERVER_PORT = 33332; 61 | public static int PAYLOAD_SERVER_PORT = 33333; 62 | 63 | // redis variables 64 | private static final String CLIENT_URI = System.getenv("REDIS_CLIENT_URI"); 65 | public static String REDIS_CLIENT_URI = (null == CLIENT_URI || CLIENT_URI.isEmpty()) ? "redis://localhost:6379" : CLIENT_URI; 66 | 67 | private Constants() { 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/shard/RSocketShardCoordinator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shard; 19 | 20 | import discord4j.common.retry.ReconnectOptions; 21 | import discord4j.connect.rsocket.ConnectRSocket; 22 | import discord4j.core.shard.ShardCoordinator; 23 | import discord4j.gateway.SessionInfo; 24 | import discord4j.gateway.ShardInfo; 25 | import discord4j.gateway.limiter.PayloadTransformer; 26 | import io.rsocket.util.DefaultPayload; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | import reactor.util.Logger; 30 | import reactor.util.Loggers; 31 | 32 | import java.net.InetSocketAddress; 33 | 34 | public class RSocketShardCoordinator implements ShardCoordinator { 35 | 36 | private static final Logger log = Loggers.getLogger(RSocketShardCoordinator.class); 37 | 38 | private final ConnectRSocket socket; 39 | 40 | @Deprecated 41 | public RSocketShardCoordinator(InetSocketAddress socketAddress) { 42 | this.socket = new ConnectRSocket("coordinator", socketAddress, ctx -> true, ReconnectOptions.create()); 43 | } 44 | 45 | public static RSocketShardCoordinator createWithServerAddress(InetSocketAddress socketAddress) { 46 | return new RSocketShardCoordinator(socketAddress); 47 | } 48 | 49 | @Override 50 | public PayloadTransformer getIdentifyLimiter(ShardInfo shardInfo, int shardingFactor) { 51 | int key = shardInfo.getIndex() % shardingFactor; 52 | return sequence -> Flux.from(sequence) 53 | .flatMap(buf -> socket.withSocket(rSocket -> 54 | rSocket.requestResponse(DefaultPayload.create("identify." + key)) 55 | .doOnNext(payload -> log.debug(">: {}", payload.getDataUtf8()))) 56 | .then(Mono.just(buf))); 57 | } 58 | 59 | @Override 60 | public Mono publishConnected(ShardInfo shard) { 61 | return socket.withSocket(rSocket -> rSocket.fireAndForget(DefaultPayload.create("notify.connected"))).then(); 62 | } 63 | 64 | @Override 65 | public Mono publishDisconnected(ShardInfo shard, SessionInfo session) { 66 | return socket.withSocket(rSocket -> rSocket.fireAndForget(DefaultPayload.create("notify.disconnected"))).then(); 67 | } 68 | 69 | @Override 70 | public Mono getConnectedCount() { 71 | return socket.withSocket(rSocket -> rSocket.requestResponse(DefaultPayload.create("request.connected")) 72 | .map(payload -> Integer.parseInt(payload.getDataUtf8()))) 73 | .next(); 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/shard/RSocketShardCoordinatorServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shard; 19 | 20 | import discord4j.common.operator.RateLimitOperator; 21 | import io.rsocket.Payload; 22 | import io.rsocket.RSocket; 23 | import io.rsocket.core.RSocketServer; 24 | import io.rsocket.transport.netty.server.CloseableChannel; 25 | import io.rsocket.transport.netty.server.TcpServerTransport; 26 | import io.rsocket.util.DefaultPayload; 27 | import reactor.core.publisher.Mono; 28 | import reactor.core.scheduler.Schedulers; 29 | import reactor.util.Logger; 30 | import reactor.util.Loggers; 31 | 32 | import java.net.InetSocketAddress; 33 | import java.time.Duration; 34 | import java.util.Map; 35 | import java.util.concurrent.ConcurrentHashMap; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | 38 | public class RSocketShardCoordinatorServer { 39 | 40 | private static final Logger log = Loggers.getLogger(RSocketShardCoordinatorServer.class); 41 | 42 | private final TcpServerTransport serverTransport; 43 | private final AtomicInteger connected = new AtomicInteger(); 44 | 45 | public RSocketShardCoordinatorServer(InetSocketAddress socketAddress) { 46 | // TODO: allow providing a custom backend - to distribute this server 47 | this.serverTransport = TcpServerTransport.create(socketAddress); 48 | } 49 | 50 | public Mono start() { 51 | Map> limiters = new ConcurrentHashMap<>(1); 52 | return RSocketServer.create((setup, sendingSocket) -> Mono.just(socketAcceptor(limiters))) 53 | .bind(serverTransport); 54 | } 55 | 56 | private RSocket socketAcceptor(Map> limiters) { 57 | return new RSocket() { 58 | 59 | @Override 60 | public Mono requestResponse(Payload payload) { 61 | String value = payload.getDataUtf8(); 62 | log.debug("[request_response] >: {}", value); 63 | if (value.startsWith("identify")) { 64 | // identify.shard_limiter_key 65 | String[] tokens = value.split("\\."); 66 | String limiterKey = tokens[1]; 67 | RateLimitOperator limiter = limiters.computeIfAbsent(limiterKey, 68 | k -> new RateLimitOperator<>(1, Duration.ofSeconds(6), Schedulers.parallel())); 69 | return Mono.just(DefaultPayload.create("identify.success")).transform(limiter); 70 | } else if (value.equals("request.connected")) { 71 | return Mono.fromCallable(connected::get).map(count -> DefaultPayload.create(String.valueOf(count))); 72 | } 73 | return Mono.empty(); 74 | } 75 | 76 | @Override 77 | public Mono fireAndForget(Payload payload) { 78 | String value = payload.getDataUtf8(); 79 | log.debug("[request_fnf] >: {}", value); 80 | if (value.equals("notify.connected")) { 81 | return Mono.fromCallable(connected::incrementAndGet).then(); 82 | } else if (value.equals("notify.disconnected")) { 83 | return Mono.fromCallable(connected::decrementAndGet).then(); 84 | } 85 | return Mono.empty(); 86 | } 87 | }; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/router/RequestBridgeStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | package discord4j.connect.rsocket.router; 18 | 19 | import discord4j.rest.request.GlobalRateLimiter; 20 | import discord4j.rest.request.RequestQueue; 21 | import discord4j.rest.request.RequestQueueFactory; 22 | import org.reactivestreams.Subscription; 23 | import reactor.core.publisher.BaseSubscriber; 24 | import reactor.core.publisher.Mono; 25 | import reactor.core.publisher.MonoProcessor; 26 | import reactor.core.publisher.SignalType; 27 | import reactor.core.scheduler.Scheduler; 28 | import reactor.util.Logger; 29 | import reactor.util.Loggers; 30 | 31 | import java.time.Duration; 32 | 33 | /** 34 | * Represents a queue of requests for a single Discord API bucket, consumed serially so they can be checked against a 35 | * {@link GlobalRateLimiter} and properly delayed in case a rate limit is in place. 36 | */ 37 | public class RequestBridgeStream { 38 | 39 | private static final Logger log = Loggers.getLogger(RequestBridgeStream.class); 40 | 41 | private final String id; 42 | private final RequestQueue> requestQueue; 43 | private final GlobalRateLimiter globalRateLimiter; 44 | private final Scheduler rateLimitScheduler; 45 | 46 | private volatile Duration sleepTime = Duration.ZERO; 47 | 48 | public RequestBridgeStream(String id, GlobalRateLimiter globalRateLimiter, Scheduler rateLimitScheduler, 49 | RequestQueueFactory requestQueueFactory) { 50 | this.id = id; 51 | this.requestQueue = requestQueueFactory.create(); 52 | this.globalRateLimiter = globalRateLimiter; 53 | this.rateLimitScheduler = rateLimitScheduler; 54 | } 55 | 56 | public void setSleepTime(Duration sleepTime) { 57 | this.sleepTime = sleepTime; 58 | } 59 | 60 | public void push(RequestBridge request) { 61 | requestQueue.push(request); 62 | } 63 | 64 | public void start() { 65 | requestQueue.requests().subscribe(new RequestSubscriber()); 66 | } 67 | 68 | private class RequestSubscriber extends BaseSubscriber> { 69 | 70 | @Override 71 | protected void hookOnSubscribe(Subscription subscription) { 72 | request(1); 73 | } 74 | 75 | @Override 76 | protected void hookOnNext(RequestBridge correlation) { 77 | String request = correlation.getRequest(); 78 | MonoProcessor acquire = correlation.getAcquire(); 79 | MonoProcessor release = correlation.getRelease(); 80 | if (log.isDebugEnabled()) { 81 | log.debug("Accepting request in bucket {}: {}", id, request); 82 | } 83 | 84 | globalRateLimiter.withLimiter( 85 | Mono.defer(() -> release) 86 | .doOnSubscribe(s -> acquire.onComplete()) 87 | .doFinally(this::next)) 88 | .subscribe(null, t -> log.error("Error while processing {}", request, t)); 89 | } 90 | 91 | private void next(SignalType signal) { 92 | Mono.delay(sleepTime, rateLimitScheduler).subscribe(l -> { 93 | if (log.isDebugEnabled()) { 94 | log.debug("Ready to consume next request in bucket {} after {}", id, signal); 95 | } 96 | sleepTime = Duration.ZERO; 97 | request(1); 98 | }, t -> log.error("Error while scheduling next request in bucket {}", id, t)); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/PayloadDestinationMapper.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.common; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import discord4j.common.JacksonResources; 5 | import org.reactivestreams.Publisher; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.util.Map; 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | 13 | /** 14 | * Reactive mapper to aide in producer-side routing of payloads. 15 | */ 16 | @FunctionalInterface 17 | public interface PayloadDestinationMapper { 18 | 19 | /** 20 | * Takes a payload and specifies the outgoing queue for it 21 | * 22 | * @param payload the payload about to process 23 | * @return the queue name for the payload 24 | */ 25 | Publisher getDestination(ConnectPayload payload); 26 | 27 | /** 28 | * A shard aware destination-mapper to queue all payloads into a specific queue for their shard. 29 | * 30 | * @param queuePrefix the prefix for the queue name 31 | * @return a {@link PayloadDestinationMapper} which returns queues in format {@code queuePrefix + shardId} 32 | */ 33 | static PayloadDestinationMapper shardAware(final String queuePrefix) { 34 | return source -> Mono.fromCallable(() -> source) 35 | .map(payload -> payload.getShard().getIndex()) 36 | .map(shardId -> queuePrefix + shardId); 37 | } 38 | 39 | /** 40 | * A gateway aware destination-mapper to queue queue all payloads into a specific queue for their gateway-node. 41 | * 42 | * @param queuePrefix the prefix for the queue name 43 | * @param gatewayShardMap a map of shardId to gatewayId for resolving the gatewayId by it the given payload shard 44 | * @return a {@link PayloadDestinationMapper} which returns queues in format {@code queuePrefix + gatewayId} 45 | */ 46 | static PayloadDestinationMapper gatewayAware(final String queuePrefix, 47 | final Map gatewayShardMap) { 48 | return source -> Mono.fromCallable(() -> source) 49 | .map(payload -> queuePrefix + gatewayShardMap.get(payload.getShard().getIndex())); 50 | } 51 | 52 | /** 53 | * A destination mapper which distributes payloads based on their event type. 54 | * This method calls {@link PayloadDestinationMapper#eventBased(ObjectMapper, String, Map)} with a new 55 | * object mapper created with a new {@link JacksonResources#getObjectMapper()} 56 | * 57 | * @param fallbackQueue the queue which is used in case there is no other queue defined in queueMap 58 | * @param queueMap Event-to-Queue name mapping 59 | * @return a {@link PayloadDestinationMapper} which returns event-specific queues 60 | */ 61 | static PayloadDestinationMapper eventBased(final String fallbackQueue, final Map queueMap) { 62 | return eventBased(JacksonResources.create().getObjectMapper(), fallbackQueue, queueMap); 63 | } 64 | 65 | /** 66 | * A destination mapper which distributes payloads based on their event type. 67 | * 68 | * @param mapper the {@link ObjectMapper} which is used for deserialization of the payload 69 | * @param fallbackQueue the queue which is used in case there is no other queue defined in queueMap 70 | * @param queueMap Event-to-Queue name mapping 71 | * @return a {@link PayloadDestinationMapper} which returns event-specific queues 72 | */ 73 | static PayloadDestinationMapper eventBased(final ObjectMapper mapper, final String fallbackQueue, 74 | final Map queueMap) { 75 | return source -> Mono.fromCallable(() -> source) 76 | .flatMap(payload -> Mono.fromCallable(() -> mapper.readValue(payload.getPayload(), 77 | PartialGatewayPayload.class))) 78 | .map(partialGatewayPayload -> Optional.ofNullable(partialGatewayPayload.getType())) 79 | .filter(Optional::isPresent) 80 | .map(event -> queueMap.getOrDefault(event.get(), fallbackQueue)) 81 | .defaultIfEmpty(fallbackQueue); 82 | } 83 | 84 | /** 85 | * A basic destination mapper that always routes to the same queue. 86 | * 87 | * @param queue the queue name 88 | * @return a {@link PayloadDestinationMapper} which routes queue to the same given {@code queue} 89 | */ 90 | static PayloadDestinationMapper fixed(final String queue) { 91 | return source -> Mono.just(queue); 92 | } 93 | 94 | /** 95 | * Returns a composed function that first applies this function to its input, and then applies the {@code after} 96 | * function to the result. 97 | * 98 | * @param the type of output of the {@code after} function, and of the composed function 99 | * @param after the function to apply after this function is applied 100 | * @return a composed function that first applies this function and then applies the {@code after} function 101 | */ 102 | default PayloadDestinationMapper andThen(Function, ? extends Publisher> after) { 103 | Objects.requireNonNull(after); 104 | return (ConnectPayload t) -> after.apply(getDestination(t)); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQPayloadSink.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import com.rabbitmq.client.ShutdownSignalException; 4 | import discord4j.connect.common.ConnectPayload; 5 | import discord4j.connect.common.PayloadSink; 6 | import discord4j.connect.common.SinkMapper; 7 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 8 | import org.reactivestreams.Publisher; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | import reactor.rabbitmq.OutboundMessage; 12 | import reactor.rabbitmq.SendOptions; 13 | import reactor.util.Logger; 14 | import reactor.util.Loggers; 15 | import reactor.util.retry.RetryBackoffSpec; 16 | import reactor.util.retry.RetrySpec; 17 | 18 | import java.time.Duration; 19 | import java.util.Objects; 20 | import java.util.function.BiFunction; 21 | 22 | /** 23 | * A RabbitMQ producer that can send payloads to a broker or cluster. 24 | */ 25 | public class RabbitMQPayloadSink implements PayloadSink { 26 | 27 | private static final Logger log = Loggers.getLogger(RabbitMQPayloadSink.class); 28 | 29 | public static final RetryBackoffSpec DEFAULT_RETRY_STRATEGY = 30 | RetrySpec.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) 31 | .filter(t -> !(t instanceof ShutdownSignalException)) 32 | .doBeforeRetry(retry -> log.info("Producer retry {} due to {}", retry.totalRetriesInARow(), 33 | retry.failure())); 34 | 35 | private final SinkMapper mapper; 36 | private final ConnectRabbitMQ rabbitMQ; 37 | private final SendOptions sendOptions; 38 | private final RetryBackoffSpec sendErrorStrategy; 39 | private final BiFunction> onSend; 40 | 41 | RabbitMQPayloadSink(SinkMapper mapper, ConnectRabbitMQ rabbitMQ, SendOptions sendOptions, 42 | RetryBackoffSpec sendErrorStrategy, 43 | BiFunction> onSend) { 44 | this.mapper = mapper; 45 | this.rabbitMQ = rabbitMQ; 46 | this.sendOptions = sendOptions; 47 | this.sendErrorStrategy = sendErrorStrategy; 48 | this.onSend = onSend; 49 | } 50 | 51 | /** 52 | * Create a default sink using an {@link OutboundMessage} mapper. 53 | * 54 | * @param mapper mapper to derive {@link OutboundMessage} instances for sending 55 | * @param rabbitMQ RabbitMQ broker abstraction 56 | * @return a sink ready for producing payloads 57 | */ 58 | public static RabbitMQPayloadSink create(SinkMapper mapper, 59 | ConnectRabbitMQ rabbitMQ) { 60 | return new RabbitMQPayloadSink(mapper, rabbitMQ, new SendOptions(), DEFAULT_RETRY_STRATEGY, null); 61 | } 62 | 63 | /** 64 | * Customize the {@link SendOptions} used when producing each payload. 65 | * 66 | * @param sendOptions options to configure publishing 67 | * @return a new instance with the given parameter 68 | */ 69 | public RabbitMQPayloadSink withSendOptions(SendOptions sendOptions) { 70 | return new RabbitMQPayloadSink(mapper, rabbitMQ, sendOptions, sendErrorStrategy, onSend); 71 | } 72 | 73 | /** 74 | * Customize the retry strategy on sending errors. 75 | * 76 | * @param sendErrorStrategy a Reactor retrying strategy to be applied on producer errors 77 | * @return a new instance with the given parameter 78 | */ 79 | public RabbitMQPayloadSink withSendErrorStrategy(RetryBackoffSpec sendErrorStrategy) { 80 | Objects.requireNonNull(sendErrorStrategy); 81 | return new RabbitMQPayloadSink(mapper, rabbitMQ, sendOptions, sendErrorStrategy, onSend); 82 | } 83 | 84 | /** 85 | * Customize the behavior to perform before a payload is sent. Can be used to declare queues, exchanges or 86 | * bindings. Calling this method will override any previous function. Defaults to no action, leaving you in charge 87 | * of declaring the right RabbitMQ resources for performance improvement. 88 | * 89 | * @param onSend a BiFunction that can be used to apply logic before a payload is sent 90 | * @return a new instance with the given parameter 91 | */ 92 | public RabbitMQPayloadSink withBeforeSendFunction(BiFunction> onSend) { 93 | Objects.requireNonNull(onSend); 94 | return new RabbitMQPayloadSink(mapper, rabbitMQ, sendOptions, sendErrorStrategy, onSend); 95 | } 96 | 97 | @Override 98 | public Flux send(final Flux source) { 99 | Publisher messages; 100 | if (onSend == null) { 101 | messages = source.flatMap(mapper::apply); 102 | } else { 103 | messages = source.flatMap(payload -> Mono.from(mapper.apply(payload))) 104 | .flatMap(message -> onSend.apply(rabbitMQ, RoutingMetadata.create(message)) 105 | .thenReturn(message)); 106 | } 107 | return rabbitMQ.getSender().sendWithTypedPublishConfirms(messages) 108 | .doOnSubscribe(s -> log.info("Begin sending to server")) 109 | .retryWhen(sendErrorStrategy) 110 | .doOnError(e -> log.error("Send failed", e)) 111 | .doFinally(s -> log.info("Sender completed after {}", s)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/ConnectRabbitMQSettings.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq; 2 | 3 | import com.rabbitmq.client.Address; 4 | import reactor.core.scheduler.Scheduler; 5 | import reactor.core.scheduler.Schedulers; 6 | import reactor.rabbitmq.ReceiverOptions; 7 | import reactor.rabbitmq.SenderOptions; 8 | 9 | /** 10 | * A set of commonly used options for connecting to a RabbitMQ broker. 11 | */ 12 | public class ConnectRabbitMQSettings { 13 | 14 | /** 15 | * Creates a new RabbitMQ settings object. 16 | * 17 | * @return settings object 18 | */ 19 | public static ConnectRabbitMQSettings create() { 20 | return new ConnectRabbitMQSettings(); 21 | } 22 | 23 | private final SenderOptions senderOptions; 24 | private final ReceiverOptions receiverOptions; 25 | 26 | /** 27 | * Private constructor, use {@link ConnectRabbitMQSettings#create()} instead 28 | * Defaults the schedulers to use {@link Schedulers#boundedElastic()} 29 | */ 30 | private ConnectRabbitMQSettings() { 31 | this.senderOptions = new SenderOptions(); 32 | this.receiverOptions = new ReceiverOptions(); 33 | withSubscriptionScheduler(Schedulers.boundedElastic()); 34 | withResourceManagementScheduler(Schedulers.boundedElastic()); 35 | } 36 | 37 | /** 38 | * Set the RabbitMQs host-and-port list as {@link Address} array. 39 | * 40 | * @param addresses array of {@link Address} to which RabbitMQ nodes the client should connect 41 | * @return this builder 42 | */ 43 | public ConnectRabbitMQSettings withAddresses(final Address... addresses) { 44 | senderOptions.connectionSupplier(connectionFactory -> connectionFactory.newConnection(addresses)); 45 | receiverOptions.connectionSupplier(connectionFactory -> connectionFactory.newConnection(addresses)); 46 | return this; 47 | } 48 | 49 | /** 50 | * Sets the RabbitMQs host list with default ports. 51 | * 52 | * @param hosts array of hostnames to which RabbitMQ nodes the client should connect on default port 53 | * @return this builder 54 | */ 55 | public ConnectRabbitMQSettings withAddresses(final String... hosts) { 56 | final Address[] addresses = new Address[hosts.length]; 57 | for (int i = 0; i < hosts.length; i++) { 58 | addresses[i] = new Address(hosts[i]); 59 | } 60 | return withAddresses(addresses); 61 | } 62 | 63 | /** 64 | * Sets the RabbitMQs host and port. 65 | * 66 | * @param host hostname to connect to 67 | * @param port port to connect to 68 | * @return this builder 69 | */ 70 | public ConnectRabbitMQSettings withAddress(final String host, final int port) { 71 | return this.withAddresses(new Address(host, port)); 72 | } 73 | 74 | /** 75 | * Sets the RabbitMQs host. This method will use the default port. 76 | * 77 | * @param host hostname to connect to 78 | * @return this builder 79 | */ 80 | public ConnectRabbitMQSettings withAddress(final String host) { 81 | return this.withAddresses(new Address(host)); 82 | } 83 | 84 | /** 85 | * The username to use for authorization. 86 | * 87 | * @param user username for authorization 88 | * @return this builder 89 | */ 90 | public ConnectRabbitMQSettings withUser(final String user) { 91 | senderOptions.getConnectionFactory().setUsername(user); 92 | receiverOptions.getConnectionFactory().setUsername(user); 93 | return this; 94 | } 95 | 96 | /** 97 | * The password to use for authorization. 98 | * 99 | * @param password password for authorization 100 | * @return this builder 101 | */ 102 | public ConnectRabbitMQSettings withPassword(final String password) { 103 | senderOptions.getConnectionFactory().setPassword(password); 104 | receiverOptions.getConnectionFactory().setPassword(password); 105 | return this; 106 | } 107 | 108 | /** 109 | * Uses a specific scheduler for connection subscriptions. 110 | *

111 | * Set the scheduler to {@code null} to create one scheduler per instance 112 | * 113 | * @param scheduler the scheduler to use 114 | * @return this builder 115 | */ 116 | public ConnectRabbitMQSettings withSubscriptionScheduler(final Scheduler scheduler) { 117 | senderOptions.connectionSubscriptionScheduler(scheduler); 118 | receiverOptions.connectionSubscriptionScheduler(scheduler); 119 | return this; 120 | } 121 | 122 | /** 123 | * Uses a specific scheduler for resource management. 124 | *

125 | * Set the scheduler to {@code null} to create one scheduler per instance. 126 | * 127 | * @param scheduler the scheduler to use 128 | * @return this builder 129 | */ 130 | public ConnectRabbitMQSettings withResourceManagementScheduler(final Scheduler scheduler) { 131 | senderOptions.resourceManagementScheduler(scheduler); 132 | return this; 133 | } 134 | 135 | /** 136 | * Internal method to retrieve the whole settings object for the RabbitMQ sender. 137 | * 138 | * @return SenderOptions 139 | */ 140 | SenderOptions getSenderOptions() { 141 | return senderOptions; 142 | } 143 | 144 | /** 145 | * Internal method to retrieve the whole settings object for the RabbitMQ receiver. 146 | * 147 | * @return ReceiverOptions 148 | */ 149 | ReceiverOptions getReceiverOptions() { 150 | return receiverOptions; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQPayloadSource.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rabbitmq.gateway; 2 | 3 | import com.rabbitmq.client.Delivery; 4 | import com.rabbitmq.client.ShutdownSignalException; 5 | import discord4j.connect.common.ConnectPayload; 6 | import discord4j.connect.common.PayloadSource; 7 | import discord4j.connect.common.SourceMapper; 8 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Mono; 11 | import reactor.rabbitmq.ConsumeOptions; 12 | import reactor.rabbitmq.QueueSpecification; 13 | import reactor.util.Logger; 14 | import reactor.util.Loggers; 15 | import reactor.util.retry.RetryBackoffSpec; 16 | import reactor.util.retry.RetrySpec; 17 | 18 | import java.time.Duration; 19 | import java.util.Arrays; 20 | import java.util.Collection; 21 | import java.util.function.Function; 22 | 23 | /** 24 | * A RabbitMQ consumer that can process a stream of incoming payloads. 25 | */ 26 | public class RabbitMQPayloadSource implements PayloadSource { 27 | 28 | private static final Logger log = Loggers.getLogger(RabbitMQPayloadSource.class); 29 | 30 | public static final RetryBackoffSpec DEFAULT_RETRY_STRATEGY = 31 | RetrySpec.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) 32 | .filter(t -> !(t instanceof ShutdownSignalException)) 33 | .doBeforeRetry(retry -> log.info("Consumer retry {} due to {}", retry.totalRetriesInARow(), 34 | retry.failure())); 35 | 36 | private final SourceMapper mapper; 37 | private final ConnectRabbitMQ rabbitMQ; 38 | private final ConsumeOptions consumeOptions; 39 | private final Collection queues; 40 | private final RetryBackoffSpec consumeErrorStrategy; 41 | 42 | RabbitMQPayloadSource(SourceMapper mapper, ConnectRabbitMQ rabbitMQ, ConsumeOptions consumeOptions, 43 | Collection queues, RetryBackoffSpec consumeErrorStrategy) { 44 | this.mapper = mapper; 45 | this.rabbitMQ = rabbitMQ; 46 | this.consumeOptions = consumeOptions; 47 | this.queues = queues; 48 | this.consumeErrorStrategy = consumeErrorStrategy; 49 | } 50 | 51 | /** 52 | * Create a default source using the given parameters, able to subscribe to a list of queues. 53 | * 54 | * @param mapper mapper to read {@link Delivery} instances from the received messages 55 | * @param rabbitMQ RabbitMQ broker abstraction 56 | * @param queues a list of queues that should be subscribed to 57 | * @return a source ready to consume payloads 58 | */ 59 | public static RabbitMQPayloadSource create(SourceMapper mapper, 60 | ConnectRabbitMQ rabbitMQ, 61 | String... queues) { 62 | return new RabbitMQPayloadSource(mapper, rabbitMQ, new ConsumeOptions(), Arrays.asList(queues), 63 | DEFAULT_RETRY_STRATEGY); 64 | } 65 | 66 | /** 67 | * Create a default source using the given parameters, able to subscribe to a list of queues. 68 | * 69 | * @param mapper mapper to read {@link Delivery} instances from the received messages 70 | * @param rabbitMQ RabbitMQ broker abstraction 71 | * @param queues a list of queues that should be subscribed to 72 | * @return a source ready to consume payloads 73 | */ 74 | public static RabbitMQPayloadSource create(SourceMapper mapper, 75 | ConnectRabbitMQ rabbitMQ, 76 | Collection queues) { 77 | return new RabbitMQPayloadSource(mapper, rabbitMQ, new ConsumeOptions(), queues, DEFAULT_RETRY_STRATEGY); 78 | } 79 | 80 | /** 81 | * Customize the {@link ConsumeOptions} used when consuming each payload. 82 | * 83 | * @param consumeOptions options to configure receiving 84 | * @return a new instance with the given parameter 85 | */ 86 | public RabbitMQPayloadSource withConsumeOptions(ConsumeOptions consumeOptions) { 87 | return new RabbitMQPayloadSource(mapper, rabbitMQ, consumeOptions, queues, consumeErrorStrategy); 88 | } 89 | 90 | /** 91 | * Customize the retry strategy on consumer errors. 92 | * 93 | * @param consumeErrorStrategy a Reactor retrying strategy to be applied on consumer errors 94 | * @return a new instance with the given parameter 95 | */ 96 | public RabbitMQPayloadSource withConsumeErrorStrategy(RetryBackoffSpec consumeErrorStrategy) { 97 | return new RabbitMQPayloadSource(mapper, rabbitMQ, consumeOptions, queues, consumeErrorStrategy); 98 | } 99 | 100 | @Override 101 | public Flux receive(Function> processor) { 102 | return Flux.fromIterable(queues) 103 | .flatMap(queue -> Mono.just(queue) 104 | .delaySubscription(rabbitMQ.getSender().declare(QueueSpecification.queue(queue)))) 105 | .flatMap(queue -> rabbitMQ.getReceiver().consumeAutoAck(queue, consumeOptions) 106 | .retryWhen(consumeErrorStrategy)) 107 | .doOnSubscribe(s -> log.info("Begin receiving from server")) 108 | .doOnError(e -> log.error("Receive failed", e)) 109 | .doFinally(s -> log.info("Receiver completed after {}", s)) 110 | .share() // allow multi-casting inbound payload 111 | .flatMap(mapper::apply) 112 | .flatMap(processor); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /rabbitmq/src/main/java/discord4j/connect/rabbitmq/gateway/RabbitMQSinkMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.gateway; 19 | 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import discord4j.connect.common.ConnectPayload; 22 | import discord4j.connect.common.PayloadDestinationMapper; 23 | import discord4j.connect.common.SinkMapper; 24 | import org.reactivestreams.Publisher; 25 | import reactor.core.publisher.Mono; 26 | import reactor.rabbitmq.OutboundMessage; 27 | 28 | import java.util.Objects; 29 | 30 | import static reactor.function.TupleUtils.function; 31 | 32 | /** 33 | * A higher level mapper that can produce {@link OutboundMessage} instances. 34 | */ 35 | public class RabbitMQSinkMapper implements SinkMapper { 36 | 37 | private final PayloadDestinationMapper destinationMapper; 38 | private final SinkMapper contentMapper; 39 | 40 | RabbitMQSinkMapper(PayloadDestinationMapper destinationMapper, 41 | SinkMapper contentMapper) { 42 | this.destinationMapper = Objects.requireNonNull(destinationMapper); 43 | this.contentMapper = Objects.requireNonNull(contentMapper); 44 | } 45 | 46 | /** 47 | * Create a mapper that uses a binary format and routes directly to the given queue. 48 | * 49 | * @param queueName the destination queue 50 | * @return a binary formatted direct sink mapper 51 | */ 52 | public static RabbitMQSinkMapper createBinarySinkToDirect(String queueName) { 53 | return createBinarySinkToDirect(PayloadDestinationMapper.fixed(queueName)); 54 | } 55 | 56 | /** 57 | * Create a mapper that uses a binary format and routes payloads using a {@link PayloadDestinationMapper} 58 | * expected to produce routing keys. The default {@code ""} exchange will be used. 59 | * 60 | * @param mapper the destination mapper to be applied on every payload 61 | * @return a binary formatted direct sink mapper 62 | */ 63 | public static RabbitMQSinkMapper createBinarySinkToDirect(PayloadDestinationMapper mapper) { 64 | return new RabbitMQSinkMapper(mapper.andThen(publisher -> 65 | Mono.just("").zipWith(Mono.from(publisher), RoutingMetadata::create)), 66 | new RabbitMQBinarySinkMapper()); 67 | } 68 | 69 | /** 70 | * Create a mapper that uses a binary format and routes payloads using a {@link PayloadDestinationMapper} 71 | * expected to produce routing keys. A given exchange name is used. 72 | * 73 | * @param mapper the destination mapper to be applied on every payload 74 | * @param exchange the destination exchange to use on each payload sent 75 | * @return a binary formatted sink mapper using a named exchange 76 | */ 77 | public static RabbitMQSinkMapper createBinarySinkToExchange(PayloadDestinationMapper mapper, 78 | String exchange) { 79 | return new RabbitMQSinkMapper(mapper.andThen(publisher -> 80 | Mono.just(exchange).zipWith(Mono.from(publisher), RoutingMetadata::create)), 81 | new RabbitMQBinarySinkMapper()); 82 | } 83 | 84 | /** 85 | * Create a mapper that uses a binary format and routes payloads using a {@link PayloadDestinationMapper} 86 | * expected to produce {@link RoutingMetadata} instances, defining both exchange and routing key. 87 | * 88 | * @param mapper the destination mapper to be applied on every payload 89 | * @return a binary formatted sink mapper using a customized routing strategy 90 | */ 91 | public static RabbitMQSinkMapper createBinarySink(PayloadDestinationMapper mapper) { 92 | return new RabbitMQSinkMapper(mapper, new RabbitMQBinarySinkMapper()); 93 | } 94 | 95 | /** 96 | * Change the underlying {@link SinkMapper} this mapper used to convert the content of a payload. 97 | * 98 | * @param sinkMapper a custom mapper to use in the new instance 99 | * @return a new instance using the given parameter 100 | */ 101 | public RabbitMQSinkMapper withContentMapper(SinkMapper sinkMapper) { 102 | return new RabbitMQSinkMapper(destinationMapper, sinkMapper); 103 | } 104 | 105 | /** 106 | * Change the underlying mapper to use {@link JacksonJsonSinkMapper} for its content. 107 | * 108 | * @param objectMapper Jackson resources to use for mapping 109 | * @return a new instance using the given parameter 110 | */ 111 | public RabbitMQSinkMapper withJsonContentMapper(ObjectMapper objectMapper) { 112 | return new RabbitMQSinkMapper(destinationMapper, new JacksonJsonSinkMapper(objectMapper)); 113 | } 114 | 115 | @Override 116 | public Publisher apply(ConnectPayload payload) { 117 | return Mono.from(destinationMapper.getDestination(payload)) 118 | .zipWith(Mono.from(contentMapper.apply(payload))) 119 | .map(function((key, content) -> new OutboundMessage(key.getExchange(), key.getRoutingKey(), content))); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/global/RSocketGlobalRateLimiter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.global; 19 | 20 | import discord4j.common.retry.ReconnectOptions; 21 | import discord4j.connect.common.Discord4JConnectException; 22 | import discord4j.connect.rsocket.ConnectRSocket; 23 | import discord4j.rest.request.GlobalRateLimiter; 24 | import io.rsocket.Payload; 25 | import io.rsocket.util.DefaultPayload; 26 | import org.reactivestreams.Publisher; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | import reactor.core.publisher.UnicastProcessor; 30 | import reactor.util.Logger; 31 | import reactor.util.Loggers; 32 | 33 | import java.net.InetSocketAddress; 34 | import java.time.Duration; 35 | 36 | /** 37 | * A {@link GlobalRateLimiter} implementation that is capable to communicate with an 38 | * {@link RSocketGlobalRateLimiterServer} or {@link RSocketGlobalRouterServer} to coordinate requests against the 39 | * Discord global rate limit. 40 | */ 41 | public class RSocketGlobalRateLimiter implements GlobalRateLimiter { 42 | 43 | private static final Logger log = Loggers.getLogger(RSocketGlobalRateLimiter.class); 44 | private static final String ACQUIRE = "ACQUIRE"; 45 | private static final String PERMIT = "PERMIT"; 46 | private static final String RELEASE = "RELEASE"; 47 | private static final String LIMIT_GLOBAL = "LIMIT:global"; 48 | private static final String LIMIT_QUERY = "QUERY:global"; 49 | 50 | private final ConnectRSocket socket; 51 | 52 | @Deprecated 53 | public RSocketGlobalRateLimiter(InetSocketAddress socketAddress) { 54 | this.socket = new ConnectRSocket("grl", socketAddress, ctx -> true, ReconnectOptions.create()); 55 | } 56 | 57 | public static RSocketGlobalRateLimiter createWithServerAddress(InetSocketAddress socketAddress) { 58 | return new RSocketGlobalRateLimiter(socketAddress); 59 | } 60 | 61 | @Override 62 | public Mono rateLimitFor(Duration duration) { 63 | return socket.withSocket(rSocket -> rSocket.requestResponse(limitPayload(duration))).then(); 64 | } 65 | 66 | @Override 67 | public Mono getRemaining() { 68 | return socket.withSocket(rSocket -> rSocket.requestResponse(queryLimit()) 69 | .map(payload -> { 70 | String content = payload.getDataUtf8(); 71 | if (content.startsWith(LIMIT_QUERY)) { 72 | String[] tokens = content.split(":", 4); 73 | Duration remaining = Duration.ofNanos(Long.parseLong(tokens[2])); 74 | long at = Long.parseLong(tokens[3]); 75 | Duration lag = Duration.ofNanos(System.nanoTime() - at); 76 | log.debug("Remaining global limit: {} (delta: {})", remaining, lag); 77 | return orZero(remaining.minus(lag)); 78 | } else { 79 | log.warn("Unknown payload: {}", content); 80 | } 81 | return Duration.ZERO; 82 | })) 83 | .next(); 84 | } 85 | 86 | private static Duration orZero(Duration duration) { 87 | return duration.isNegative() ? Duration.ZERO : duration; 88 | } 89 | 90 | @Override 91 | public Flux withLimiter(Publisher stage) { 92 | return socket.withSocket(rSocket -> { 93 | UnicastProcessor toLeader = UnicastProcessor.create(); 94 | String id = Integer.toHexString(System.identityHashCode(stage)); 95 | toLeader.onNext(acquirePayload(id)); 96 | return rSocket.requestChannel(toLeader) 97 | .onErrorMap(Discord4JConnectException::new) 98 | .doOnSubscribe(s -> log.info("[{}] Subscribed to RSocketGRL pipeline", id)) 99 | .doFinally(s -> log.info("[{}] Released RSocketGRL pipeline: {}", id, s)) 100 | .flatMap(payload -> { 101 | String content = payload.getDataUtf8(); 102 | if (content.startsWith(PERMIT)) { 103 | return Flux.from(stage) 104 | .doOnTerminate(() -> { 105 | log.debug("[{}] Request completed", id); 106 | toLeader.onNext(releasePayload(id)); 107 | toLeader.onComplete(); 108 | }); 109 | } else { 110 | log.warn("Unknown payload: {}", content); 111 | } 112 | return Mono.empty(); 113 | }); 114 | }); 115 | } 116 | 117 | private static Payload limitPayload(Duration duration) { 118 | return DefaultPayload.create(LIMIT_GLOBAL + ":" + duration.toNanos() + ":" + System.nanoTime()); 119 | } 120 | 121 | private static Payload queryLimit() { 122 | return DefaultPayload.create(LIMIT_QUERY); 123 | } 124 | 125 | private static Payload acquirePayload(String id) { 126 | return DefaultPayload.create(ACQUIRE + ":" + id); 127 | } 128 | 129 | private static Payload releasePayload(String id) { 130 | return DefaultPayload.create(RELEASE + ":" + id); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/router/RSocketRouterServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.router; 19 | 20 | import discord4j.rest.request.GlobalRateLimiter; 21 | import discord4j.rest.request.RequestQueueFactory; 22 | import io.rsocket.Payload; 23 | import io.rsocket.RSocket; 24 | import io.rsocket.core.RSocketServer; 25 | import io.rsocket.transport.netty.server.CloseableChannel; 26 | import io.rsocket.transport.netty.server.TcpServerTransport; 27 | import io.rsocket.util.DefaultPayload; 28 | import org.reactivestreams.Publisher; 29 | import reactor.core.publisher.Flux; 30 | import reactor.core.publisher.Mono; 31 | import reactor.core.publisher.MonoProcessor; 32 | import reactor.core.scheduler.Scheduler; 33 | import reactor.util.Logger; 34 | import reactor.util.Loggers; 35 | 36 | import java.net.InetSocketAddress; 37 | import java.time.Duration; 38 | import java.util.Map; 39 | import java.util.concurrent.ConcurrentHashMap; 40 | 41 | /** 42 | * An RSocket server capable of queueing and handling the rate limits of Discord API requests, supporting 43 | * {@link RSocketRouter} implementations. 44 | */ 45 | public class RSocketRouterServer { 46 | 47 | private static final Logger log = Loggers.getLogger(RSocketRouterServer.class); 48 | private static final String REQUEST = "REQUEST"; 49 | private static final String READY = "READY"; 50 | private static final String LIMIT = "LIMIT"; 51 | private static final String DONE = "DONE"; 52 | 53 | private final TcpServerTransport serverTransport; 54 | private final GlobalRateLimiter globalRateLimiter; 55 | private final Scheduler rateLimitScheduler; 56 | private final RequestQueueFactory requestQueueFactory; 57 | private final Map streams = new ConcurrentHashMap<>(); 58 | 59 | public RSocketRouterServer(InetSocketAddress socketAddress, GlobalRateLimiter globalRateLimiter, 60 | Scheduler rateLimitScheduler, RequestQueueFactory requestQueueFactory) { 61 | this.serverTransport = TcpServerTransport.create(socketAddress); 62 | this.globalRateLimiter = globalRateLimiter; 63 | this.rateLimitScheduler = rateLimitScheduler; 64 | this.requestQueueFactory = requestQueueFactory; 65 | } 66 | 67 | public Mono start() { 68 | return RSocketServer.create((setup, sendingSocket) -> Mono.just(leaderAcceptor())) 69 | .bind(serverTransport); 70 | } 71 | 72 | private RSocket leaderAcceptor() { 73 | return new RSocket() { 74 | 75 | @Override 76 | public Flux requestChannel(Publisher payloads) { 77 | MonoProcessor release = MonoProcessor.create(); 78 | return Flux.from(payloads) 79 | .flatMap(payload -> { 80 | String content = payload.getDataUtf8(); 81 | if (content.startsWith(REQUEST)) { 82 | String[] tokens = content.split(":", 3); 83 | String bucket = tokens[1]; 84 | String request = tokens[2]; 85 | log.debug("[B:{}, R:{}] Incoming request", bucket, request); 86 | MonoProcessor acquire = MonoProcessor.create(); 87 | RequestBridge notifier = new RequestBridge<>(request, acquire, release); 88 | getStream(bucket).push(notifier); 89 | return acquire.thenReturn(DefaultPayload.create(READY)) 90 | .doOnSuccess(__ -> log.debug("[B:{}, R:{}] Notifying worker to execute request", 91 | bucket, request)); 92 | } else if (content.startsWith(DONE)) { 93 | String[] tokens = content.split(":", 3); 94 | String bucket = tokens[1]; 95 | String request = tokens[2]; 96 | log.debug("[B:{}, R:{}] Completing request", bucket, request); 97 | release.onComplete(); 98 | } else { 99 | log.warn("[requestChannel] Unsupported payload: {}", content); 100 | } 101 | return Mono.empty(); 102 | }); 103 | } 104 | 105 | @Override 106 | public Mono requestResponse(Payload payload) { 107 | String content = payload.getDataUtf8(); 108 | if (content.startsWith(LIMIT)) { 109 | String[] tokens = content.split(":", 3); 110 | String bucket = tokens[1]; 111 | long millis = Long.parseLong(tokens[2]); 112 | Duration delay = Duration.ofMillis(millis); 113 | log.debug("[B:{}] Notifying server to delay by {}", bucket, delay); 114 | getStream(bucket).setSleepTime(delay); 115 | } else { 116 | log.warn("[requestResponse] Unsupported payload: {}", content); 117 | } 118 | return Mono.empty(); 119 | } 120 | }; 121 | } 122 | 123 | private RequestBridgeStream getStream(String bucket) { 124 | return streams.computeIfAbsent(bucket, k -> { 125 | RequestBridgeStream stream = new RequestBridgeStream(k, globalRateLimiter, rateLimitScheduler, 126 | requestQueueFactory); 127 | stream.start(); 128 | return stream; 129 | }); 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/gateway/RSocketPayloadServer.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.rsocket.gateway; 2 | 3 | import io.rsocket.Payload; 4 | import io.rsocket.RSocket; 5 | import io.rsocket.core.RSocketServer; 6 | import io.rsocket.transport.netty.server.CloseableChannel; 7 | import io.rsocket.transport.netty.server.TcpServerTransport; 8 | import io.rsocket.util.DefaultPayload; 9 | import org.reactivestreams.Publisher; 10 | import reactor.core.publisher.*; 11 | import reactor.extra.processor.WorkQueueProcessor; 12 | import reactor.util.Logger; 13 | import reactor.util.Loggers; 14 | import reactor.util.function.Tuple2; 15 | 16 | import java.net.InetSocketAddress; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.function.Function; 20 | 21 | /** 22 | * An RSocket-based payload server, holding queues to process messages from multiple sources. 23 | */ 24 | public class RSocketPayloadServer { 25 | 26 | private static final Logger log = Loggers.getLogger(RSocketPayloadServer.class); 27 | 28 | private final TcpServerTransport serverTransport; 29 | private final Function> processorFactory; 30 | 31 | private final Map> queues = new ConcurrentHashMap<>(); 32 | private final Map> sinks = new ConcurrentHashMap<>(); 33 | 34 | public RSocketPayloadServer(InetSocketAddress socketAddress) { 35 | this(socketAddress, topic -> { 36 | if (topic.contains("outbound")) { 37 | log.info("Creating fanout queue: {}", topic); 38 | return EmitterProcessor.create(1024, false); 39 | } 40 | log.info("Creating work queue: {}", topic); 41 | return WorkQueueProcessor.builder() 42 | .autoCancel(false) 43 | .share(true) 44 | .name(topic) 45 | .bufferSize(1024) 46 | .build(); 47 | }); 48 | } 49 | 50 | public RSocketPayloadServer(InetSocketAddress socketAddress, 51 | Function> processorFactory) { 52 | this.serverTransport = TcpServerTransport.create(socketAddress); 53 | this.processorFactory = processorFactory; 54 | } 55 | 56 | public Mono start() { 57 | return RSocketServer.create((setup, sendingSocket) -> Mono.just(leaderAcceptor(sendingSocket))) 58 | .bind(serverTransport); 59 | } 60 | 61 | private RSocket leaderAcceptor(RSocket sendingSocket) { 62 | return new RSocket() { 63 | 64 | @Override 65 | public Flux requestChannel(Publisher payloads) { 66 | 67 | // an incoming payload MUST have routing metadata information 68 | // : 69 | // "produce:inbound" or "consume:inbound", etc 70 | 71 | // produce case: [payload] [payload] ... 72 | // consume case: [START] [ACK] [ACK] ... 73 | 74 | String id = Integer.toHexString(System.identityHashCode(sendingSocket)); 75 | 76 | return Flux.from(payloads) 77 | .switchOnFirst((signal, flux) -> { 78 | if (signal.hasValue()) { 79 | Payload first = signal.get(); 80 | assert first != null; 81 | String[] command = getCommand(first); 82 | String key = command[0]; 83 | String topic = command[1]; 84 | if (key.equals("produce")) { 85 | return flux.doOnSubscribe(s -> log.debug("[{}] Producing to {}", id, topic)) 86 | .map(payload -> { 87 | log.trace("[{}] Produce to {}: {}", id, topic, payload.getDataUtf8()); 88 | getSink(topic).next(payload); 89 | return DefaultPayload.create("OK", topic); 90 | }); 91 | } else if (key.equals("consume")) { 92 | // flux is a sequence of "ACKs" to trigger the next payload 93 | return Flux.defer(() -> getQueue(topic)) 94 | .limitRate(1) 95 | .zipWith(flux) 96 | .map(Tuple2::getT1) 97 | .doOnSubscribe(s -> log.debug("[{}] Consuming from {}", id, topic)) 98 | .doOnNext(payload -> { 99 | if (sendingSocket.availability() < 1.0d) { 100 | throw new IllegalStateException("Consumer is unavailable"); 101 | } 102 | }); 103 | } 104 | } 105 | return Flux.error(new IllegalArgumentException( 106 | "Invalid routing: must be produce, consume")); 107 | }) 108 | .doFinally(s -> log.info("[{}] Terminating channel after {}", id, s)); 109 | } 110 | 111 | private String[] getCommand(Payload payload) { 112 | if (!payload.hasMetadata()) { 113 | throw new IllegalArgumentException("Missing topic metadata"); 114 | } 115 | String metadata = payload.getMetadataUtf8(); 116 | String[] tokens = metadata.split(":", 2); 117 | if (tokens.length != 2 || tokens[1].isEmpty()) { 118 | throw new IllegalArgumentException("Invalid topic metadata"); 119 | } 120 | return tokens; 121 | } 122 | 123 | private FluxProcessor getQueue(String topic) { 124 | return queues.computeIfAbsent(topic, processorFactory); 125 | } 126 | 127 | private FluxSink getSink(String topic) { 128 | return sinks.computeIfAbsent(topic, k -> getQueue(k).sink(FluxSink.OverflowStrategy.LATEST)); 129 | } 130 | }; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /rsocket/src/main/java/discord4j/connect/rsocket/global/RSocketGlobalRateLimiterServer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.global; 19 | 20 | import discord4j.connect.rsocket.router.RequestBridge; 21 | import discord4j.connect.rsocket.router.RequestBridgeStream; 22 | import discord4j.rest.request.GlobalRateLimiter; 23 | import discord4j.rest.request.RequestQueueFactory; 24 | import io.rsocket.Payload; 25 | import io.rsocket.RSocket; 26 | import io.rsocket.core.RSocketServer; 27 | import io.rsocket.transport.netty.server.CloseableChannel; 28 | import io.rsocket.transport.netty.server.TcpServerTransport; 29 | import io.rsocket.util.DefaultPayload; 30 | import org.reactivestreams.Publisher; 31 | import reactor.core.publisher.Flux; 32 | import reactor.core.publisher.Mono; 33 | import reactor.core.publisher.MonoProcessor; 34 | import reactor.core.scheduler.Scheduler; 35 | import reactor.util.Logger; 36 | import reactor.util.Loggers; 37 | 38 | import java.net.InetSocketAddress; 39 | import java.time.Duration; 40 | 41 | /** 42 | * An RSocket server providing Discord global rate limit handling. 43 | */ 44 | public class RSocketGlobalRateLimiterServer { 45 | 46 | private static final Logger log = Loggers.getLogger(RSocketGlobalRateLimiterServer.class); 47 | private static final String ACQUIRE = "ACQUIRE"; 48 | private static final String PERMIT = "PERMIT"; 49 | private static final String RELEASE = "RELEASE"; 50 | private static final String LIMIT_GLOBAL = "LIMIT:global"; 51 | private static final String LIMIT_QUERY = "QUERY:global"; 52 | 53 | private final TcpServerTransport serverTransport; 54 | private final GlobalRateLimiter delegate; 55 | private final RequestBridgeStream globalStream; 56 | 57 | public RSocketGlobalRateLimiterServer(InetSocketAddress socketAddress, GlobalRateLimiter delegate, 58 | Scheduler rateLimitScheduler, RequestQueueFactory requestQueueFactory) { 59 | this.serverTransport = TcpServerTransport.create(socketAddress); 60 | this.delegate = delegate; 61 | this.globalStream = new RequestBridgeStream("global", delegate, rateLimitScheduler, requestQueueFactory); 62 | this.globalStream.start(); 63 | } 64 | 65 | public Mono start() { 66 | return RSocketServer.create((setup, sendingSocket) -> Mono.just(socketAcceptor())) 67 | .bind(serverTransport); 68 | } 69 | 70 | private RSocket socketAcceptor() { 71 | return new RSocket() { 72 | 73 | @Override 74 | public Flux requestChannel(Publisher payloads) { 75 | MonoProcessor release = MonoProcessor.create(); 76 | return Flux.from(payloads) 77 | .flatMap(payload -> { 78 | String content = payload.getDataUtf8(); 79 | if (content.startsWith(ACQUIRE)) { 80 | String[] tokens = content.split(":", 2); 81 | String id = tokens[1]; 82 | log.debug("[{}] Acquire request", id); 83 | MonoProcessor acquire = MonoProcessor.create(); 84 | RequestBridge notifier = new RequestBridge<>(id, acquire, release); 85 | globalStream.push(notifier); 86 | return acquire.thenReturn(DefaultPayload.create(PERMIT)) 87 | .doOnSuccess(__ -> log.debug("[{}] Acquired permit for request", id)); 88 | } else if (content.startsWith(RELEASE)) { 89 | String[] tokens = content.split(":", 2); 90 | String id = tokens[1]; 91 | log.debug("[{}] Release request", id); 92 | release.onComplete(); 93 | } else { 94 | log.warn("[requestChannel] Unsupported payload: {}", content); 95 | } 96 | return Mono.empty(); 97 | }); 98 | } 99 | 100 | @Override 101 | public Mono requestResponse(Payload payload) { 102 | String content = payload.getDataUtf8(); 103 | if (content.startsWith(LIMIT_GLOBAL)) { 104 | // LIMIT:global:{rateLimitNanos}:{nanoTimestamp} 105 | // reply with "OK:{nanoTimestamp}" 106 | String[] tokens = content.split(":", 4); 107 | Duration rateLimit = Duration.ofNanos(Long.parseLong(tokens[2])); 108 | Duration lag = Duration.ofNanos(System.nanoTime() - Long.parseLong(tokens[3])); 109 | log.debug("[{}] Rate limiting globally by {} (delta: {})", rateLimit, lag); 110 | return delegate.rateLimitFor(orZero(rateLimit.minus(lag))).then(Mono.just(okPayload())); 111 | } else if (content.startsWith(LIMIT_QUERY)) { 112 | // QUERY:global 113 | // reply with "QUERY:global:{remaining}:{nanoTimestamp}" 114 | return delegate.getRemaining().map(RSocketGlobalRateLimiterServer::queryLimitReply); 115 | } 116 | return Mono.empty(); 117 | } 118 | }; 119 | } 120 | 121 | private static Duration orZero(Duration duration) { 122 | return duration.isNegative() ? Duration.ZERO : duration; 123 | } 124 | 125 | private static Payload okPayload() { 126 | return DefaultPayload.create("OK:" + System.nanoTime()); 127 | } 128 | 129 | private static Payload queryLimitReply(Duration remaining) { 130 | return DefaultPayload.create(LIMIT_QUERY + ":" + remaining.toNanos() + ":" + System.nanoTime()); 131 | } 132 | 133 | 134 | } 135 | -------------------------------------------------------------------------------- /common/src/main/java/discord4j/connect/common/UpstreamGatewayClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.common; 19 | 20 | import discord4j.common.close.CloseStatus; 21 | import discord4j.discordjson.json.gateway.Dispatch; 22 | import discord4j.gateway.*; 23 | import discord4j.gateway.json.GatewayPayload; 24 | import discord4j.gateway.payload.PayloadReader; 25 | import io.netty.buffer.ByteBuf; 26 | import io.netty.buffer.Unpooled; 27 | import org.reactivestreams.Publisher; 28 | import reactor.core.Scannable; 29 | import reactor.core.publisher.Flux; 30 | import reactor.core.publisher.Mono; 31 | import reactor.core.publisher.Sinks; 32 | import reactor.core.scheduler.Schedulers; 33 | import reactor.util.Logger; 34 | import reactor.util.Loggers; 35 | import reactor.util.retry.Retry; 36 | 37 | import java.nio.charset.StandardCharsets; 38 | import java.time.Duration; 39 | import java.util.concurrent.locks.LockSupport; 40 | import java.util.function.Function; 41 | 42 | /** 43 | * A {@link GatewayClient} implementation that connects to Discord Gateway through {@link DefaultGatewayClient} while 44 | * also providing a way to route messages through {@link PayloadSink} and {@link PayloadSource}, capable of delivering 45 | * such messages across multiple nodes. 46 | */ 47 | public class UpstreamGatewayClient implements GatewayClient { 48 | 49 | private static final Logger log = Loggers.getLogger(UpstreamGatewayClient.class); 50 | 51 | private final DefaultGatewayClient delegate; 52 | private final PayloadSink sink; 53 | private final PayloadSource source; 54 | private final ShardInfo shardInfo; 55 | private final PayloadReader payloadReader; 56 | 57 | public UpstreamGatewayClient(ConnectGatewayOptions gatewayOptions) { 58 | this.delegate = new DefaultGatewayClient(gatewayOptions); 59 | this.shardInfo = gatewayOptions.getIdentifyOptions().getShardInfo(); 60 | this.sink = gatewayOptions.getPayloadSink(); 61 | this.source = gatewayOptions.getPayloadSource(); 62 | this.payloadReader = gatewayOptions.getPayloadReader(); 63 | } 64 | 65 | @Override 66 | public Mono execute(String gatewayUrl) { 67 | // Receive from Discord --> Send to downstream 68 | Mono senderFuture = 69 | sink.send(receiver( 70 | buf -> Mono.just(toConnectPayload(buf.toString(StandardCharsets.UTF_8))) 71 | .doFinally(s -> buf.release()))) 72 | .subscribeOn(Schedulers.newSingle("payload-sender")) 73 | .doOnError(t -> log.error("Sender error", t)) 74 | .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(2)) 75 | .maxBackoff(Duration.ofSeconds(30))) 76 | .then(); 77 | 78 | // Receive from downstream --> Send to Discord 79 | Mono receiverFuture = source.receive(payloadProcessor()) 80 | .doOnError(t -> log.error("Receiver error", t)) 81 | .retryWhen(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(2)) 82 | .maxBackoff(Duration.ofSeconds(30))) 83 | .then(); 84 | 85 | return Mono.zip(senderFuture, receiverFuture, delegate.execute(gatewayUrl)).then(); 86 | } 87 | 88 | private Function> payloadProcessor() { 89 | Sinks.Many> senderSink = sender(); 90 | return connectPayload -> { 91 | if (Boolean.TRUE.equals(senderSink.scan(Scannable.Attr.CANCELLED))) { 92 | return Mono.error(new IllegalStateException("Sender was cancelled")); 93 | } 94 | if (connectPayload.getShard().getIndex() != shardInfo.getIndex()) { 95 | return Mono.empty(); 96 | } 97 | // TODO: reduce allocations in this area 98 | return Flux.from(payloadReader.read(Unpooled.wrappedBuffer(connectPayload.getPayload().getBytes(StandardCharsets.UTF_8)))) 99 | .doOnNext(gatewayPayload -> { 100 | while (senderSink.tryEmitNext(gatewayPayload).isFailure()) { 101 | LockSupport.parkNanos(10); 102 | }}) 103 | .then(); 104 | }; 105 | } 106 | 107 | private ConnectPayload toConnectPayload(String gatewayPayload) { 108 | return new ConnectPayload(shardInfo, SessionInfo.create(getSessionId(), getSequence()), gatewayPayload); 109 | } 110 | 111 | @Override 112 | public Mono close(boolean allowResume) { 113 | return delegate.close(allowResume); 114 | } 115 | 116 | @Override 117 | public Flux dispatch() { 118 | return delegate.dispatch(); 119 | } 120 | 121 | @Override 122 | public Flux> receiver() { 123 | return delegate.receiver(); 124 | } 125 | 126 | @Override 127 | public Flux receiver(Function> mapper) { 128 | return delegate.receiver(mapper); 129 | } 130 | 131 | @Override 132 | public Sinks.Many> sender() { 133 | return delegate.sender(); 134 | } 135 | 136 | @Override 137 | public Mono sendBuffer(Publisher publisher) { 138 | return delegate.sendBuffer(publisher); 139 | } 140 | 141 | @Override 142 | public int getShardCount() { 143 | return shardInfo.getCount(); 144 | } 145 | 146 | @Override 147 | public String getSessionId() { 148 | return delegate.getSessionId(); 149 | } 150 | 151 | @Override 152 | public int getSequence() { 153 | return delegate.getSequence(); 154 | } 155 | 156 | @Override 157 | public Mono isConnected() { 158 | return delegate.isConnected(); 159 | } 160 | 161 | @Override 162 | public Flux stateEvents() { 163 | return delegate.stateEvents(); 164 | } 165 | 166 | @Override 167 | public Duration getResponseTime() { 168 | return delegate.getResponseTime(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/ExampleRSocketWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.DownstreamGatewayClient; 26 | import discord4j.connect.rsocket.gateway.RSocketJacksonSinkMapper; 27 | import discord4j.connect.rsocket.gateway.RSocketJacksonSourceMapper; 28 | import discord4j.connect.rsocket.gateway.RSocketPayloadSink; 29 | import discord4j.connect.rsocket.gateway.RSocketPayloadSource; 30 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 31 | import discord4j.connect.rsocket.router.RSocketRouter; 32 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 33 | import discord4j.connect.support.BotSupport; 34 | import discord4j.connect.support.ExtraBotSupport; 35 | import discord4j.connect.support.LogoutHttpServer; 36 | import discord4j.connect.support.NoBotSupport; 37 | import discord4j.core.DiscordClient; 38 | import discord4j.core.GatewayDiscordClient; 39 | import discord4j.core.shard.MemberRequestFilter; 40 | import discord4j.core.shard.ShardingStrategy; 41 | import discord4j.store.api.readonly.ReadOnlyStoreService; 42 | import discord4j.store.redis.RedisStoreService; 43 | import io.lettuce.core.RedisClient; 44 | import reactor.core.publisher.Mono; 45 | 46 | import java.net.InetSocketAddress; 47 | 48 | /** 49 | * An example distributed Discord4J worker, or a node that is capable of processing payloads coming from leaders, 50 | * executing API requests and sending payloads back to the leaders, if needed. 51 | *

52 | * In particular, this example covers: 53 | *

    54 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 55 | *
  • Connecting to a distributed Router for API requests
  • 56 | *
  • Connecting to a distributed ShardCoordinator for connect/IDENTIFY request rate limiting
  • 57 | *
  • Connecting to a RSocket payload server to send messages across boundaries
  • 58 | *
  • Using redis as entity cache (write capable in leaders, read-only in workers)
  • 59 | *
  • Shared subscription using {@link ShardingStrategy#single()}: stateless workers reading from every shard.
  • 60 | *
61 | */ 62 | public class ExampleRSocketWorker { 63 | 64 | public static void main(String[] args) { 65 | 66 | // define the host and port where the global router is listening to 67 | // define the host and port where the payload server is listening to 68 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 69 | InetSocketAddress payloadServerAddress = new InetSocketAddress(Constants.PAYLOAD_SERVER_HOST, Constants.PAYLOAD_SERVER_PORT); 70 | 71 | // use a common jackson factory to reuse it where possible 72 | JacksonResources jackson = JacksonResources.create(); 73 | 74 | // use redis to store entity caches 75 | RedisClient redisClient = RedisClient.create(Constants.REDIS_CLIENT_URI); 76 | 77 | // use a "single" strategy where this worker will be capable of reading payloads from every shard 78 | // - load will be shared across workers 79 | // - no guarantee (yet..) about receiving payloads from the same shard ID in this worker node 80 | // - if that is your use case, use ShardingStrategy.recommended() 81 | ShardingStrategy singleStrategy = ShardingStrategy.single(); 82 | 83 | // define the GlobalRouterServer as GRL for all nodes in this architecture 84 | // define the GlobalRouterServer as Router for all request buckets in this architecture 85 | // create the RSocket capable Router of queueing API requests across boundaries 86 | // shard coordinator is not needed by workers: they do not establish Discord Gateway connections 87 | // disable memberRequests as leader makes them (and we have disabled write access to entity cache) 88 | // define the ConnectGatewayOptions to send payloads across boundaries 89 | // RSocketPayloadSink: payloads workers send to leaders through the payload server 90 | // RSocketPayloadSource: payloads leaders send to workers through the payload server 91 | // we use DownstreamGatewayClient that is capable of using above components to work in a distributed way 92 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 93 | .setJacksonResources(jackson) 94 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 95 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 96 | .build(RSocketRouter::new) 97 | .gateway() 98 | .setSharding(singleStrategy) 99 | .setMemberRequestFilter(MemberRequestFilter.none()) 100 | .setStore(Store.fromLayout(LegacyStoreLayout.of(new ReadOnlyStoreService(RedisStoreService.builder() 101 | .redisClient(redisClient) 102 | .build())))) 103 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 104 | new RSocketPayloadSink(payloadServerAddress, 105 | new RSocketJacksonSinkMapper(jackson.getObjectMapper(), "outbound")), 106 | new RSocketPayloadSource(payloadServerAddress, "inbound", 107 | new RSocketJacksonSourceMapper(jackson.getObjectMapper())))) 108 | .login(DownstreamGatewayClient::new) 109 | .blockOptional() 110 | .orElseThrow(RuntimeException::new); 111 | 112 | LogoutHttpServer.startAsync(client); 113 | if (Boolean.parseBoolean(System.getenv("EXAMPLE_COMMANDS"))) { 114 | Mono.when( 115 | BotSupport.create(client).eventHandlers(), 116 | ExtraBotSupport.create(client).eventHandlers() 117 | ).block(); 118 | } else { 119 | NoBotSupport.create(client) 120 | .eventHandlers() 121 | .block(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/support/BotSupport.java: -------------------------------------------------------------------------------- 1 | package discord4j.connect.support; 2 | 3 | import discord4j.common.util.Snowflake; 4 | import discord4j.core.GatewayDiscordClient; 5 | import discord4j.core.event.domain.lifecycle.ReadyEvent; 6 | import discord4j.core.event.domain.message.MessageCreateEvent; 7 | import discord4j.core.object.entity.Message; 8 | import discord4j.core.object.entity.User; 9 | import discord4j.core.object.presence.ClientPresence; 10 | import discord4j.discordjson.json.ApplicationInfoData; 11 | import discord4j.discordjson.json.ImmutableMessageCreateRequest; 12 | import discord4j.discordjson.possible.Possible; 13 | import reactor.core.publisher.Mono; 14 | import reactor.core.scheduler.Schedulers; 15 | import reactor.util.Logger; 16 | import reactor.util.Loggers; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | import java.util.stream.Collectors; 21 | 22 | public class BotSupport { 23 | 24 | private static final Logger log = Loggers.getLogger(BotSupport.class); 25 | 26 | private final GatewayDiscordClient client; 27 | 28 | public static BotSupport create(GatewayDiscordClient client) { 29 | return new BotSupport(client); 30 | } 31 | 32 | BotSupport(GatewayDiscordClient client) { 33 | this.client = client; 34 | } 35 | 36 | public Mono eventHandlers() { 37 | return Mono.when(readyHandler(client), commandHandler(client)); 38 | } 39 | 40 | public static Mono readyHandler(GatewayDiscordClient client) { 41 | return client.on(ReadyEvent.class) 42 | .doOnNext(ready -> log.info("Logged in as {}", ready.getSelf().getUsername())) 43 | .then(); 44 | } 45 | 46 | public static Mono commandHandler(GatewayDiscordClient client) { 47 | Mono ownerId = client.rest().getApplicationInfo() 48 | .map(ApplicationInfoData::owner) 49 | .map(user -> Snowflake.asLong(user.id())) 50 | .cache(); 51 | 52 | List eventHandlers = new ArrayList<>(); 53 | eventHandlers.add(new Echo()); 54 | eventHandlers.add(new Status()); 55 | eventHandlers.add(new StatusEmbed()); 56 | eventHandlers.add(new Exit()); 57 | 58 | return client.on(MessageCreateEvent.class, 59 | event -> ownerId.filter( 60 | owner -> { 61 | Long author = event.getMessage().getAuthor() 62 | .map(User::getId) 63 | .map(Snowflake::asLong) 64 | .orElse(null); 65 | return owner.equals(author); 66 | }) 67 | .flatMap(id -> Mono.when(eventHandlers.stream() 68 | .map(handler -> handler.onMessageCreate(event)) 69 | .collect(Collectors.toList())) 70 | )) 71 | .then(); 72 | } 73 | 74 | public static class Echo extends EventHandler { 75 | 76 | @Override 77 | public Mono onMessageCreate(MessageCreateEvent event) { 78 | Message message = event.getMessage(); 79 | String content = message.getContent(); 80 | if (content.startsWith("!echo ")) { 81 | return message.getRestChannel().createMessage( 82 | ImmutableMessageCreateRequest.builder() 83 | .content(Possible.of("<@" + message.getUserData().id() + "> " + content.substring("!echo ".length()))) 84 | .build()) 85 | .then(); 86 | } 87 | return Mono.empty(); 88 | } 89 | } 90 | 91 | public static class StatusEmbed extends EventHandler { 92 | 93 | @Override 94 | public Mono onMessageCreate(MessageCreateEvent event) { 95 | Message message = event.getMessage(); 96 | return Mono.justOrEmpty(message.getContent()) 97 | .filter(content -> content.equals("!status")) 98 | .flatMap(source -> message.getChannel() 99 | .publishOn(Schedulers.boundedElastic()) 100 | .flatMap(channel -> channel.createEmbed(spec -> { 101 | spec.setThumbnail(event.getClient().getSelf() 102 | .blockOptional() 103 | .orElseThrow(RuntimeException::new) 104 | .getAvatarUrl()); 105 | spec.addField("Servers", event.getClient().getGuilds().count() 106 | .blockOptional() 107 | .orElse(-1L) 108 | .toString(), false); 109 | spec.addField("Application-Info", event.getClient().getApplicationInfo() 110 | .blockOptional() 111 | .orElseThrow(RuntimeException::new) 112 | .toString(), false); 113 | }))) 114 | .then(); 115 | } 116 | } 117 | 118 | public static class Status extends EventHandler { 119 | 120 | @Override 121 | public Mono onMessageCreate(MessageCreateEvent event) { 122 | Message message = event.getMessage(); 123 | return Mono.justOrEmpty(message.getContent()) 124 | .filter(content -> content.startsWith("!status ")) 125 | .map(content -> { 126 | String status = content.substring("!status ".length()); 127 | if (status.equalsIgnoreCase("online")) { 128 | return ClientPresence.online(); 129 | } else if (status.equalsIgnoreCase("dnd")) { 130 | return ClientPresence.doNotDisturb(); 131 | } else if (status.equalsIgnoreCase("idle")) { 132 | return ClientPresence.idle(); 133 | } else if (status.equalsIgnoreCase("invisible")) { 134 | return ClientPresence.invisible(); 135 | } else { 136 | throw new IllegalArgumentException("Invalid argument"); 137 | } 138 | }) 139 | .flatMap(presence -> event.getClient().updatePresence(presence)) 140 | .then(); 141 | } 142 | } 143 | 144 | public static class Exit extends EventHandler { 145 | 146 | @Override 147 | public Mono onMessageCreate(MessageCreateEvent event) { 148 | Message message = event.getMessage(); 149 | return Mono.justOrEmpty(message.getContent()) 150 | .filter(content -> content.equals("!exit")) 151 | .flatMap(presence -> event.getClient().logout()) 152 | .then(); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rabbitmq/shared/ExampleRabbitLocalCacheWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.DownstreamGatewayClient; 26 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 27 | import discord4j.connect.rabbitmq.ConnectRabbitMQSettings; 28 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSink; 29 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSource; 30 | import discord4j.connect.rabbitmq.gateway.RabbitMQSinkMapper; 31 | import discord4j.connect.rabbitmq.gateway.RabbitMQSourceMapper; 32 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 33 | import discord4j.connect.rsocket.router.RSocketRouter; 34 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 35 | import discord4j.connect.support.BotSupport; 36 | import discord4j.connect.support.ExtraBotSupport; 37 | import discord4j.connect.support.LogoutHttpServer; 38 | import discord4j.connect.support.NoBotSupport; 39 | import discord4j.core.DiscordClient; 40 | import discord4j.core.GatewayDiscordClient; 41 | import discord4j.core.shard.ShardingStrategy; 42 | import discord4j.store.redis.RedisStoreService; 43 | import io.lettuce.core.RedisClient; 44 | import reactor.core.publisher.Mono; 45 | 46 | import java.net.InetSocketAddress; 47 | 48 | /** 49 | * An example distributed Discord4J worker, or a node that is capable of processing payloads coming from leaders, 50 | * executing API requests and sending payloads back to the leaders, if needed. 51 | *

52 | * In particular, this example covers: 53 | *

    54 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 55 | *
  • Connecting to a distributed Router for API requests
  • 56 | *
  • Connecting to a RabbitMQ broker to send and receive messages from other nodes
  • 57 | *
  • Connecting to a redis server to use it as entity cache, using READ-ONLY mode
  • 58 | *
  • Shared subscription using {@link ShardingStrategy#single()}: stateless workers reading from every shard
  • 59 | *
60 | */ 61 | public class ExampleRabbitLocalCacheWorker { 62 | 63 | public static void main(String[] args) { 64 | 65 | /* 66 | * Define the location of the Global Router Server (GRS). A GRS combines coordinated routing across API 67 | * requests while also dealing with the global rate limits. 68 | * 69 | * We will use RSocket GRS in this example: see ExampleRSocketGlobalRouterServer 70 | */ 71 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 72 | 73 | /* 74 | * Define the redis server that will be used as entity cache. 75 | */ 76 | RedisClient redisClient = RedisClient.create(Constants.REDIS_CLIENT_URI); 77 | 78 | /* 79 | * Create a default factory for working with Jackson, this can be reused across the application. 80 | */ 81 | JacksonResources jackson = JacksonResources.create(); 82 | 83 | /* 84 | * Define the sharding strategy. Workers in this "stateless" configuration should use the single factory. 85 | * This saves bootstrap efforts by grouping all inbound payloads into one entity (when using 86 | * DownstreamGatewayClient) 87 | */ 88 | ShardingStrategy shardingStrategy = ShardingStrategy.single(); 89 | 90 | /* 91 | * Define the key resources for working with RabbitMQ. 92 | * - ConnectRabbitMQ defines the parameters to a server 93 | * - RabbitMQSinkMapper will be used to PRODUCE payloads to other nodes 94 | * - "createBinarySinkToDirect" will create binary messages, sent to the "gateway" queue directly. 95 | * - RabbitMQSourceMapper will be used to CONSUME payloads from other nodes 96 | * - "createBinarySource" will read binary messages 97 | */ 98 | ConnectRabbitMQ rabbitMQ; 99 | if (!Constants.RABBITMQ_HOST.isEmpty()) { 100 | ConnectRabbitMQSettings settings = ConnectRabbitMQSettings.create().withAddress(Constants.RABBITMQ_HOST, Constants.RABBITMQ_PORT); 101 | rabbitMQ = ConnectRabbitMQ.createFromSettings(settings); 102 | } else { 103 | rabbitMQ = ConnectRabbitMQ.createDefault(); 104 | } 105 | RabbitMQSinkMapper sinkMapper = RabbitMQSinkMapper.createBinarySinkToDirect("gateway"); 106 | RabbitMQSourceMapper sourceMapper = RabbitMQSourceMapper.createBinarySource(); 107 | 108 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 109 | .setJacksonResources(jackson) 110 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 111 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 112 | .build(RSocketRouter::new) 113 | .gateway() 114 | .setSharding(shardingStrategy) 115 | // Set a fully capable entity cache as this worker is also performing save tasks 116 | .setStore(Store.fromLayout(LegacyStoreLayout.of(RedisStoreService.builder() 117 | .redisClient(redisClient) 118 | .build()))) 119 | // Turn this gateway into a RabbitMQ-based one 120 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 121 | RabbitMQPayloadSink.create(sinkMapper, rabbitMQ), 122 | RabbitMQPayloadSource.create(sourceMapper, rabbitMQ, "payload"))) 123 | // DownstreamGatewayClient does not connect to Gateway and receives payloads from other nodes 124 | .login(DownstreamGatewayClient::new) 125 | .blockOptional() 126 | .orElseThrow(RuntimeException::new); 127 | 128 | LogoutHttpServer.startAsync(client); 129 | if (Boolean.parseBoolean(System.getenv("EXAMPLE_COMMANDS"))) { 130 | Mono.when( 131 | BotSupport.create(client).eventHandlers(), 132 | ExtraBotSupport.create(client).eventHandlers() 133 | ).block(); 134 | } else { 135 | NoBotSupport.create(client) 136 | .eventHandlers() 137 | .block(); 138 | } 139 | rabbitMQ.close(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rabbitmq/shared/ExampleRabbitDistributedCacheWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.DownstreamGatewayClient; 26 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 27 | import discord4j.connect.rabbitmq.ConnectRabbitMQSettings; 28 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSink; 29 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSource; 30 | import discord4j.connect.rabbitmq.gateway.RabbitMQSinkMapper; 31 | import discord4j.connect.rabbitmq.gateway.RabbitMQSourceMapper; 32 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 33 | import discord4j.connect.rsocket.router.RSocketRouter; 34 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 35 | import discord4j.connect.support.BotSupport; 36 | import discord4j.connect.support.ExtraBotSupport; 37 | import discord4j.connect.support.LogoutHttpServer; 38 | import discord4j.connect.support.NoBotSupport; 39 | import discord4j.core.DiscordClient; 40 | import discord4j.core.GatewayDiscordClient; 41 | import discord4j.core.shard.MemberRequestFilter; 42 | import discord4j.core.shard.ShardingStrategy; 43 | import discord4j.store.api.readonly.ReadOnlyStoreService; 44 | import discord4j.store.redis.RedisStoreService; 45 | import io.lettuce.core.RedisClient; 46 | import reactor.core.publisher.Mono; 47 | 48 | import java.net.InetSocketAddress; 49 | 50 | /** 51 | * An example distributed Discord4J worker, or a node that is capable of processing payloads coming from leaders, 52 | * executing API requests and sending payloads back to the leaders, if needed. 53 | *

54 | * In particular, this example covers: 55 | *

    56 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 57 | *
  • Connecting to a distributed Router for API requests
  • 58 | *
  • Connecting to a RabbitMQ broker to send and receive messages from other nodes
  • 59 | *
  • Connecting to a redis server to use it as entity cache, using READ-ONLY mode
  • 60 | *
  • Shared subscription using {@link ShardingStrategy#single()}: stateless workers reading from every shard
  • 61 | *
62 | */ 63 | public class ExampleRabbitDistributedCacheWorker { 64 | 65 | public static void main(String[] args) { 66 | 67 | /* 68 | * Define the location of the Global Router Server (GRS). A GRS combines coordinated routing across API 69 | * requests while also dealing with the global rate limits. 70 | * 71 | * We will use RSocket GRS in this example: see ExampleRSocketGlobalRouterServer 72 | */ 73 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 74 | 75 | /* 76 | * Define the redis server that will be used as entity cache. 77 | */ 78 | RedisClient redisClient = RedisClient.create(Constants.REDIS_CLIENT_URI); 79 | 80 | /* 81 | * Create a default factory for working with Jackson, this can be reused across the application. 82 | */ 83 | JacksonResources jackson = JacksonResources.create(); 84 | 85 | /* 86 | * Define the sharding strategy. Workers in this "stateless" configuration should use the single factory. 87 | * This saves bootstrap efforts by grouping all inbound payloads into one entity (when using 88 | * DownstreamGatewayClient) 89 | */ 90 | ShardingStrategy shardingStrategy = ShardingStrategy.single(); 91 | 92 | /* 93 | * Define the key resources for working with RabbitMQ. 94 | * - ConnectRabbitMQ defines the parameters to a server 95 | * - RabbitMQSinkMapper will be used to PRODUCE payloads to other nodes 96 | * - "createBinarySinkToDirect" will create binary messages, sent to the "gateway" queue directly. 97 | * - RabbitMQSourceMapper will be used to CONSUME payloads from other nodes 98 | * - "createBinarySource" will read binary messages 99 | */ 100 | ConnectRabbitMQ rabbitMQ; 101 | if (!Constants.RABBITMQ_HOST.isEmpty()) { 102 | ConnectRabbitMQSettings settings = ConnectRabbitMQSettings.create().withAddress(Constants.RABBITMQ_HOST, Constants.RABBITMQ_PORT); 103 | rabbitMQ = ConnectRabbitMQ.createFromSettings(settings); 104 | } else { 105 | rabbitMQ = ConnectRabbitMQ.createDefault(); 106 | } 107 | RabbitMQSinkMapper sinkMapper = RabbitMQSinkMapper.createBinarySinkToDirect("gateway"); 108 | RabbitMQSourceMapper sourceMapper = RabbitMQSourceMapper.createBinarySource(); 109 | 110 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 111 | .setJacksonResources(jackson) 112 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 113 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 114 | .build(RSocketRouter::new) 115 | .gateway() 116 | .setSharding(shardingStrategy) 117 | // Gateway member requests are handled upstream, so disable them here 118 | .setMemberRequestFilter(MemberRequestFilter.none()) 119 | // Set the entity cache, but wrap with a read-only decorator 120 | .setStore(Store.fromLayout(LegacyStoreLayout.of(new ReadOnlyStoreService(RedisStoreService.builder() 121 | .redisClient(redisClient) 122 | .build())))) 123 | // Turn this gateway into a RabbitMQ-based one 124 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 125 | RabbitMQPayloadSink.create(sinkMapper, rabbitMQ), 126 | RabbitMQPayloadSource.create(sourceMapper, rabbitMQ, "payload"))) 127 | // DownstreamGatewayClient does not connect to Gateway and receives payloads from other nodes 128 | .login(DownstreamGatewayClient::new) 129 | .blockOptional() 130 | .orElseThrow(RuntimeException::new); 131 | 132 | LogoutHttpServer.startAsync(client); 133 | if (Boolean.parseBoolean(System.getenv("EXAMPLE_COMMANDS"))) { 134 | Mono.when( 135 | BotSupport.create(client).eventHandlers(), 136 | ExtraBotSupport.create(client).eventHandlers() 137 | ).block(); 138 | } else { 139 | NoBotSupport.create(client) 140 | .eventHandlers() 141 | .block(); 142 | } 143 | rabbitMQ.close(); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rsocket/shared/ExampleRSocketLeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rsocket.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.UpstreamGatewayClient; 26 | import discord4j.connect.rsocket.gateway.RSocketJacksonSinkMapper; 27 | import discord4j.connect.rsocket.gateway.RSocketJacksonSourceMapper; 28 | import discord4j.connect.rsocket.gateway.RSocketPayloadSink; 29 | import discord4j.connect.rsocket.gateway.RSocketPayloadSource; 30 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 31 | import discord4j.connect.rsocket.router.RSocketRouter; 32 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 33 | import discord4j.connect.rsocket.shard.RSocketShardCoordinator; 34 | import discord4j.connect.support.LogoutHttpServer; 35 | import discord4j.core.DiscordClient; 36 | import discord4j.core.GatewayDiscordClient; 37 | import discord4j.core.event.dispatch.DispatchEventMapper; 38 | import discord4j.core.shard.ShardingStrategy; 39 | import discord4j.store.redis.RedisStoreService; 40 | import io.lettuce.core.RedisClient; 41 | import reactor.util.Logger; 42 | import reactor.util.Loggers; 43 | 44 | import java.net.InetSocketAddress; 45 | 46 | /** 47 | * An example distributed Discord4J leader, or a node that is capable of connecting to Discord Gateway and routing 48 | * its messages to other nodes, across JVM boundaries. 49 | *

50 | * In particular, this example covers: 51 | *

    52 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 53 | *
  • Connecting to a distributed Router for API requests
  • 54 | *
  • Connecting to a distributed ShardCoordinator for connect/IDENTIFY request rate limiting
  • 55 | *
  • Connecting to a RSocket payload server to send messages across boundaries
  • 56 | *
  • Using redis as entity cache (write capable in leaders, read-only in workers)
  • 57 | *
  • Defining the sharding strategy for the leaders
  • 58 | *
59 | */ 60 | public class ExampleRSocketLeader { 61 | 62 | public static void main(String[] args) { 63 | 64 | // define the host and port where the global router is listening to 65 | // define the host and port where the shard coordinator is listening to 66 | // define the host and port where the payload server is listening to 67 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 68 | InetSocketAddress coordinatorServerAddress = new InetSocketAddress(Constants.SHARD_COORDINATOR_SERVER_HOST, Constants.SHARD_COORDINATOR_SERVER_PORT); 69 | InetSocketAddress payloadServerAddress = new InetSocketAddress(Constants.PAYLOAD_SERVER_HOST, Constants.PAYLOAD_SERVER_PORT); 70 | 71 | // use a common jackson factory to reuse it where possible 72 | JacksonResources jackson = JacksonResources.create(); 73 | 74 | // use redis to store entity caches 75 | RedisClient redisClient = RedisClient.create(Constants.REDIS_CLIENT_URI); 76 | 77 | // Select your ShardingStrategy 78 | // a) use the recommended amount of shards, and connect this leader to all of them into a group 79 | // b) use a set number of shards, and connect this leader to all of them into a group 80 | // c) build a custom sharding strategy: 81 | // - indexes to filter which shard IDs to connect this leader 82 | // - count to set a fixed shard count (or do not set one to use the recommended amount) 83 | ShardingStrategy recommendedStrategy = ShardingStrategy.recommended(); 84 | ShardingStrategy fixedStrategy = ShardingStrategy.fixed(2); 85 | ShardingStrategy customStrategy = ShardingStrategy.builder() 86 | .indices(0, 2) // only connect this leader to shard IDs 0 and 2 87 | .count(4) // but still split our bot guilds into 4 shards 88 | .build(); 89 | 90 | // define the GlobalRouterServer as GRL for all nodes in this architecture 91 | // define the GlobalRouterServer as Router for all request buckets in this architecture 92 | // create the RSocket capable Router of queueing API requests across boundaries 93 | // coordinate ws connect and IDENTIFY rate limit across leader nodes using this server 94 | // define the ConnectGatewayOptions to send payloads across boundaries 95 | // RSocketPayloadSink: payloads leaders send to workers through the payload server 96 | // RSocketPayloadSource: payloads workers send to leaders through the payload server 97 | // we use UpstreamGatewayClient that is capable of using above components to work in a distributed way 98 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 99 | .setJacksonResources(jackson) 100 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 101 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 102 | .build(RSocketRouter::new) 103 | .gateway() 104 | .setSharding(recommendedStrategy) 105 | .setShardCoordinator(RSocketShardCoordinator.createWithServerAddress(coordinatorServerAddress)) 106 | // .setDisabledIntents(IntentSet.of( 107 | // Intent.GUILD_PRESENCES, 108 | // Intent.GUILD_MESSAGE_TYPING, 109 | // Intent.DIRECT_MESSAGE_TYPING)) 110 | // .setInitialStatus(s -> Presence.invisible()) 111 | 112 | // .setInvalidationStrategy(InvalidationStrategy.disable()) 113 | 114 | .setStore(Store.fromLayout(LegacyStoreLayout.of(RedisStoreService.builder() 115 | .redisClient(redisClient) 116 | .useSharedConnection(false) 117 | .build()))) 118 | .setDispatchEventMapper(DispatchEventMapper.discardEvents()) 119 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 120 | new RSocketPayloadSink(payloadServerAddress, 121 | new RSocketJacksonSinkMapper(jackson.getObjectMapper(), "inbound")), 122 | new RSocketPayloadSource(payloadServerAddress, "outbound", 123 | new RSocketJacksonSourceMapper(jackson.getObjectMapper())))) 124 | .login(UpstreamGatewayClient::new) 125 | .blockOptional() 126 | .orElseThrow(RuntimeException::new); 127 | 128 | // Proof of concept allowing leader management via API 129 | LogoutHttpServer.startAsync(client); 130 | 131 | client.onDisconnect().block(); 132 | } 133 | 134 | private static final Logger log = Loggers.getLogger(ExampleRSocketLeader.class); 135 | } 136 | -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rabbitmq/shared/ExampleRabbitLocalCacheLeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.UpstreamGatewayClient; 26 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 27 | import discord4j.connect.rabbitmq.ConnectRabbitMQSettings; 28 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSink; 29 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSource; 30 | import discord4j.connect.rabbitmq.gateway.RabbitMQSinkMapper; 31 | import discord4j.connect.rabbitmq.gateway.RabbitMQSourceMapper; 32 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 33 | import discord4j.connect.rsocket.router.RSocketRouter; 34 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 35 | import discord4j.connect.rsocket.shard.RSocketShardCoordinator; 36 | import discord4j.connect.support.LogoutHttpServer; 37 | import discord4j.core.DiscordClient; 38 | import discord4j.core.GatewayDiscordClient; 39 | import discord4j.core.event.dispatch.DispatchEventMapper; 40 | import discord4j.core.object.presence.ClientPresence; 41 | import discord4j.core.shard.ShardingStrategy; 42 | import discord4j.gateway.intent.Intent; 43 | import discord4j.gateway.intent.IntentSet; 44 | import discord4j.store.api.noop.NoOpStoreService; 45 | import reactor.core.publisher.Mono; 46 | import reactor.rabbitmq.QueueSpecification; 47 | import reactor.util.Logger; 48 | import reactor.util.Loggers; 49 | 50 | import java.net.InetSocketAddress; 51 | 52 | /** 53 | * An example distributed Discord4J leader, or a node that is capable of connecting to Discord Gateway and routing 54 | * its messages to other nodes, across JVM boundaries. 55 | *

56 | * In particular, this example covers: 57 | *

    58 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 59 | *
  • Connecting to a distributed Router for API requests
  • 60 | *
  • Connecting to a distributed ShardCoordinator for connect/IDENTIFY request rate limiting
  • 61 | *
  • Connecting to a RabbitMQ broker to send and receive messages from other nodes
  • 62 | *
  • Disabling entity cache capabilities to reduce memory footprint, focusing on message routing ONLY
  • 63 | *
64 | */ 65 | public class ExampleRabbitLocalCacheLeader { 66 | 67 | 68 | public static void main(String[] args) { 69 | 70 | /* 71 | * Define the location of the Global Router Server (GRS). A GRS combines coordinated routing across API 72 | * requests while also dealing with the global rate limits. 73 | * 74 | * We will use RSocket GRS in this example: see ExampleRSocketGlobalRouterServer 75 | */ 76 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 77 | 78 | /* 79 | * Define the location of the Shard Coordinator Server (SCS). An SCS establishes predictable ordering across 80 | * multiple leaders attempting to connect to the Gateway. 81 | * 82 | * We will use RSocket SCS in this example: see ExampleRSocket 83 | */ 84 | InetSocketAddress coordinatorServerAddress = new InetSocketAddress(Constants.SHARD_COORDINATOR_SERVER_HOST, Constants.SHARD_COORDINATOR_SERVER_PORT); 85 | 86 | /* 87 | * Create a default factory for working with Jackson, this can be reused across the application. 88 | */ 89 | JacksonResources jackson = JacksonResources.create(); 90 | 91 | /* 92 | * Define the sharding strategy. Refer to the class docs for more details or options. 93 | */ 94 | ShardingStrategy shardingStrategy = ShardingStrategy.recommended(); 95 | 96 | /* 97 | * Define the key resources for working with RabbitMQ. 98 | * - ConnectRabbitMQ defines the parameters to a server 99 | * - RabbitMQSinkMapper will be used to PRODUCE payloads to other nodes 100 | * - "createBinarySinkToDirect" will create binary messages, sent to the "payload" queue directly. 101 | * - RabbitMQSourceMapper will be used to CONSUME payloads from other nodes 102 | * - "createBinarySource" will read binary messages 103 | */ 104 | ConnectRabbitMQ rabbitMQ; 105 | if (!Constants.RABBITMQ_HOST.isEmpty()) { 106 | ConnectRabbitMQSettings settings = ConnectRabbitMQSettings.create().withAddress(Constants.RABBITMQ_HOST, Constants.RABBITMQ_PORT); 107 | rabbitMQ = ConnectRabbitMQ.createFromSettings(settings); 108 | } else { 109 | rabbitMQ = ConnectRabbitMQ.createDefault(); 110 | } 111 | RabbitMQSinkMapper sink = RabbitMQSinkMapper.createBinarySinkToDirect("payload"); 112 | RabbitMQSourceMapper source = RabbitMQSourceMapper.createBinarySource(); 113 | 114 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 115 | .setJacksonResources(jackson) 116 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 117 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 118 | .build(RSocketRouter::new) 119 | .gateway() 120 | .setSharding(shardingStrategy) 121 | // Properly coordinate IDENTIFY attempts across all shards 122 | .setShardCoordinator(RSocketShardCoordinator.createWithServerAddress(coordinatorServerAddress)).setInitialPresence(s -> ClientPresence.invisible()) 123 | // Disable invalidation strategy, event publishing and entity cache to save memory usage 124 | .setDispatchEventMapper(DispatchEventMapper.discardEvents()) 125 | .setStore(Store.fromLayout(LegacyStoreLayout.of(new NoOpStoreService()))) 126 | // .setInvalidationStrategy(InvalidationStrategy.disable()) 127 | .setEnabledIntents(IntentSet.all()) 128 | // Turn this gateway into a RabbitMQ-based one 129 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 130 | RabbitMQPayloadSink.create(sink, rabbitMQ) 131 | .withBeforeSendFunction((rmq, meta) -> rmq.getSender() 132 | .declare(QueueSpecification.queue(meta.getRoutingKey())) 133 | .onErrorResume(t -> Mono.empty())), 134 | RabbitMQPayloadSource.create(source, rabbitMQ, "gateway"))) 135 | // UpstreamGatewayClient connects to Discord Gateway and forwards payloads to other nodes 136 | .login(UpstreamGatewayClient::new) 137 | .blockOptional() 138 | .orElseThrow(RuntimeException::new); 139 | 140 | LogoutHttpServer.startAsync(client); 141 | client.onDisconnect().block(); 142 | rabbitMQ.close(); 143 | } 144 | 145 | private static final Logger log = Loggers.getLogger(ExampleRabbitLocalCacheLeader.class); 146 | } 147 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /examples/src/main/java/discord4j/connect/rabbitmq/shared/ExampleRabbitDistributedCacheLeader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of Discord4J. 3 | * 4 | * Discord4J is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Lesser General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * Discord4J is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public License 15 | * along with Discord4J. If not, see . 16 | */ 17 | 18 | package discord4j.connect.rabbitmq.shared; 19 | 20 | import discord4j.common.JacksonResources; 21 | import discord4j.common.store.Store; 22 | import discord4j.common.store.legacy.LegacyStoreLayout; 23 | import discord4j.connect.Constants; 24 | import discord4j.connect.common.ConnectGatewayOptions; 25 | import discord4j.connect.common.UpstreamGatewayClient; 26 | import discord4j.connect.rabbitmq.ConnectRabbitMQ; 27 | import discord4j.connect.rabbitmq.ConnectRabbitMQSettings; 28 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSink; 29 | import discord4j.connect.rabbitmq.gateway.RabbitMQPayloadSource; 30 | import discord4j.connect.rabbitmq.gateway.RabbitMQSinkMapper; 31 | import discord4j.connect.rabbitmq.gateway.RabbitMQSourceMapper; 32 | import discord4j.connect.rsocket.global.RSocketGlobalRateLimiter; 33 | import discord4j.connect.rsocket.router.RSocketRouter; 34 | import discord4j.connect.rsocket.router.RSocketRouterOptions; 35 | import discord4j.connect.rsocket.shard.RSocketShardCoordinator; 36 | import discord4j.connect.support.LogoutHttpServer; 37 | import discord4j.core.DiscordClient; 38 | import discord4j.core.GatewayDiscordClient; 39 | import discord4j.core.event.dispatch.DispatchEventMapper; 40 | import discord4j.core.object.presence.ClientPresence; 41 | import discord4j.core.shard.ShardingStrategy; 42 | import discord4j.gateway.intent.Intent; 43 | import discord4j.gateway.intent.IntentSet; 44 | import discord4j.store.redis.RedisStoreService; 45 | import io.lettuce.core.RedisClient; 46 | import reactor.util.Logger; 47 | import reactor.util.Loggers; 48 | 49 | import java.net.InetSocketAddress; 50 | 51 | /** 52 | * An example distributed Discord4J leader, or a node that is capable of connecting to Discord Gateway and routing 53 | * its messages to other nodes, across JVM boundaries. 54 | *

55 | * In particular, this example covers: 56 | *

    57 | *
  • Connecting to a distributed GlobalRateLimiter for API requests
  • 58 | *
  • Connecting to a distributed Router for API requests
  • 59 | *
  • Connecting to a distributed ShardCoordinator for connect/IDENTIFY request rate limiting
  • 60 | *
  • Connecting to a RabbitMQ broker to send and receive messages from other nodes
  • 61 | *
  • Connecting to a redis server to use it as entity cache, writing every update from the Gateway
  • 62 | *
63 | */ 64 | public class ExampleRabbitDistributedCacheLeader { 65 | 66 | public static void main(String[] args) { 67 | 68 | /* 69 | * Define the location of the Global Router Server (GRS). A GRS combines coordinated routing across API 70 | * requests while also dealing with the global rate limits. 71 | * 72 | * We will use RSocket GRS in this example: see ExampleRSocketGlobalRouterServer 73 | */ 74 | InetSocketAddress globalRouterServerAddress = new InetSocketAddress(Constants.GLOBAL_ROUTER_SERVER_HOST, Constants.GLOBAL_ROUTER_SERVER_PORT); 75 | 76 | /* 77 | * Define the location of the Shard Coordinator Server (SCS). An SCS establishes predictable ordering across 78 | * multiple leaders attempting to connect to the Gateway. 79 | * 80 | * We will use RSocket SCS in this example: see ExampleRSocket 81 | */ 82 | InetSocketAddress coordinatorServerAddress = new InetSocketAddress(Constants.SHARD_COORDINATOR_SERVER_HOST, Constants.SHARD_COORDINATOR_SERVER_PORT); 83 | 84 | /* 85 | * Define the redis server that will be used as entity cache. 86 | */ 87 | RedisClient redisClient = RedisClient.create(Constants.REDIS_CLIENT_URI); 88 | 89 | /* 90 | * Create a default factory for working with Jackson, this can be reused across the application. 91 | */ 92 | JacksonResources jackson = JacksonResources.create(); 93 | 94 | /* 95 | * Define the sharding strategy. Refer to the class docs for more details or options. 96 | */ 97 | ShardingStrategy shardingStrategy = ShardingStrategy.recommended(); 98 | 99 | /* 100 | * Define the key resources for working with RabbitMQ. 101 | * - ConnectRabbitMQ defines the parameters to a server 102 | * - RabbitMQSinkMapper will be used to PRODUCE payloads to other nodes 103 | * - "createBinarySinkToDirect" will create binary messages, sent to the "payload" queue directly. 104 | * - RabbitMQSourceMapper will be used to CONSUME payloads from other nodes 105 | * - "createBinarySource" will read binary messages 106 | */ 107 | ConnectRabbitMQ rabbitMQ; 108 | if (!Constants.RABBITMQ_HOST.isEmpty()) { 109 | ConnectRabbitMQSettings settings = ConnectRabbitMQSettings.create().withAddress(Constants.RABBITMQ_HOST, Constants.RABBITMQ_PORT); 110 | rabbitMQ = ConnectRabbitMQ.createFromSettings(settings); 111 | } else { 112 | rabbitMQ = ConnectRabbitMQ.createDefault(); 113 | } 114 | RabbitMQSinkMapper sink = RabbitMQSinkMapper.createBinarySinkToDirect("payload"); 115 | RabbitMQSourceMapper source = RabbitMQSourceMapper.createBinarySource(); 116 | 117 | GatewayDiscordClient client = DiscordClient.builder(System.getenv("BOT_TOKEN")) 118 | .setJacksonResources(jackson) 119 | .setGlobalRateLimiter(RSocketGlobalRateLimiter.createWithServerAddress(globalRouterServerAddress)) 120 | .setExtraOptions(o -> new RSocketRouterOptions(o, request -> globalRouterServerAddress)) 121 | .build(RSocketRouter::new) 122 | .gateway() 123 | .setSharding(shardingStrategy) 124 | // Properly coordinate IDENTIFY attempts across all shards 125 | .setShardCoordinator(RSocketShardCoordinator.createWithServerAddress(coordinatorServerAddress)) 126 | .setDisabledIntents(IntentSet.of( 127 | Intent.GUILD_PRESENCES, 128 | Intent.GUILD_MESSAGE_TYPING, 129 | Intent.DIRECT_MESSAGE_TYPING)).setInitialPresence(s -> ClientPresence.invisible()) 130 | // Disable invalidation strategy and event publishing to save memory usage 131 | //.setInvalidationStrategy(InvalidationStrategy.disable()) 132 | .setEnabledIntents(IntentSet.all()) 133 | .setDispatchEventMapper(DispatchEventMapper.discardEvents()) 134 | // Define the entity cache 135 | .setStore(Store.fromLayout(LegacyStoreLayout.of(RedisStoreService.builder() 136 | .redisClient(redisClient) 137 | .useSharedConnection(false) 138 | .build()))) 139 | // Turn this gateway into a RabbitMQ-based one 140 | .setExtraOptions(o -> new ConnectGatewayOptions(o, 141 | RabbitMQPayloadSink.create(sink, rabbitMQ), 142 | RabbitMQPayloadSource.create(source, rabbitMQ, "gateway"))) 143 | // UpstreamGatewayClient connects to Discord Gateway and forwards payloads to other nodes 144 | .login(UpstreamGatewayClient::new) 145 | .blockOptional() 146 | .orElseThrow(RuntimeException::new); 147 | 148 | LogoutHttpServer.startAsync(client); 149 | client.onDisconnect().block(); 150 | rabbitMQ.close(); 151 | } 152 | 153 | private static final Logger log = Loggers.getLogger(ExampleRabbitDistributedCacheLeader.class); 154 | } 155 | --------------------------------------------------------------------------------