source) {
99 | Publisher extends OutboundMessage> 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 |
--------------------------------------------------------------------------------